diff --git a/.env b/.env
index 43770b2e..f1ad1d9f 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/CodeQL.yml b/.github/workflows/CodeQL.yml
new file mode 100644
index 00000000..e8fb4c00
--- /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 00000000..8f2abbc0
--- /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 00000000..957f4a7d
--- /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 00000000..e23fe173
--- /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 00000000..4a58ee66
--- /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 00000000..3fc5c1bf
--- /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 00000000..443566d3
--- /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 00000000..66b87be7
--- /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
diff --git a/Makefile b/Makefile
index 93313266..73d0341b 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}
@@ -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/cmd/device-agent/cli/register.go b/cmd/device-agent/cli/register.go
index b3cc6319..6b4849bd 100644
--- a/cmd/device-agent/cli/register.go
+++ b/cmd/device-agent/cli/register.go
@@ -38,6 +38,7 @@ type envoyRegistrationConfig struct {
Labels valueobject.Labels `json:"labels"`
SkipCommonPythonFiles bool `json:"skip_common_python_files" yaml:"skipCommonPythonFiles"`
EnablePSP bool `json:"enable_psp" yaml:"enablePSP"`
+ LessPrivileged bool `json:"less_privileged" yaml:"lessPrivileged"`
RegistryConfig valueobject.KubeRegistryConfig `json:"registry_config" yaml:"registryConfig"`
}
diff --git a/doc/OpenFL_Guide.md b/doc/OpenFL_Guide.md
index 5f0387ae..4239dadd 100644
--- a/doc/OpenFL_Guide.md
+++ b/doc/OpenFL_Guide.md
@@ -1,6 +1,6 @@
# Managing OpenFL Federations
-This document provides an end-to-end guide to set up an OpenFL federation using FedLCM service. Currently, director-based mode is supported.
+This document provides an end-to-end guide to set up an OpenFL v1.5 federation using FedLCM service. Currently, director-based mode is supported.
The overall deployment architecture is in below diagram:
@@ -12,7 +12,8 @@ The high-level steps are
1. FedLCM deploys the KubeFATE service into a central K8s cluster and use this KubeFATE service to deploy OpenFL director component, which includes a director service and a Jupyter Lab service.
2. On each device/node/machine that will do the actual FML training using their local data, a device-agent program is launched to register this device/node/machine to FedLCM. The registration information contains a KubeConfig file so the FedLCM can further operate the K8s cluster on the device/node/machine.
3. FedLCM deploys the KubeFATE service and the OpenFL envoy components onto the device/node/machine's K8s cluster.
-4. the envoy is configured with the address of the director service, so it will register to the director service upon started.
+4. The envoy is configured with the address of the director service, so it will register to the director service upon started.
+5. Users can use the deployed Jupyter Lab service to work with the OpenFL federation to run FL experiments.
> Currently the core images for FedLCM's OpenFL federations are not made public yet, please talk with the maintainer for the access details.
@@ -59,12 +60,12 @@ With the service running and CA configured, we can start create the OpenFL feder
### Add Kubernetes Infrastructure
Kubernetes clusters are considered as Infrastructures in the FedLCM service. All the other installation are performed on these K8s clusters. To deploy the director, one must firstly add the target K8s into the system.
-Go to the "Infrastructure" section and click the "NEW" button. What needs to be filled is the KubeConfig content that FedLCM will use to connect the K8s cluster.
+Go to the "Infrastructure" section and click the "NEW" button. What needs to be filled is the kubeconfig content that FedLCM will use to connect the K8s cluster.
-**Even though for FATE we can support namespace-wide admin, the user configured in the KubeConfig for OpenFL should have the privilege to create all core K8s resource including namespace, deployment, configmap, role, secret, etc. We haven't tested the exact rules. If not sure, use the cluster-admin ClusterRole**
+**By default, FedLCM expects the kubeconfig to have cluster-admin permission to operate the cluster. An alternative is using namespace wide admin permission. To use this less privileged config, enable the "Limited to certain namespaces" option and input the namespace(s) this kubeconfig can only use**
-
+
Click "TEST" to make sure the cluster is available. And "SUBMIT" to save the new infrastructure.
@@ -74,7 +75,7 @@ The "FATE Registry Configuration" section is for FATE usage and not for OpenFL,
### Install KubeFATE Endpoint
In the "Endpoint" section, we can install KubeFATE service onto the K8s infrastructure. And later it can be used to deploy OpenFL components.
-To add a new KubeFATE endpoint, select the infrastructure and the system will try to find if there is already a KubeFATE service running.
+To add a new KubeFATE endpoint, select the infrastructure (and the namespace if the kubeconfig has less privilege) and then system will try to find if there is already a KubeFATE service running.
If yes, the system will add the KubeFATE into its database directly. If no, the system will provide an installation step as shown below:
@@ -150,7 +151,7 @@ class FedLCMDummyShardDescriptor(DummyShardDescriptor):
f'target shape: {self.target_shape}'
)
"""Return the dataset description."""
- return 'This is dummy data shard descriptor provided by FedLCM project. You should implement your own data '
+ return 'This is dummy data shard descriptor provided by FedLCM project. You should implement your own data ' \
'loader to load your local data for each Envoy.'
```
@@ -167,6 +168,7 @@ After the federation is created, we can create the director. Click "NEW" under t
Several things to note:
* Try to give a unique name and namespace for the director as it may cause some issue if the name and namespace conflicts with existing ones in the cluster.
+* If the selected endpoint and infrastructure is using less privileged kubeconfig, the namespace is predefined and cannot be changed.
* It is suggested to choose "Install certificates for me" in the Certificate section. Only select "I will manually install certificates" if you want to import your own certificate instead of using the CA to create a new one. Refer to the OpenFL helm chart guide on how to import existing one.
* Choose "NodePort" if your cluster doesn't have any controller that can handle `LoadBalancer` type of service.
* If your cluster doesn't enable [Pod Security Policies](https://kubernetes.io/docs/concepts/security/pod-security-policy/), you don't have to enable it in the "Pod Security Policy Configuration".
@@ -186,59 +188,37 @@ You can click into the director details page and keep refreshing it. If things w
Now, we can open up the deployed Jupyter Lab system by clicking the Jupyter Notebook link, input the password we just configured and open a notebook we want to use, or create a new notebook where we can write our own code.
* For this example we use the `interactive_api/Tensorflow_MNIST/workspace/Tensorflow_MNIST.ipynb` notebook.
-* If the federation is configured with the default Unbounded Shard Descriptor, you can use `interactive_api/Tensorflow_MNIST_With_Dummy_Envoy_Shard_FedLCM/Tensorflow_MNIST_With_Dummy_Envoy_Shard_FedLCM.ipynb` as an example on how to put real data reading logic in the `DataInterface`.
+* If the federation is configured with the default Unbounded Shard Descriptor, you can use [Tensorflow_MNIST_With_Dummy_Envoy_Shard_FedLCM.ipynb](./examples/Tensorflow_MNIST_With_Dummy_Envoy_Shard_FedLCM/Tensorflow_MNIST_With_Dummy_Envoy_Shard_FedLCM.ipynb) as an example on how to put real data reading logic in the `DataInterface`. Just upload this file to the Jupyter Lab instance and follow the guide there.
-The content in the notebook is from OpenFL's [official repo](https://github.com/intel/openfl/tree/develop/openfl-tutorials). We assume you have basic knowledge on how to use the OpenFL sdk to work with the director API.
+The existing content in the notebook service is from OpenFL's [official repo](https://github.com/intel/openfl/tree/develop/openfl-tutorials). We assume you have basic knowledge on how to use the OpenFL sdk to work with the director API.
-For the `federation` creation part, most of the examples are using below code:
+For the `federation` creation part, to connect to the director from the deployed Jupyter Lab instance, use the following code:
```python
# Create a federation
from openfl.interface.interactive_api.federation import Federation
-# please use the same identificator that was used in signed certificate
client_id = 'api'
-cert_dir = 'cert'
-director_node_fqdn = 'localhost'
-director_port=50051
-# 1) Run with API layer - Director mTLS
-# If the user wants to enable mTLS their must provide CA root chain, and signed key pair to the federation interface
-# cert_chain = f'{cert_dir}/root_ca.crt'
-# api_certificate = f'{cert_dir}/{client_id}.crt'
-# api_private_key = f'{cert_dir}/{client_id}.key'
-
-# federation = Federation(
-# client_id=client_id,
-# director_node_fqdn=director_node_fqdn,
-# director_port=director_port,
-# cert_chain=cert_chain,
-# api_cert=api_certificate,
-# api_private_key=api_private_key
-# )
-
-# --------------------------------------------------------------------------------------------------------------------
-
-# 2) Run with TLS disabled (trusted environment)
-# Federation can also determine local fqdn automatically
+director_node_fqdn = 'director'
+director_port = 50051
+cert_chain = '/openfl/workspace/cert/root_ca.crt'
+api_cert = '/openfl/workspace/cert/notebook.crt'
+api_private_key = '/openfl/workspace/cert/priv.key'
+
federation = Federation(
- client_id=client_id,
- director_node_fqdn=director_node_fqdn,
- director_port=director_port,
- tls=False
+ client_id=client_id,
+ director_node_fqdn=director_node_fqdn,
+ director_port=director_port,
+ cert_chain=cert_chain,
+ api_cert=api_cert,
+ api_private_key=api_private_key
)
```
-But we actually don't need that to be this complicated, since we have internally configured the SDK to work with the deployed director by default.
-So to create a federation that represent the director, use below code is sufficient:
-
-```python
-# Create a federation
-from openfl.interface.interactive_api.federation import Federation
-
-federation = Federation()
-```
+The certificates, keys, director listening port and other settings are pre-configured by FedLCM so we can use them as shown above.
And if we call the federation's `get_shard_registry()` API, we will notice the returned data is an empty dict. It is expected as current there is no "client (envoy)" created yet.
+
We can move on now.
## Register Device/Node/Machine to the Federation
@@ -291,6 +271,7 @@ chartUUID: "type: string, default:
"
labels: "type: map, default:, the labels for the envoy will be a merge from this field and labels of the token"
skipCommonPythonFiles: "type: bool, default: false, if true, the python shard descriptor files configured in the federation will not be used, and user will need to manually import the python files."
enablePSP: "type: bool, default: false, if true, the deployd envoy pod will have PSP associated"
+lessPrivileged: "type: bool, default: false, if ture, all the components will be installed in one namespace. Make sure the namespace already exists and the kubeconfig has the permission to operate in this namespace"
registryConfig:
useRegistry: "type: bool, default false"
registry: "type: string, default , if set, the image will be /fedlcm-openfl:v0.1.0"
@@ -303,6 +284,8 @@ registryConfig:
> The registryConfig will affect both KubeFATE and OpenFL related images. Make sure you have all the images in your customized registry.
+**If the kubeconfig used for Envoy registration only has namespace wide admin permissions, we must specify the namespace and set `lessPrivileged` to true in the extra-config file**
+
### Start Registration and Envoy Deployment
Assume on the device/node/machine we have the `openfl-device-agent` program, we can start the registration process
@@ -379,4 +362,64 @@ Now, we have finished the whole process of installing FedLCM to deploying OpenFL
## Caveats
* If there are errors when running experiment in the envoy side, the experiment may become "never finished". This is OpenFL's own issue. Currently, the workaround is restart the director and envoy.
* There is no "unregister" support in OpenFL yet so if we delete an envoy, it may still show in the director's `federation.get_shard_registry()` API. But its status is offline so director won't send future experiment to this removed envoy.
-* For the KubeConfig used in the infrastructure, We haven't tested what are exactly the minimal requirement permissions.
\ No newline at end of file
+* To facilitate the envoy's container image registry configuration, we can set the `LIFECYCLEMANAGER_OPENFL_ENVOY_REGISTRY_OVERRIDE` environment variable for FedLCM service, which will take precedence of the registry url configured in the `extra-config` file used by the device agent.
+
+### Preparing the fedlcm-openfl Image Locally & Using You Own Registry
+The Director, Envoy and Jupyter Lab container deployed by FedLCM all use a same container image. This image is built using OpenFL's official dockerfile but with small modifications. Here is how to build this image locally and use your own image registry:
+
+1. Checkout OpenFL's v1.5 release code
+```bash
+git clone -b v1.5 https://github.com/securefederatedai/openfl.git
+cd openfl
+```
+
+2. Run the following command to add the modification
+```bash
+patch -p1 </fedlcm-openfl:v0.3.0 -f openfl-docker/Dockerfile.base .
+docker push /fedlcm-openfl:v0.3.0
+```
+
+For example, assuming we want to use my dockerhub account "foobar", then the command would look like:
+```bash
+docker build -t foobar/fedlcm-openfl:v0.3.0 -f openfl-docker/Dockerfile.base .
+docker push foobar/fedlcm-openfl:v0.3.0
+```
+
+4. When deploying directors, we need to set the registry url to `foobar`.
+5. When registering envoys, we need to configure the registry in `--extra-config` or set the `LIFECYCLEMANAGER_OPENFL_ENVOY_REGISTRY_OVERRIDE` environment variable of FedLCM service to `foobar`.
+
+With the above steps, we will be using our locally built image from our own container image registry.
\ No newline at end of file
diff --git a/doc/examples/Tensorflow_MNIST_With_Dummy_Envoy_Shard_FedLCM/Tensorflow_MNIST_With_Dummy_Envoy_Shard_FedLCM.ipynb b/doc/examples/Tensorflow_MNIST_With_Dummy_Envoy_Shard_FedLCM/Tensorflow_MNIST_With_Dummy_Envoy_Shard_FedLCM.ipynb
new file mode 100644
index 00000000..49971b00
--- /dev/null
+++ b/doc/examples/Tensorflow_MNIST_With_Dummy_Envoy_Shard_FedLCM/Tensorflow_MNIST_With_Dummy_Envoy_Shard_FedLCM.ipynb
@@ -0,0 +1,542 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "26fdd9ed",
+ "metadata": {},
+ "source": [
+ "# Federated Tensorflow Mnist Tutorial With Dummy Envoy Shard Descriptor\n",
+ "\n",
+ "This tutorial is based on the \"Tensorflow_MNIST\" tutorial but with changes to demostrate how to use the dummy shard descriptor. By default, OpenFL federation created by FedLCM will only use this dummy shard descriptor. And user have to specify how envoy can retreive local data by defining the `DataInterface`.\n",
+ "\n",
+ "**Understanding of the original \"Tensorflow_MNIST\" is highly recommended!**"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "d0570122",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Install dependencies if not already installed\n",
+ "!python -m pip install tensorflow==2.8"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "6479191e-bb78-44cd-9462-8e420de6aa63",
+ "metadata": {},
+ "source": [
+ "## Imports"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "06915d29-07bc-47e2-8d4a-ff73428ac104",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "import tensorflow as tf\n",
+ "print('TensorFlow', tf.__version__)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "246f9c98",
+ "metadata": {
+ "tags": []
+ },
+ "source": [
+ "## Connect to the Federation\n",
+ "\n",
+ "This cell connects this notebook to the Federation.\n",
+ "\n",
+ "Note *the parameters provided in the cell is for the Jupyter Lab instance deployed along with the director by FedLCM. Change the parameters if necessary if you are running this notebook in other environments.*"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "d657e463",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Create a federation\n",
+ "from openfl.interface.interactive_api.federation import Federation\n",
+ "\n",
+ "client_id = 'api'\n",
+ "director_node_fqdn = 'director'\n",
+ "director_port = 50051\n",
+ "cert_chain = '/openfl/workspace/cert/root_ca.crt'\n",
+ "api_cert = '/openfl/workspace/cert/notebook.crt'\n",
+ "api_private_key = '/openfl/workspace/cert/priv.key'\n",
+ "\n",
+ "federation = Federation(\n",
+ " client_id=client_id,\n",
+ " director_node_fqdn=director_node_fqdn,\n",
+ " director_port=director_port,\n",
+ " cert_chain=cert_chain,\n",
+ " api_cert=api_cert,\n",
+ " api_private_key=api_private_key\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "74c57e78-dfc9-47f7-8a4c-96fdc48f64fc",
+ "metadata": {
+ "tags": []
+ },
+ "source": [
+ "## Query Datasets from Shard Registry"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "47dcfab3",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "shard_registry = federation.get_shard_registry()\n",
+ "shard_registry"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "a2a6c237",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# First, request a dummy_shard_desc that holds information about the federated dataset \n",
+ "# If the shard descriptor is the default dummy one, the shape of the sample and target will be \"1\", which is not the actual data shape\n",
+ "dummy_shard_desc = federation.get_dummy_shard_descriptor(size=10)\n",
+ "dummy_shard_dataset = dummy_shard_desc.get_dataset('train')\n",
+ "sample, target = dummy_shard_dataset[0]\n",
+ "f\"Sample shape: {sample.shape}, target shape: {target.shape}\""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "cc0dbdbd",
+ "metadata": {},
+ "source": [
+ "## Describing FL experimen"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "fc88700a",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "from openfl.interface.interactive_api.experiment import TaskInterface\n",
+ "from openfl.interface.interactive_api.experiment import ModelInterface\n",
+ "from openfl.interface.interactive_api.experiment import FLExperiment"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3b468ae1",
+ "metadata": {},
+ "source": [
+ "### Register model"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "06545bbb",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# Define model\n",
+ "model = tf.keras.Sequential([\n",
+ " tf.keras.layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)),\n",
+ " tf.keras.layers.MaxPooling2D((2, 2)),\n",
+ " tf.keras.layers.BatchNormalization(),\n",
+ " tf.keras.layers.Conv2D(64, (3, 3), activation='relu', input_shape=(28, 28, 1)),\n",
+ " tf.keras.layers.MaxPooling2D((2, 2)),\n",
+ " tf.keras.layers.BatchNormalization(),\n",
+ " tf.keras.layers.Flatten(),\n",
+ " tf.keras.layers.Dense(10, activation=None),\n",
+ "], name='simplecnn')\n",
+ "model.summary()\n",
+ "\n",
+ "# Define optimizer\n",
+ "optimizer = tf.optimizers.Adam(learning_rate=1e-3)\n",
+ "\n",
+ "# Loss and metrics. These will be used later.\n",
+ "loss_fn = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)\n",
+ "train_acc_metric = tf.keras.metrics.SparseCategoricalAccuracy()\n",
+ "val_acc_metric = tf.keras.metrics.SparseCategoricalAccuracy()\n",
+ "\n",
+ "# Create ModelInterface\n",
+ "framework_adapter = 'openfl.plugins.frameworks_adapters.keras_adapter.FrameworkAdapterPlugin'\n",
+ "MI = ModelInterface(model=model, optimizer=optimizer, framework_plugin=framework_adapter)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b0979470",
+ "metadata": {},
+ "source": [
+ "### Register dataset\n",
+ "\n",
+ "This is the main difference between this tutorial and the original \"Tensorflow_MNIST\" tutorial. Instead of configuring each Envoy with a specific shard descriptor class, the dummy one is configuraed for them, and we move the data retrieval logic into this subclass from `DataInterface`. For other tasks not using the MNIST dataset, we can write our own local data retrival logic here."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "d8c9eb50",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "import os\n",
+ "import requests\n",
+ "import numpy as np\n",
+ "from tensorflow.keras.utils import Sequence\n",
+ "from openfl.interface.interactive_api.shard_descriptor import ShardDataset\n",
+ "\n",
+ "from openfl.interface.interactive_api.experiment import DataInterface\n",
+ "\n",
+ "class MnistShardDataset(ShardDataset):\n",
+ " \"\"\"Mnist Shard dataset class.\"\"\"\n",
+ "\n",
+ " def __init__(self, x, y, rank=1, worldsize=1):\n",
+ " \"\"\"Initialize TinyImageNetDataset.\"\"\"\n",
+ " self.rank = rank\n",
+ " self.worldsize = worldsize\n",
+ " self.x = x[self.rank - 1::self.worldsize]\n",
+ " self.y = y[self.rank - 1::self.worldsize]\n",
+ "\n",
+ " def __getitem__(self, index: int):\n",
+ " \"\"\"Return an item by the index.\"\"\"\n",
+ " return self.x[index], self.y[index]\n",
+ "\n",
+ " def __len__(self):\n",
+ " \"\"\"Return the len of the dataset.\"\"\"\n",
+ " return len(self.x)\n",
+ " \n",
+ "\n",
+ "class DataGenerator(Sequence):\n",
+ "\n",
+ " def __init__(self, data_set, batch_size):\n",
+ " self.data_set = data_set\n",
+ " self.batch_size = batch_size\n",
+ " self.indices = np.arange(len(data_set))\n",
+ " self.on_epoch_end()\n",
+ "\n",
+ " def __len__(self):\n",
+ " return len(self.indices) // self.batch_size\n",
+ "\n",
+ " def __getitem__(self, index):\n",
+ " index = self.indices[index * self.batch_size:(index + 1) * self.batch_size]\n",
+ " batch = [self.indices[k] for k in index]\n",
+ "\n",
+ " X, y = self.data_set[batch]\n",
+ " return X, y\n",
+ "\n",
+ " def on_epoch_end(self):\n",
+ " np.random.shuffle(self.indices)\n",
+ "\n",
+ "\n",
+ "class MnistFedDataset(DataInterface):\n",
+ "\n",
+ " def __init__(self, **kwargs):\n",
+ " super().__init__(**kwargs)\n",
+ "\n",
+ " @property\n",
+ " def shard_descriptor(self):\n",
+ " return self._shard_descriptor\n",
+ "\n",
+ " @shard_descriptor.setter\n",
+ " def shard_descriptor(self, shard_descriptor):\n",
+ " \"\"\"\n",
+ " Describe per-collaborator procedures or sharding.\n",
+ "\n",
+ " This method will be called during a collaborator initialization.\n",
+ " Local shard_descriptor will be set by Envoy.\n",
+ " \"\"\"\n",
+ " self._shard_descriptor = shard_descriptor\n",
+ " \n",
+ " (x_train, y_train), (x_test, y_test) = self.download_data()\n",
+ " self.train_set = MnistShardDataset(x_train, y_train)\n",
+ " self.valid_set = MnistShardDataset(x_test, y_test)\n",
+ " \n",
+ " def __getitem__(self, index):\n",
+ " return self.shard_descriptor[index]\n",
+ "\n",
+ " def __len__(self):\n",
+ " return len(self.shard_descriptor)\n",
+ " \n",
+ " def get_train_loader(self):\n",
+ " \"\"\"\n",
+ " Output of this method will be provided to tasks with optimizer in contract\n",
+ " \"\"\"\n",
+ " if self.kwargs['train_bs']:\n",
+ " batch_size = self.kwargs['train_bs']\n",
+ " else:\n",
+ " batch_size = 32\n",
+ " return DataGenerator(self.train_set, batch_size=batch_size)\n",
+ "\n",
+ " def get_valid_loader(self):\n",
+ " \"\"\"\n",
+ " Output of this method will be provided to tasks without optimizer in contract\n",
+ " \"\"\"\n",
+ " if self.kwargs['valid_bs']:\n",
+ " batch_size = self.kwargs['valid_bs']\n",
+ " else:\n",
+ " batch_size = 32\n",
+ " \n",
+ " return DataGenerator(self.valid_set, batch_size=batch_size)\n",
+ "\n",
+ " def get_train_data_size(self):\n",
+ " \"\"\"\n",
+ " Information for aggregation\n",
+ " \"\"\"\n",
+ " \n",
+ " return len(self.train_set)\n",
+ "\n",
+ " def get_valid_data_size(self):\n",
+ " \"\"\"\n",
+ " Information for aggregation\n",
+ " \"\"\"\n",
+ " return len(self.valid_set)\n",
+ " \n",
+ " def download_data(self):\n",
+ " \"\"\"Download prepared dataset.\"\"\"\n",
+ " local_file_path = 'mnist.npz'\n",
+ " mnist_url = 'https://storage.googleapis.com/tensorflow/tf-keras-datasets/mnist.npz'\n",
+ " response = requests.get(mnist_url)\n",
+ " with open(local_file_path, 'wb') as f:\n",
+ " f.write(response.content)\n",
+ "\n",
+ " with np.load(local_file_path) as f:\n",
+ " x_train, y_train = f['x_train'], f['y_train']\n",
+ " x_test, y_test = f['x_test'], f['y_test']\n",
+ " x_train = np.reshape(x_train, (-1, 28, 28, 1))\n",
+ " x_test = np.reshape(x_test, (-1, 28, 28, 1))\n",
+ "\n",
+ " os.remove(local_file_path) # remove mnist.npz\n",
+ " print('Mnist data was loaded!')\n",
+ " return (x_train, y_train), (x_test, y_test)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b0dfb459",
+ "metadata": {},
+ "source": [
+ "### Create Mnist federated dataset"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "4af5c4c2",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "fed_dataset = MnistFedDataset(train_bs=64, valid_bs=512)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "849c165b",
+ "metadata": {},
+ "source": [
+ "## Define and register FL tasks"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "b9649385",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "import time\n",
+ "\n",
+ "\n",
+ "\n",
+ "TI = TaskInterface()\n",
+ "\n",
+ "# from openfl.interface.aggregation_functions import AdagradAdaptiveAggregation # Uncomment this lines to use \n",
+ "# agg_fn = AdagradAdaptiveAggregation(model_interface=MI, learning_rate=0.4) # Adaptive Federated Optimization\n",
+ "# @TI.set_aggregation_function(agg_fn) # alghorithm!\n",
+ "# # See details in the:\n",
+ "# # https://arxiv.org/abs/2003.00295\n",
+ "\n",
+ "@TI.register_fl_task(model='model', data_loader='train_dataset', device='device', optimizer='optimizer') \n",
+ "def train(model, train_dataset, optimizer, device, loss_fn=loss_fn, warmup=False):\n",
+ " start_time = time.time()\n",
+ "\n",
+ " # Iterate over the batches of the dataset.\n",
+ " for step, (x_batch_train, y_batch_train) in enumerate(train_dataset):\n",
+ " with tf.GradientTape() as tape:\n",
+ " logits = model(x_batch_train, training=True)\n",
+ " loss_value = loss_fn(y_batch_train, logits)\n",
+ " grads = tape.gradient(loss_value, model.trainable_weights)\n",
+ " optimizer.apply_gradients(zip(grads, model.trainable_weights))\n",
+ "\n",
+ " # Update training metric.\n",
+ " train_acc_metric.update_state(y_batch_train, logits)\n",
+ "\n",
+ " # Log every 200 batches.\n",
+ " if step % 200 == 0:\n",
+ " print(\n",
+ " \"Training loss (for one batch) at step %d: %.4f\"\n",
+ " % (step, float(loss_value))\n",
+ " )\n",
+ " print(\"Seen so far: %d samples\" % ((step + 1) * 64))\n",
+ " if warmup:\n",
+ " break\n",
+ "\n",
+ " # Display metrics at the end of each epoch.\n",
+ " train_acc = train_acc_metric.result()\n",
+ " print(\"Training acc over epoch: %.4f\" % (float(train_acc),))\n",
+ "\n",
+ " # Reset training metrics at the end of each epoch\n",
+ " train_acc_metric.reset_states()\n",
+ "\n",
+ " \n",
+ " return {'train_acc': train_acc,}\n",
+ "\n",
+ "\n",
+ "@TI.register_fl_task(model='model', data_loader='val_dataset', device='device') \n",
+ "def validate(model, val_dataset, device):\n",
+ " # Run a validation loop at the end of each epoch.\n",
+ " for x_batch_val, y_batch_val in val_dataset:\n",
+ " val_logits = model(x_batch_val, training=False)\n",
+ " # Update val metrics\n",
+ " val_acc_metric.update_state(y_batch_val, val_logits)\n",
+ " val_acc = val_acc_metric.result()\n",
+ " val_acc_metric.reset_states()\n",
+ " print(\"Validation acc: %.4f\" % (float(val_acc),))\n",
+ " \n",
+ " return {'validation_accuracy': val_acc,}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "8f0ebf2d",
+ "metadata": {},
+ "source": [
+ "## Time to start a federated learning experiment"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "d41b7896",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# create an experimnet in federation\n",
+ "experiment_name = 'mnist_experiment'\n",
+ "fl_experiment = FLExperiment(federation=federation, experiment_name=experiment_name)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "9f2074ca-9dcc-48ad-93fe-be4f65479b7b",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# print the default federated learning plan\n",
+ "import openfl.native as fx\n",
+ "print(fx.get_plan(fl_plan=fl_experiment.plan))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "41b44de9",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "# The following command zips the workspace and python requirements to be transfered to collaborator nodes\n",
+ "fl_experiment.start(model_provider=MI, \n",
+ " task_keeper=TI,\n",
+ " data_loader=fed_dataset,\n",
+ " rounds_to_train=5,\n",
+ " opt_treatment='CONTINUE_GLOBAL',\n",
+ " override_config={'aggregator.settings.db_store_rounds': 1, 'compression_pipeline.template': 'openfl.pipelines.KCPipeline', 'compression_pipeline.settings.n_clusters': 2})"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "01fa7cea",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "fl_experiment.stream_metrics()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "ba13c3d4-bc2f-4bdb-86f0-5a72d827d9c8",
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.10.7"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/doc/images/fedlcm-new-infra.jpg b/doc/images/fedlcm-new-infra.jpg
deleted file mode 100644
index 023fc590..00000000
Binary files a/doc/images/fedlcm-new-infra.jpg and /dev/null differ
diff --git a/doc/images/fedlcm-new-infra.png b/doc/images/fedlcm-new-infra.png
new file mode 100644
index 00000000..65f040bd
Binary files /dev/null and b/doc/images/fedlcm-new-infra.png differ
diff --git a/docker-compose.yml b/docker-compose.yml
index be5165f3..0b0d7af0 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -29,7 +29,7 @@ services:
postgres:
image: postgres:13.3
volumes:
- - ./output/data/postgres:/var/lib/postgresql
+ - ./output/data/postgres:/var/lib/postgresql/data
ports:
- "5432:5432"
restart: always
diff --git a/fml-manager/.env b/fml-manager/.env
index b6aa0c6a..843db873 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 bc098bfe..74a1a67a 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/fml-manager/server/api/job.go b/fml-manager/server/api/job.go
index d46d435a..2249d731 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 4be28e5e..91e043c4 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 bc689c65..e5725283 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 f2cdf383..661094b8 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 39f4dc4d..c0a57fcf 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 9f754d13..eb50e708 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, minusCircleIcon, minusIcon, plusIcon} 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, minusCircleIcon, minusIcon, plusIcon);
@Component({
selector: 'app-root',
diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts
index 7cfa8b8d..66a85d67 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 d37da851..5a17cec1 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/endpoint/endpoint-new/endpoint-new.component.ts b/frontend/src/app/view/endpoint/endpoint-new/endpoint-new.component.ts
index 0b45c7f8..407e0373 100644
--- a/frontend/src/app/view/endpoint/endpoint-new/endpoint-new.component.ts
+++ b/frontend/src/app/view/endpoint/endpoint-new/endpoint-new.component.ts
@@ -288,6 +288,7 @@ export class EndpointNewComponent implements OnInit {
})
this.codeMirror.on('change', (cm: any) => {
this.codeMirror.save()
+ this.form.get('install')?.get('yaml')?.setValue(cm.getValue())
})
this.hasYAMLTextAreaDOM = true
}
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 1d08ae32..9cad3380 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}}
@@ -30,6 +30,9 @@
{{'ClusterDetail.detail'|translate}}
{{'CommonlyUse.delete'|translate}}
+
+ {{'CommonlyUse.upgrade'|translate}}
+
@@ -53,13 +56,17 @@ {{'ClusterDetail.detail'|translate}}
{{'NewCluster.namespace'|translate}}:
{{clusterDetail.namespace}}
+
+ {{'CommonlyUse.version'|translate}}:
+ {{clusterDetail.version}}
+
{{'ClusterDetail.partyId'|translate}}:
{{clusterDetail.party_id}}
{{'CommonlyUse.status'|translate}}:
- {{constantGather('participantFATEstatus',
clusterDetail.status).name | translate}}
diff --git a/frontend/src/app/view/federation/cluster-detail/cluster-detail.component.ts b/frontend/src/app/view/federation/cluster-detail/cluster-detail.component.ts
index 8658604f..480a0d90 100644
--- a/frontend/src/app/view/federation/cluster-detail/cluster-detail.component.ts
+++ b/frontend/src/app/view/federation/cluster-detail/cluster-detail.component.ts
@@ -187,4 +187,13 @@ export class ClusterDetailComponent implements OnInit {
this.errorMessage = err.error.message;
});
}
+
+ // Exchange and cluster jump to the upgrade page through toUpgrade
+ toUpgrade(item: {uuid: string, name: string, version: string}, type: string) {
+ this.router.navigate(['/federation', 'fate', this.uuid, 'detail', item.uuid, item.version, type+'-'+item.name, 'upgrade'])
+ }
+
+ back() {
+ this.router.navigate(['federation', 'fate', this.uuid])
+ }
}
diff --git a/frontend/src/app/view/federation/cluster-new/cluster-new.component.html b/frontend/src/app/view/federation/cluster-new/cluster-new.component.html
index eaa8eafd..28d58fed 100644
--- a/frontend/src/app/view/federation/cluster-new/cluster-new.component.html
+++ b/frontend/src/app/view/federation/cluster-new/cluster-new.component.html
@@ -9,6 +9,7 @@ {{'NewCluster.name'| translate}}
{{'NewCluster.basicInformation'| translate}}
+
{{'CommonlyUse.type'| translate}}
@@ -326,6 +327,37 @@ 2. {{'NewCluster.certificate'|translate}}:
{{'CommonlyUse.next' | translate}}
+
+
+ {{'NewCluster.deviceConfiguration'|translate}}
+
+
+
+
+ {{'CommonlyUse.next' | translate}}
+
+
+
{{'InfraProvider.configuration' | translate}}
diff --git a/frontend/src/app/view/federation/cluster-new/cluster-new.component.scss b/frontend/src/app/view/federation/cluster-new/cluster-new.component.scss
index c877fa2c..240b5015 100644
--- a/frontend/src/app/view/federation/cluster-new/cluster-new.component.scss
+++ b/frontend/src/app/view/federation/cluster-new/cluster-new.component.scss
@@ -84,5 +84,56 @@ clr-stepper-panel{
align-items: center;
}
}
+ .gpu-list {
+ display: flex;
+ .gpu-number-warp {
+ display: flex;
+ align-items: center;
+ margin-top: 24px;
+ label {
+ display: block;
+ color: #454545;
+ font-size: .65rem;
+ font-weight: 600;
+ font-weight: var(--clr-forms-label-font-weight, 600);
+ line-height: .9rem;
+ margin-right: 10px;
+ }
+ .gpu-number {
+ display: flex;
+ button {
+ outline: none;
+ color: #0072A3;
+ border: 1px solid #0072A3;
+ width: 25px;
+ height: 25px;
+ background-color: transparent;
+ cursor: pointer;
+ cds-icon{
+ position: relative;
+ left: -2px;
+ top: -2px;
+ }
+ &.minus{
+ border-top-left-radius: 3px;
+ border-bottom-left-radius: 3px;
+ }
+ &.plus {
+ border-top-right-radius: 3px;
+ border-bottom-right-radius: 3px;
+ }
+ }
+ input {
+ width: 40px;
+ height: 25px;
+ outline: none;
+ border: none;
+ border-top: 1px solid #0072A3;
+ border-bottom: 1px solid #0072A3;
+ text-align: center;
+ }
+ }
+ }
+ }
}
}
\ No newline at end of file
diff --git a/frontend/src/app/view/federation/cluster-new/cluster-new.component.ts b/frontend/src/app/view/federation/cluster-new/cluster-new.component.ts
index 32bd2bfc..4084fa69 100644
--- a/frontend/src/app/view/federation/cluster-new/cluster-new.component.ts
+++ b/frontend/src/app/view/federation/cluster-new/cluster-new.component.ts
@@ -112,6 +112,18 @@ export class ClusterNewComponent implements OnInit {
},
])
),
+ deviceConfiguration:this.formBuilder.group(ValidatorGroup([
+ {
+ name: 'enableGPU',
+ type: ['notRequired'],
+ value: false
+ },
+ {
+ name: 'gpuNumber',
+ type: ['number'],
+ value: 1
+ },
+ ])),
registry: this.formBuilder.group(
ValidatorGroup([
{
@@ -281,6 +293,14 @@ export class ClusterNewComponent implements OnInit {
return false
}
}
+ // deviceConfiguration_disabled returns true when the input provided for adding external spark is invalid
+ get deviceConfiguration_disabled () {
+ let result = false
+ if (this.form.get('deviceConfiguration')?.get('enableGPU')?.value) {
+ this.form.get('deviceConfiguration')?.get('gpuNumber')?.value > 0 ? result = false : result = true
+ }
+ return result
+ }
// external_spark_disabled returns true when the input provided for adding external spark is invalid
get external_spark_disabled() {
@@ -811,6 +831,8 @@ export class ClusterNewComponent implements OnInit {
const spark = this.form.get('externalSpark')?.get('enable_external_spark')?.value
const hdfs = this.form.get('externalSpark')?.get('enable_external_hdfs')?.value
const pulsar = this.form.get('externalSpark')?.get('enable_external_pulsar')?.value
+ const gpu = this.form.get('deviceConfiguration')?.get('enableGPU')?.value
+
const {sparkValuess, hdfsValues, pulsarValues} = this.extractSparkHandler(true)
// Build the passed query parameter list
const queryList: any = [
@@ -890,6 +912,17 @@ export class ClusterNewComponent implements OnInit {
queryList.push(item)
})
}
+ if (gpu) {
+ queryList.push({
+ key: 'fateflow_gpu_num',
+ value : this.form.get('deviceConfiguration')?.get('gpuNumber')?.value ? this.form.get('deviceConfiguration')?.get('gpuNumber')?.value : 1
+ })
+ } else {
+ queryList.push({
+ key: 'fateflow_gpu_num',
+ value : 0
+ })
+ }
this.fedservice.getClusterYaml(queryList).
subscribe(
@@ -1022,7 +1055,11 @@ export class ClusterNewComponent implements OnInit {
binding_mode: Number(this.form.controls['certificate'].get('site_portal_server_cert_mode')?.value),
common_name: "",
uuid: this.form.controls['certificate'].get('site_portal_server_cert_uuid')?.value
- }
+ },
+ fateflow_gpu_num: 0
+ }
+ if (this.form.get('deviceConfiguration')?.get('enableGPU')?.value) {
+ clusterInfo.fateflow_gpu_num = this.form.get('deviceConfiguration')?.get('gpuNumber')?.value ? this.form.get('deviceConfiguration')?.get('gpuNumber')?.value : 1
}
@@ -1076,5 +1113,15 @@ export class ClusterNewComponent implements OnInit {
}
+ // gpuActions user increase or decrease the number of gpu
+ gpuActions(num: number) {
+ let value = this.form.get('deviceConfiguration')?.get('gpuNumber')?.value
+ value +=num
+ if (value <= 1) {
+ this.form.get('deviceConfiguration')?.get('gpuNumber')?.setValue(1)
+ } else {
+ this.form.get('deviceConfiguration')?.get('gpuNumber')?.setValue(value)
+ }
+ }
}
diff --git a/frontend/src/app/view/federation/exchange-cluster-upgrade/exchange-cluster-upgrade.component.html b/frontend/src/app/view/federation/exchange-cluster-upgrade/exchange-cluster-upgrade.component.html
new file mode 100644
index 00000000..3c6bee8f
--- /dev/null
+++ b/frontend/src/app/view/federation/exchange-cluster-upgrade/exchange-cluster-upgrade.component.html
@@ -0,0 +1,35 @@
+
+
<<{{'CommonlyUse.back'| translate}}
+
+
{{title}} {{'CommonlyUse.Upgrade'|translate}}
+
+
+
\ No newline at end of file
diff --git a/frontend/src/app/view/federation/exchange-cluster-upgrade/exchange-cluster-upgrade.component.scss b/frontend/src/app/view/federation/exchange-cluster-upgrade/exchange-cluster-upgrade.component.scss
new file mode 100644
index 00000000..02a959bf
--- /dev/null
+++ b/frontend/src/app/view/federation/exchange-cluster-upgrade/exchange-cluster-upgrade.component.scss
@@ -0,0 +1,18 @@
+// mark changes
+.no-warp::ng-deep {
+ label {
+ margin-right: 25px;
+ line-height: 24px;
+ }
+ input {
+ width:300px;
+ }
+ flex-direction: row;
+ align-items: center;
+}
+.btn-sub {
+ margin-top: 20px;
+ display: flex;
+ align-items: center;
+
+}
\ No newline at end of file
diff --git a/frontend/src/app/view/federation/exchange-cluster-upgrade/exchange-cluster-upgrade.component.spec.ts b/frontend/src/app/view/federation/exchange-cluster-upgrade/exchange-cluster-upgrade.component.spec.ts
new file mode 100644
index 00000000..6e95b9c8
--- /dev/null
+++ b/frontend/src/app/view/federation/exchange-cluster-upgrade/exchange-cluster-upgrade.component.spec.ts
@@ -0,0 +1,25 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ExchangeClusterUpgradeComponent } from './exchange-cluster-upgrade.component';
+
+describe('ExchangeClusterUpgradeComponent', () => {
+ let component: ExchangeClusterUpgradeComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ ExchangeClusterUpgradeComponent ]
+ })
+ .compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ExchangeClusterUpgradeComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/frontend/src/app/view/federation/exchange-cluster-upgrade/exchange-cluster-upgrade.component.ts b/frontend/src/app/view/federation/exchange-cluster-upgrade/exchange-cluster-upgrade.component.ts
new file mode 100644
index 00000000..663e1e6d
--- /dev/null
+++ b/frontend/src/app/view/federation/exchange-cluster-upgrade/exchange-cluster-upgrade.component.ts
@@ -0,0 +1,65 @@
+import { Component, OnInit } from '@angular/core';
+import { FormGroup, FormBuilder } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
+import { FedService } from 'src/app/services/federation-fate/fed.service';
+
+@Component({
+ selector: 'app-exchange-cluster-upgrade',
+ templateUrl: './exchange-cluster-upgrade.component.html',
+ styleUrls: ['./exchange-cluster-upgrade.component.scss']
+})
+export class ExchangeClusterUpgradeComponent implements OnInit {
+ form!: FormGroup;
+ upgradeVersionList:string[] = []
+ isShowChartFailed = false
+ errorMessage = ''
+ title = ''
+ fedUuid = ''
+ upgradeUuid = ''
+ version = ''
+ type!: 'cluster' | 'exchange'
+ submiting = false
+ constructor(private formBuilder: FormBuilder, private fedservice: FedService, private route: ActivatedRoute, private router: Router) {
+ this.form = this.formBuilder.group({
+ version: this.formBuilder.group({
+ version: ['']
+ })
+ })
+ }
+
+ ngOnInit(): void {
+ this.route.params.subscribe(
+ value => {
+ this.title = value.name
+ this.fedUuid = value.id
+ this.upgradeUuid = value.uuid
+ this.version = value.version
+ this.type = this.title.split('-')[0].toLocaleLowerCase() as 'cluster' | 'exchange'
+ this.getExchangeClusterUpgradeVersionList(this.fedUuid, this.upgradeUuid, this.type)
+ }
+ )
+ }
+
+ getExchangeClusterUpgradeVersionList(fed_uuid: string, upgrade_uuid: string, type: 'cluster' | 'exchange') {
+ this.fedservice.getExchangeClusterUpgradeVersionList(fed_uuid, upgrade_uuid, type).subscribe(
+ data => {
+ this.upgradeVersionList = data.data.upgradeable_version_list
+ }
+ )
+ }
+
+ upgradeExchangeCluster(fed_uuid: string, upgrade_uuid: string, type: 'cluster' | 'exchange') {
+ this.submiting = true
+ this.fedservice.upgradeExchangeCluster(fed_uuid, upgrade_uuid, type, {upgradeVersion: this.form.controls['version'].get('version')?.value}).subscribe(
+ data => {
+ this.router.navigate(['federation', 'fate', this.fedUuid, this.type, 'detail', this.upgradeUuid])
+ this.submiting = false
+ },
+ err => {
+ this.errorMessage = err.error.message;
+ this.isShowChartFailed = true
+ this.submiting = false
+ }
+ )
+ }
+}
diff --git a/frontend/src/app/view/federation/exchange-detail/exchange-detail.component.html b/frontend/src/app/view/federation/exchange-detail/exchange-detail.component.html
index f61054ae..60109712 100644
--- a/frontend/src/app/view/federation/exchange-detail/exchange-detail.component.html
+++ b/frontend/src/app/view/federation/exchange-detail/exchange-detail.component.html
@@ -1,5 +1,5 @@
-
<<{{'CommonlyUse.back'|translate}}
+
<<{{'CommonlyUse.back'|translate}}
{{'ExchangeDetail.detail'|translate}}
@@ -28,6 +28,9 @@
{{'ExchangeDetail.detail'|translate}}
{{'CommonlyUse.delete'|translate}}
+
+ {{'CommonlyUse.upgrade'|translate}}
+
@@ -52,9 +55,13 @@
{{'ExchangeDetail.detail'|translate}}
{{'NewCluster.namespace'|translate}}:
{{exchangeDetail?.namespace}}
+
+ {{'CommonlyUse.version'|translate}}:
+ {{exchangeDetail.version}}
+
{{'CommonlyUse.status'|translate}}:
- {{constantGather('participantFATEstatus',
exchangeDetail?.status).name | translate}}
diff --git a/frontend/src/app/view/federation/exchange-detail/exchange-detail.component.ts b/frontend/src/app/view/federation/exchange-detail/exchange-detail.component.ts
index aab63197..44a3dfe7 100644
--- a/frontend/src/app/view/federation/exchange-detail/exchange-detail.component.ts
+++ b/frontend/src/app/view/federation/exchange-detail/exchange-detail.component.ts
@@ -164,4 +164,13 @@ export class ExchangeDetailComponent implements OnInit {
});
}
+
+ // Exchange and cluster jump to the upgrade page through toUpgrade
+ toUpgrade(item: {uuid: string, name: string, version: string}, type: string) {
+ this.router.navigate(['/federation', 'fate', this.uuid, 'detail', item.uuid, item.version, type+'-'+item.name, 'upgrade'])
+ }
+
+ back() {
+ this.router.navigate(['federation', 'fate', this.uuid])
+ }
}
diff --git a/frontend/src/app/view/federation/fed-detail-fate/fed-detail-fate.component.html b/frontend/src/app/view/federation/fed-detail-fate/fed-detail-fate.component.html
index d562c55b..ef8ccece 100644
--- a/frontend/src/app/view/federation/fed-detail-fate/fed-detail-fate.component.html
+++ b/frontend/src/app/view/federation/fed-detail-fate/fed-detail-fate.component.html
@@ -63,6 +63,7 @@
{{'EndpointMg.name'|translate}}
{{'InfraProvider.name'|translate}}
{{'NewCluster.namespace'| translate}}
+ {{'CommonlyUse.version'| translate}}
{{'CommonlyUse.status'|translate}}
{{'CommonlyUse.action'|translate}}
@@ -99,13 +100,19 @@
{{exchange.endpoint_name}}
{{exchange.infra_provider_name}}
{{exchange.namespace}}
+ {{exchange.version}}
{{constantGather('participantFATEstatus',
exchange.status).name | translate}}
- {{'CommonlyUse.delete'|translate}}
-
+
+ {{'CommonlyUse.delete'|translate}}
+
+ {{'CommonlyUse.Upgrade'|translate}}
+
+
@@ -129,6 +136,7 @@
{{'EndpointMg.name'|translate}}
{{'InfraProvider.name'|translate}}
{{'NewCluster.namespace'| translate}}
+ {{'CommonlyUse.version'| translate}}
{{'FederationDetail.partyId'|translate}}
{{'CommonlyUse.status'|translate}}
{{'CommonlyUse.action'|translate}}
@@ -136,8 +144,18 @@
0">
- {{cluster.name}}
+
+
+ {{cluster.name}}
+
+
+ {{cluster.name}}
+
+
+
+ {{'FederationDetail.versionWarn'| translate}}
+
+
{{cluster.description}}
{{cluster.created_at | dateFormat}}
@@ -168,13 +186,19 @@
{{cluster.endpoint_name}}
{{cluster.infra_provider_name}}
{{cluster.namespace}}
+ {{cluster.version}}
{{cluster.party_id}}
{{constantGather('participantFATEstatus',
cluster.status).name | translate}}
{{'CommonlyUse.delete'|translate}}
+ (click)="openDeleteConfrimModal('cluster',cluster.uuid)">{{'CommonlyUse.delete'|translate}}
+
+ {{'CommonlyUse.Upgrade'|translate}}
+
+
diff --git a/frontend/src/app/view/federation/fed-detail-fate/fed-detail-fate.component.ts b/frontend/src/app/view/federation/fed-detail-fate/fed-detail-fate.component.ts
index f16afa4e..baf83147 100644
--- a/frontend/src/app/view/federation/fed-detail-fate/fed-detail-fate.component.ts
+++ b/frontend/src/app/view/federation/fed-detail-fate/fed-detail-fate.component.ts
@@ -68,7 +68,10 @@ export class FedDetailFateComponent implements OnInit {
this.participantList = data.data;
this.clusterlist = this.participantList.clusters || [];
this.exchange = this.participantList.exchange;
+ this.exchangeInfoList = []
if (this.exchange) {
+ const exchangeVersion = this.exchange.version.split('-')[0] || ''
+
for (const key in this.exchange.access_info) {
const obj: any = {
name: key,
@@ -83,6 +86,12 @@ export class FedDetailFateComponent implements OnInit {
}
this.clusterlist.forEach(cluster => {
cluster.clusterList = []
+ const clusterVersion = cluster.version.split('-')[0]
+ if (clusterVersion !== exchangeVersion) {
+ cluster.flag = true
+ } else {
+ cluster.flag = false
+ }
for (const key in cluster.access_info) {
const obj: any = {
name: key,
@@ -182,9 +191,15 @@ export class FedDetailFateComponent implements OnInit {
//createClusterDisabled is to disabled the 'new cluster' button when there is no active exchange in the current federation
get createClusterDisabled() {
- if (this.exchange && this.exchange.status === 1) {
+ if (this.exchange && (this.exchange.status === 1 || this.exchange.status === 6)) {
return false
}
return true
}
+
+ // Exchange and cluster jump to the upgrade page through toUpgrade
+ toUpgrade(item: {uuid: string, name: string, version: string}, type: string) {
+ this.router.navigate(['/federation', 'fate', this.uuid, 'detail', item.uuid, item.version, type+'-'+item.name, 'upgrade'])
+ }
+
}
diff --git a/frontend/src/app/view/openfl/create-openfl-fed/create-openfl-fed.component.ts b/frontend/src/app/view/openfl/create-openfl-fed/create-openfl-fed.component.ts
index 77deafb4..abc05df5 100644
--- a/frontend/src/app/view/openfl/create-openfl-fed/create-openfl-fed.component.ts
+++ b/frontend/src/app/view/openfl/create-openfl-fed/create-openfl-fed.component.ts
@@ -50,7 +50,7 @@ export class CreateOpenflComponent implements OnInit {
{
name: 'sample',
value: '',
- type: ['number-list']
+ type: ['require']
},
{
name: 'envoyYaml',
diff --git a/frontend/src/app/view/openfl/director-new/director-new.component.html b/frontend/src/app/view/openfl/director-new/director-new.component.html
index e1645d91..90028732 100644
--- a/frontend/src/app/view/openfl/director-new/director-new.component.html
+++ b/frontend/src/app/view/openfl/director-new/director-new.component.html
@@ -45,6 +45,7 @@ {{'EndpointNew.currentSelection'|translate}}: &nbs
{{'CommonlyUse.name'| translate}}
{{'CommonlyUse.description'| translate}}
{{'CommonlyUse.type'| translate}}
+ {{'NewCluster.namespace'| translate}}
{{'CommonlyUse.creationTime'| translate}}
{{'EndpointMg.infraName'|translate}}
{{'EndpointMg.endpointURL'|translate}}
@@ -53,6 +54,7 @@ {{'EndpointNew.currentSelection'|translate}}: &nbs
{{endpoint?.name}}
{{endpoint?.description}}
{{endpoint?.type}}
+ {{endpoint?.namespace}}
{{endpoint?.created_at | date : "medium"}}
{{endpoint?.infra_provider_name}}
{{endpoint?.kubefate_host}}
@@ -91,6 +93,8 @@ {{'EndpointNew.currentSelection'|translate}}: &nbs
{{'NewCluster.namespace'|translate}}
{{'CommonlyUse.next'| translate}}
diff --git a/frontend/src/app/view/openfl/director-new/director-new.component.scss b/frontend/src/app/view/openfl/director-new/director-new.component.scss
index 63e40374..fd72ac36 100644
--- a/frontend/src/app/view/openfl/director-new/director-new.component.scss
+++ b/frontend/src/app/view/openfl/director-new/director-new.component.scss
@@ -56,4 +56,8 @@ form {
margin-right: 20px;
}
}
+}
+.disabled {
+ color: #666;
+ cursor: not-allowed;
}
\ No newline at end of file
diff --git a/frontend/src/app/view/openfl/director-new/director-new.component.ts b/frontend/src/app/view/openfl/director-new/director-new.component.ts
index dc3d3d1b..deed8b7a 100644
--- a/frontend/src/app/view/openfl/director-new/director-new.component.ts
+++ b/frontend/src/app/view/openfl/director-new/director-new.component.ts
@@ -317,6 +317,16 @@ export class DirectorNewComponent implements OnInit {
return this.codeMirror && !this.hasYAMLTextAreaDOM && this.form.controls['yaml'].get('yaml')?.value
}
+ // set Namespace Disabled
+ get setNamespaceDisabled() {
+ if (this.selectedEndpoint && this.selectedEndpoint.namespace) {
+ this.form.get('namespace')?.get('namespace')?.setValue(this.selectedEndpoint.namespace)
+ return true
+ } else {
+ return false
+ }
+ }
+
//reset form when selection change
onSelectEndpoint() {
if (this.selectedEndpoint) {
diff --git a/frontend/src/app/view/openfl/fed-detail-openfl/fed-detail-openfl.component.ts b/frontend/src/app/view/openfl/fed-detail-openfl/fed-detail-openfl.component.ts
index fb18940a..9d39c194 100644
--- a/frontend/src/app/view/openfl/fed-detail-openfl/fed-detail-openfl.component.ts
+++ b/frontend/src/app/view/openfl/fed-detail-openfl/fed-detail-openfl.component.ts
@@ -175,6 +175,7 @@ export class FedDetailOpneFLComponent implements OnInit {
this.director = data.data.director
this.envoylist = data.data.envoy
this.storageDataList = data.data.envoy
+ this.directorAccessInfoList = []
if (this.director) {
for (const key in this.director.access_info) {
const obj: any = {
diff --git a/frontend/src/assets/i18n/en.json b/frontend/src/assets/i18n/en.json
index 8caa7554..54a03e76 100644
--- a/frontend/src/assets/i18n/en.json
+++ b/frontend/src/assets/i18n/en.json
@@ -113,7 +113,9 @@
"expirationDate": "Expiration Date",
"filter": "Filter",
"requirements": "(Optional) requirements.txt",
- "editorModeHelperMessage":"Click text area to enter editor mode"
+ "editorModeHelperMessage":"Click text area to enter editor mode",
+ "upgrade": "upgrade",
+ "Upgrade": "Upgrade"
},
"EndpointMg": {
"name": "Endpoint",
@@ -212,7 +214,10 @@
"noexchange": "No exchange, please add one.",
"noactiveexchange": "You should have an active exchange to create cluster.",
"accessInfo": "Exposed Services",
- "forceRemove": "Force remove"
+ "forceRemove": "Force remove",
+ "currentVersion": "Current Version",
+ "upgradeVersion": "Updated Version",
+ "versionWarn": "The versions of Cluster and Exchange are inconsistent and may not be compatible."
},
"NewCluster": {
"name": "Create a New Cluster",
@@ -265,7 +270,10 @@
"pulsarMngPort": "Manager Port",
"pulsarPort": "Port",
"sslPort": "SSL Port",
- "coresPerNode": "Cores Per Node"
+ "coresPerNode": "Cores Per Node",
+ "deviceConfiguration": "Device Configuration",
+ "enableGPU": "Enabled GPU Support for FATE-Flow (FATE 1.11 and Later) ",
+ "gpuNumber": "Number of GPU"
},
"ExchangeNew": {
"name": "Create a New Exchange",
diff --git a/frontend/src/assets/i18n/zh_CN.json b/frontend/src/assets/i18n/zh_CN.json
index bfef0ccd..48d44b9a 100644
--- a/frontend/src/assets/i18n/zh_CN.json
+++ b/frontend/src/assets/i18n/zh_CN.json
@@ -113,7 +113,9 @@
"expirationDate": "到期时间",
"filter": "筛选",
"requirements": "(可选) requirements.txt 文件",
- "editorModeHelperMessage":"点击文本区域进入编辑器模式"
+ "editorModeHelperMessage":"点击文本区域进入编辑器模式",
+ "upgrade": "升级",
+ "Upgrade": "升级"
},
"EndpointMg": {
"name": "服务端点",
@@ -212,7 +214,10 @@
"noexchange": "没有Exchange, 请添加一个",
"noactiveexchange": "你应该有一个活动的Exchange来创建群集",
"accessInfo": "公开服务",
- "forceRemove": "强制移除"
+ "forceRemove": "强制移除",
+ "currentVersion": "当前版本",
+ "upgradeVersion": "升级版本",
+ "versionWarn": "Cluster和Exchange 版本不一致,可能不兼容。"
},
"NewCluster": {
"name": "创建Cluster",
@@ -265,7 +270,10 @@
"pulsarMngPort": "管理端口",
"pulsarPort": "端口",
"sslPort": "SSL 端口",
- "coresPerNode": "单个节点核心数"
+ "coresPerNode": "单个节点核心数",
+ "deviceConfiguration": "设备配置",
+ "enableGPU": "为FATE-Flow启用GPU支持(FATE版本需不低于1.11)",
+ "gpuNumber": "GPU数量"
},
"ExchangeNew": {
"name": "创建Exchange",
diff --git a/frontend/src/utils/constant.ts b/frontend/src/utils/constant.ts
index 1d4ef03f..71f505b5 100644
--- a/frontend/src/utils/constant.ts
+++ b/frontend/src/utils/constant.ts
@@ -32,7 +32,8 @@ export const ParticipantFATEStatus:ConstantModel = {
Installing: 2,
Removing: 3,
Reconfiguring: 4,
- Failed: 5
+ Failed: 5,
+ Upgrading: 6
}
export const ParticipantFATEType :ConstantModel = {
Unknown: 0,
diff --git a/helm-charts/charts/fate-exchange/Chart.yaml b/helm-charts/charts/fate-exchange/Chart.yaml
index 2425ade0..58f7df54 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.11.1 & 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.11.1-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 07a5ec34..e5acd3ec 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 30d0273d..862e998d 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.11.1-fedlcm-v0.3.0
+partyId: 1
registry: ""
+pullPolicy:
imagePullSecrets:
- name: myregistrykey
persistence: false
@@ -18,7 +19,7 @@ modules:
- postgres
- fmlManagerServer
-# rollsite:
+# rollsite:
# type: NodePort
# nodePort: 30001
# loadBalancerIP: 192.168.0.1
@@ -39,7 +40,7 @@ modules:
# type: NodePort
# nodePort: 30007
# loadBalancerIP: 192.168.0.1
-# route_table:
+# route_table:
# sni:
# - fqdn: 10000.fate.org
# tunnelRoute: 192.168.0.2:30109
@@ -55,22 +56,22 @@ modules:
# httpNodePort: 30003
# grpcNodePort: 30008
# loadBalancerIP: 192.168.0.1
-# route_table:
-# 9999:
-# proxy:
-# - host: 192.168.9.1
+# route_table:
+# 9999:
+# proxy:
+# - host: 192.168.9.1
# http_port: 30093
-# grpc_port: 30098
-# fateflow:
+# grpc_port: 30098
+# fateflow:
# - host: 192.168.9.1
# http_port: 30097
# grpc_port: 30092
-# 10000:
-# proxy:
-# - host: 192.168.10.1
+# 10000:
+# proxy:
+# - host: 192.168.10.1
# http_port: 30103
-# grpc_port: 30108
-# fateflow:
+# grpc_port: 30108
+# fateflow:
# - host: 192.168.10.1
# http_port: 30107
# grpc_port: 30102
@@ -93,7 +94,7 @@ modules:
# tolerations:
# affinity:
# type: NodePort
- # nodePort:
+ # nodePort:
# loadBalancerIP: 192.168.0.1
# postgresHost: postgres
# postgresPort: 5432
diff --git a/helm-charts/charts/fate-exchange/values.yaml b/helm-charts/charts/fate-exchange/values.yaml
index 2b05f1a5..57816135 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.11.1-release
pullPolicy: IfNotPresent
- imagePullSecrets:
-# - name:
-
-partyId: 9999
-partyName: fate-9999
+ imagePullSecrets:
+# - name:
podSecurityPolicy:
enabled: false
@@ -110,12 +107,12 @@ modules:
fmlManagerServer:
include: true
image: federatedai/fml-manager-server
- imageTag: v0.2.0
+ imageTag: v0.3.0
# nodeSelector:
# tolerations:
# affinity:
type: ClusterIP
- # nodePort:
+ # nodePort:
# loadBalancerIP: 192.168.0.1
postgresHost: postgres
postgresPort: 5432
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 00000000..5b678e89
--- /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 982c5fbd..bc2474e4 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.11.1+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.11.1-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/backends/eggroll/_helpers.tpl b/helm-charts/charts/fate/templates/backends/eggroll/_helpers.tpl
index c35a3cc4..8abd2d17 100644
--- a/helm-charts/charts/fate/templates/backends/eggroll/_helpers.tpl
+++ b/helm-charts/charts/fate/templates/backends/eggroll/_helpers.tpl
@@ -19,4 +19,7 @@
{{- if eq .Values.device "IPCL" -}}
-ipcl
{{- end -}}
+{{- if eq .Values.device "GPU" -}}
+-gpu
+{{- end -}}
{{- end -}}
diff --git a/helm-charts/charts/fate/templates/backends/spark/_helpers.tpl b/helm-charts/charts/fate/templates/backends/spark/_helpers.tpl
index 6df82b04..80228c56 100644
--- a/helm-charts/charts/fate/templates/backends/spark/_helpers.tpl
+++ b/helm-charts/charts/fate/templates/backends/spark/_helpers.tpl
@@ -19,4 +19,7 @@
{{- if eq .Values.device "IPCL" -}}
-ipcl
{{- end -}}
+{{- if eq .Values.device "GPU" -}}
+-gpu
+{{- end -}}
{{- end -}}
diff --git a/helm-charts/charts/fate/templates/core/_helpers.tpl b/helm-charts/charts/fate/templates/core/_helpers.tpl
index 306c582a..5fbd730a 100644
--- a/helm-charts/charts/fate/templates/core/_helpers.tpl
+++ b/helm-charts/charts/fate/templates/core/_helpers.tpl
@@ -21,4 +21,7 @@
{{- if eq .Values.device "IPCL" -}}
-ipcl
{{- end -}}
+{{- if eq .Values.device "GPU" -}}
+-gpu
+{{- end -}}
{{- end -}}
diff --git a/helm-charts/charts/fate/templates/core/client/statefulSet.yaml b/helm-charts/charts/fate/templates/core/client/statefulSet.yaml
index bb0ef7db..83f218db 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 00000000..7c16c9e6
--- /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 a7bd717f..51098d14 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 5059f5bc..0920ff8d 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 298218dc..aba22c4e 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 | default "3" }}
+ cluster: {{ .Values.modules.python.pulsar.cluster | default "standalone" }}
+ tenant: {{ .Values.modules.python.pulsar.tenant | default "fl-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,67 +202,18 @@ 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: |
- paths: # dir or path
- - "python/federatedml/transfer_variable/auth_conf"
- component_registry.json: |
- {
- "components": {
- },
- "providers": {
- },
- "default_settings": {
- "fate_flow":{
- "default_version_key": "FATEFlow"
- },
- "fate": {
- "default_version_key": "FATE"
- },
- "class_path": {
- "interface": "components.components.Components",
- "feature_instance": "feature.instance.Instance",
- "feature_vector": "feature.sparse_vector.SparseVector",
- "model": "protobuf.generated",
- "model_migrate": "protobuf.model_migrate.model_migrate",
- "homo_model_convert": "protobuf.homo_model_convert.homo_model_convert"
- }
- }
- }
- job_default_config.yaml: |
- # component provider, relative path to get_fate_python_directory
- default_component_provider_path: federatedml
-
- # resource
- total_cores_overweight_percent: 1 # 1 means no overweight
- total_memory_overweight_percent: 1 # 1 means no overweight
- task_parallelism: 1
- task_cores: 4
- task_memory: 0 # mb
- max_cores_percent_per_job: 1 # 1 means total
-
- # scheduling
- job_timeout: 259200 # s
- remote_request_timeout: 30000 # ms
- 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
- # 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
- detect_connect_long_retry_count: 2
-
- # upload
- upload_max_bytes: 104857600 # bytes
-
- #component output
- output_data_summary_count_limit: 100
+
---
kind: ConfigMap
apiVersion: v1
diff --git a/helm-charts/charts/fate/templates/core/fateflow/service.yaml b/helm-charts/charts/fate/templates/core/fateflow/service.yaml
index e2d7bce3..a94757b5 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 ca14a14b..766a37b6 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
@@ -118,12 +118,9 @@ spec:
- |
set -x
mkdir -p /data/projects/fate/conf/
- cp /data/projects/fate/conf-tmp/transfer_conf.yaml /data/projects/fate/conf/transfer_conf.yaml
cp /data/projects/fate/conf-tmp/service_conf.yaml /data/projects/fate/conf/service_conf.yaml
- 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 +175,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 +216,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: {}
@@ -290,6 +235,6 @@ spec:
storageClassName: {{ .Values.modules.python.storageClass }}
resources:
requests:
- storage: {{ .Values.modules.mysql.size }}
+ storage: {{ .Values.modules.python.size }}
{{- end }}
{{- end }}
diff --git a/helm-charts/charts/fate/templates/site-portal/_helpers.tpl b/helm-charts/charts/fate/templates/site-portal/_helpers.tpl
index 6b7b30a4..d03ccf43 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 d1d53d27..24f8dfe7 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.11.1-fedlcm-v0.3.0
partyId: 9999
registry: ""
-pullPolicy:
-imagePullSecrets:
+pullPolicy:
+imagePullSecrets:
- name: myregistrykey
persistence: false
istio:
@@ -34,7 +34,7 @@ federation: Pulsar
storage: HDFS
# Algorithm: [Basic, NN]
algorithm: Basic
-# Device: [IPCL, CPU]
+# Device: [IPCL, CPU, GPU]
device: CPU
skippedKeys:
@@ -46,30 +46,30 @@ 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:
+ # frontend:
# hosts:
# - name: party9999.frontend.example.com
-
-# rollsite:
+
+# 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.11.1-release"
# replicas: 1
# resources:
# requests:
@@ -274,7 +292,7 @@ skippedKeys:
# nodePort: 30977
# worker:
# Image: "federatedai/spark-worker"
- # ImageTag: "1.9.1-release"
+ # ImageTag: "1.11.1-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
@@ -413,20 +431,20 @@ skippedKeys:
# storageClass: ""
# accessMode: ReadWriteOnce
# size: 1Gi
-
+
# frontend:
# image: federatedai/site-portal-frontend
-# imageTag: v0.2.0
+# imageTag: v0.3.0
# nodeSelector:
# tolerations:
# affinity:
# type: ClusterIP
-# nodePort:
+# nodePort:
# loadBalancerIP:
# 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 f73ee164..60d02fe1 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 46131ba7..ac08fe72 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.11.1-release
pullPolicy: IfNotPresent
imagePullSecrets:
# - name:
@@ -18,7 +18,7 @@ federation: Eggroll
storage: Eggroll
# Algorithm: Basic, NN
algorithm: Basic
-# Device: CPU, IPCL
+# Device: CPU, IPCL, GPU
device: IPCL
istio:
@@ -64,12 +64,12 @@ ingress:
path: /
tls: []
frontend:
- # annotations:
+ # annotations:
hosts:
- name: frontend.example.com
path: /
tls: []
-
+
exchange:
partyIp: 192.168.1.1
partyPort: 30001
@@ -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,20 +221,8 @@ modules:
cpu: "2"
memory: "4Gi"
- client:
- include: true
- ip: client
- type: ClusterIP
- nodeSelector:
- tolerations:
- affinity:
- subPath: "client"
- existingClaim:
- storageClass:
- accessMode: ReadWriteOnce
- size: 1Gi
- mysql:
+ mysql:
include: true
type: ClusterIP
nodeSelector:
@@ -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
@@ -373,7 +382,7 @@ modules:
# tolerations:
# affinity:
type: ClusterIP
- # nodePort:
+ # nodePort:
# loadBalancerIP:
user: site_portal
password: site_portal
@@ -383,28 +392,28 @@ modules:
storageClass: ""
accessMode: ReadWriteOnce
size: 1Gi
-
+
frontend:
include: false
image: federatedai/site-portal-frontend
- imageTag: v0.2.0
+ imageTag: v0.3.0
# nodeSelector:
# tolerations:
# affinity:
type: ClusterIP
-
- # nodePort:
+
+ # nodePort:
# loadBalancerIP:
-
+
sitePortalServer:
include: false
image: site-portal-server
- imageTag: v0.2.0
+ imageTag: v0.3.0
# nodeSelector:
# tolerations:
# affinity:
type: ClusterIP
- # nodePort:
+ # nodePort:
# loadBalancerIP:
existingClaim: ""
storageClass: "sitePortalServer"
diff --git a/helm-charts/charts/openfl-director/Chart.yaml b/helm-charts/charts/openfl-director/Chart.yaml
index a34a4915..f0beedaf 100644
--- a/helm-charts/charts/openfl-director/Chart.yaml
+++ b/helm-charts/charts/openfl-director/Chart.yaml
@@ -1,7 +1,7 @@
apiVersion: v1
-appVersion: "openfl-director-v0.1.0"
-description: A Helm chart for openfl director, based on official OpenFL container image
name: openfl-director
-version: v0.1.0
+version: v0.3.0
+description: A Helm chart for openfl director, based on official OpenFL container image
+appVersion: "openfl-director-v1.5"
sources:
- - https://github.com/FederatedAI/KubeFATE.git
\ No newline at end of file
+ - https://github.com/securefederatedai/openfl
\ No newline at end of file
diff --git a/helm-charts/charts/openfl-director/templates/director/deployment.yaml b/helm-charts/charts/openfl-director/templates/director/deployment.yaml
index d0738cc2..fc67bd36 100644
--- a/helm-charts/charts/openfl-director/templates/director/deployment.yaml
+++ b/helm-charts/charts/openfl-director/templates/director/deployment.yaml
@@ -1,4 +1,4 @@
-# Copyright 2022 VMware, Inc.
+# Copyright 2022-2023 VMware, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -41,6 +41,9 @@ spec:
mkdir -p workspace/logs
cd workspace
fx director start --director-config-path director_config.yaml --root-cert-path cert/root_ca.crt --private-key-path cert/priv.key --public-cert-path cert/director.crt
+ env:
+ - name: OVERRIDE_AGG_PORT
+ value: "50052"
image: {{ with .Values.image.registry }}{{ printf "%s/" . }}{{ end }}{{ .Values.modules.director.image | default "fedlcm-openfl" }}:{{ .Values.modules.director.imageTag | default "latest"}}
imagePullPolicy: {{ .Values.image.pullPolicy }}
name: director
diff --git a/helm-charts/charts/openfl-director/templates/notebook/deployment.yaml b/helm-charts/charts/openfl-director/templates/notebook/deployment.yaml
index ff13859f..3ad3f365 100644
--- a/helm-charts/charts/openfl-director/templates/notebook/deployment.yaml
+++ b/helm-charts/charts/openfl-director/templates/notebook/deployment.yaml
@@ -38,7 +38,7 @@ spec:
- /bin/bash
- -c
- |
- jupyter lab --notebook-dir /usr/local/lib/python3.8/dist-packages/openfl-tutorials --allow-root --ip=0.0.0.0 --NotebookApp.token='' --NotebookApp.password={{ .Values.modules.notebook.password }}
+ jupyter lab --notebook-dir /openfl/openfl-tutorials --allow-root --ip=0.0.0.0 --NotebookApp.token='' --NotebookApp.password={{ .Values.modules.notebook.password }}
image: {{ with .Values.image.registry }}{{ printf "%s/" . }}{{ end }}{{ .Values.modules.notebook.image | default "fedlcm-openfl" }}:{{ .Values.modules.notebook.imageTag | default "latest"}}
imagePullPolicy: {{ .Values.image.pullPolicy }}
name: notebook
diff --git a/helm-charts/charts/openfl-director/values-template-example.yaml b/helm-charts/charts/openfl-director/values-template-example.yaml
index 9cd71af0..64c38815 100644
--- a/helm-charts/charts/openfl-director/values-template-example.yaml
+++ b/helm-charts/charts/openfl-director/values-template-example.yaml
@@ -1,9 +1,9 @@
name: openfl-director
namespace: openfl-director
chartName: openfl-director
-chartVersion: v0.1.0
+chartVersion: v0.3.0
registry: "federatedai"
-imageTag: "v0.1.0"
+imageTag: "v0.3.0"
pullPolicy: IfNotPresent
# imagePullSecrets:
# - name: myregistrykey
@@ -26,7 +26,7 @@ modules:
#director:
# image: fedlcm-openfl
-# imageTag: v0.1.0
+# imageTag: v0.3.0
# sampleShape: "['784']"
# targetShape: "['1']"
# envoyHealthCheckPeriod: 60
@@ -39,7 +39,7 @@ modules:
#notebook:
# image: fedlcm-openfl
-# imageTag: v0.1.0
+# imageTag: v0.3.0
# password: argon2:$argon2id$v=19$m=10240,t=10,p=8$TmW50aM7Fey2lNrU7kpOhQ$s4SY7l8QItxgR9iwVA+DTc2uwGnawh1p1dB42bbLH48
# nodeSelector:
# tolerations:
diff --git a/helm-charts/charts/openfl-director/values-template.yaml b/helm-charts/charts/openfl-director/values-template.yaml
index f7e6a50c..81cb126c 100644
--- a/helm-charts/charts/openfl-director/values-template.yaml
+++ b/helm-charts/charts/openfl-director/values-template.yaml
@@ -36,6 +36,7 @@ modules:
director:
{{- with .director }}
image: {{ .image }}
+ imageTag: {{ .imageTag }}
sampleShape: "{{ .sampleShape }}"
targetShape: "{{ .targetShape }}"
envoyHealthCheckPeriod: {{ .envoyHealthCheckPeriod | default "60"}}
@@ -59,6 +60,7 @@ modules:
notebook:
{{- with .notebook}}
image: {{ .image }}
+ imageTag: {{ .imageTag }}
password: {{ .password }}
{{- with .nodeSelector }}
nodeSelector:
diff --git a/helm-charts/charts/openfl-director/values.yaml b/helm-charts/charts/openfl-director/values.yaml
index 5da4dda9..34cd6b30 100644
--- a/helm-charts/charts/openfl-director/values.yaml
+++ b/helm-charts/charts/openfl-director/values.yaml
@@ -18,7 +18,7 @@ ingress:
modules:
director:
image: fedlcm-openfl
- imageTag: v0.1.0
+ imageTag: v0.3.0
sampleShape: "['1']"
targetShape: "['1']"
envoyHealthCheckPeriod: 60
@@ -31,7 +31,7 @@ modules:
notebook:
image: fedlcm-openfl
- imageTag: v0.1.0
+ imageTag: v0.3.0
# password:
# nodeSelector:
# tolerations:
diff --git a/helm-charts/charts/openfl-envoy/Chart.yaml b/helm-charts/charts/openfl-envoy/Chart.yaml
index 22698160..2605d109 100644
--- a/helm-charts/charts/openfl-envoy/Chart.yaml
+++ b/helm-charts/charts/openfl-envoy/Chart.yaml
@@ -1,7 +1,7 @@
apiVersion: v1
-appVersion: "openfl-envoy-v0.1.0"
-description: A Helm chart for openfl envoy
name: openfl-envoy
-version: v0.1.0
+version: v0.3.0
+description: A Helm chart for openfl envoy
+appVersion: "openfl-envoy-v1.5"
sources:
- - https://github.com/FederatedAI/KubeFATE.git
\ No newline at end of file
+ - https://github.com/securefederatedai/openfl
\ No newline at end of file
diff --git a/helm-charts/charts/openfl-envoy/templates/envoy/deployment.yaml b/helm-charts/charts/openfl-envoy/templates/envoy/deployment.yaml
index 27a8e1be..cb53837c 100644
--- a/helm-charts/charts/openfl-envoy/templates/envoy/deployment.yaml
+++ b/helm-charts/charts/openfl-envoy/templates/envoy/deployment.yaml
@@ -1,4 +1,4 @@
-# Copyright 2022 VMware, Inc.
+# Copyright 2022-2023 VMware, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -41,7 +41,9 @@ spec:
value: {{ .Values.modules.envoy.directorFqdn }}
- name: DIRECTOR_PORT
value: "{{ .Values.modules.envoy.directorPort }}"
- - name: AGG_PORT
+ - name: OVERRIDE_AGG_ADDR
+ value: {{ .Values.modules.envoy.directorFqdn }}
+ - name: OVERRIDE_AGG_PORT
value: "{{ .Values.modules.envoy.aggPort }}"
image: {{ with .Values.image.registry }}{{ printf "%s/" . }}{{ end }}{{ .Values.modules.envoy.image | default "fedlcm-openfl" }}:{{ .Values.modules.envoy.imageTag | default "latest"}}
imagePullPolicy: {{ .Values.image.pullPolicy }}
diff --git a/helm-charts/charts/openfl-envoy/values-template-example.yaml b/helm-charts/charts/openfl-envoy/values-template-example.yaml
index 1eba47d9..9a105af6 100644
--- a/helm-charts/charts/openfl-envoy/values-template-example.yaml
+++ b/helm-charts/charts/openfl-envoy/values-template-example.yaml
@@ -1,7 +1,7 @@
name: envoy-1
namespace: openfl-envoy-1
chartName: openfl-envoy
-chartVersion: v0.1.0
+chartVersion: v0.3.0
registry: "federatedai"
pullPolicy: IfNotPresent
imagePullSecrets:
@@ -13,7 +13,7 @@ modules:
#envoy:
# image: fedlcm-openfl
-# imageTag: v0.1.0
+# imageTag: v0.3.0
# directorFqdn: director.openfl.example.com
# directorIp: 192.168.1.1.
# directorPort: 50051
diff --git a/helm-charts/charts/openfl-envoy/values.yaml b/helm-charts/charts/openfl-envoy/values.yaml
index 84627c93..8c479aa7 100644
--- a/helm-charts/charts/openfl-envoy/values.yaml
+++ b/helm-charts/charts/openfl-envoy/values.yaml
@@ -12,7 +12,7 @@ podSecurityPolicy:
modules:
envoy:
image: fedlcm-openfl
- imageTag: v0.1.0
+ imageTag: v0.3.0
directorFqdn: director
directorIp: 192.168.1.1
directorPort: 50051
diff --git a/helm-charts/fml-manager.yaml b/helm-charts/fml-manager.yaml
index 7b53058d..085d4268 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.11.1-fedlcm-v0.3.0
partyId: 0
registry: ""
pullPolicy:
diff --git a/helm-charts/site-portal.yaml b/helm-charts/site-portal.yaml
index d1d53d27..4aec10e5 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.11.1-fedlcm-v0.3.0
partyId: 9999
registry: ""
pullPolicy:
@@ -258,7 +258,7 @@ skippedKeys:
# spark:
# master:
# Image: "federatedai/spark-master"
- # ImageTag: "1.9.1-release"
+ # ImageTag: "1.11.1-release"
# replicas: 1
# resources:
# requests:
@@ -274,7 +274,7 @@ skippedKeys:
# nodePort: 30977
# worker:
# Image: "federatedai/spark-worker"
- # ImageTag: "1.9.1-release"
+ # ImageTag: "1.11.1-release"
# replicas: 2
# resources:
# requests:
@@ -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 f932db0a..f4890d42 100644
--- a/k8s_deploy.yaml
+++ b/k8s_deploy.yaml
@@ -118,6 +118,7 @@ spec:
volumeMounts:
- name: postgres-volume
mountPath: /var/lib/postgresql/data
+ subPath: data
env:
- name: POSTGRES_USER
value: "lifecycle_manager"
@@ -153,7 +154,7 @@ spec:
# - ReadWriteOnce
# resources:
# requests:
-# storage: 5Gi
+# storage: 5Gi
---
apiVersion: apps/v1
kind: Deployment
@@ -201,10 +202,10 @@ spec:
- mountPath: /home/step
name: stepca-volume
- mountPath: /entrypoint.sh
- subPath: entrypoint.sh
name: stepca-entrypoint
+ subPath: entrypoint.sh
- name: server
- image: federatedai/fedlcm-server:v0.2.0
+ image: federatedai/fedlcm-server:v0.3.0
imagePullPolicy: IfNotPresent
securityContext:
runAsUser: 1000
@@ -270,7 +271,7 @@ spec:
# - ReadWriteOnce
# resources:
# requests:
-# storage: 5Gi
+# storage: 5Gi
---
apiVersion: v1
kind: Service
@@ -312,7 +313,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/pkg/kubefate/constants.go b/pkg/kubefate/constants.go
index 97c08c1d..f8c53835 100644
--- a/pkg/kubefate/constants.go
+++ b/pkg/kubefate/constants.go
@@ -96,6 +96,8 @@ rules:
resources:
- deployments
- statefulsets
+ - deployments/status
+ - statefulsets/status
verbs:
- get
- list
@@ -117,25 +119,14 @@ rules:
- apiGroups:
- networking.istio.io
resources:
- - gateway
- - virtualservice
+ - gateways
+ - virtualservices
verbs:
- get
- create
- delete
- update
- patch
-- apiGroups:
- - policy
- resources:
- - podsecuritypolicies
- verbs:
- - get
- - use
- - create
- - delete
- - update
- - patch
- apiGroups:
- rbac.authorization.k8s.io
resources:
@@ -147,6 +138,17 @@ rules:
- delete
- update
- patch
+- apiGroups:
+ - batch
+ resources:
+ - jobs
+ verbs:
+ - get
+ - list
+ - create
+ - delete
+ - update
+ - patch
{{- else }}
---
apiVersion: rbac.authorization.k8s.io/v1
diff --git a/pkg/kubefate/kubefate.go b/pkg/kubefate/kubefate.go
index 6c398ed6..f17cd10f 100644
--- a/pkg/kubefate/kubefate.go
+++ b/pkg/kubefate/kubefate.go
@@ -587,7 +587,7 @@ func (c *client) parseResponse(response *http.Response) ([]byte, error) {
log.Error().Err(err).Msg("read response body error")
return body, err
}
- log.Info().Str("body", string(body)).Msg("response body")
+ log.Debug().Str("body", string(body)).Msg("response body")
if response.StatusCode != http.StatusOK {
log.Error().Msgf("request error: %s", response.Status)
m := make(map[string]string)
diff --git a/server/api/certificate-authority.go b/server/api/certificate-authority.go
index ed0b6c63..5f604dcb 100644
--- a/server/api/certificate-authority.go
+++ b/server/api/certificate-authority.go
@@ -50,13 +50,14 @@ func (controller *CertificateAuthorityController) Route(r *gin.RouterGroup) {
}
// get returns the certificate authority info
-// @Summary Return certificate authority info
-// @Tags CertificateAuthority
-// @Produce json
-// @Success 200 {object} GeneralResponse{data=service.CertificateAuthorityDetail} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /certificate-authority [get]
+//
+// @Summary Return certificate authority info
+// @Tags CertificateAuthority
+// @Produce json
+// @Success 200 {object} GeneralResponse{data=service.CertificateAuthorityDetail} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /certificate-authority [get]
func (controller *CertificateAuthorityController) get(c *gin.Context) {
if caInfo, err := controller.certificateAuthorityApp.Get(); err != nil {
resp := &GeneralResponse{
@@ -76,14 +77,15 @@ func (controller *CertificateAuthorityController) get(c *gin.Context) {
}
// create a new certificate authority
-// @Summary Create a new certificate authority
-// @Tags CertificateAuthority
-// @Produce json
-// @Param certificateAuthority body service.CertificateAuthorityEditableItem true "The CA information, currently for the type field only '1(StepCA)' is supported"
-// @Success 200 {object} GeneralResponse "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /certificate-authority [post]
+//
+// @Summary Create a new certificate authority
+// @Tags CertificateAuthority
+// @Produce json
+// @Param certificateAuthority body service.CertificateAuthorityEditableItem true "The CA information, currently for the type field only '1(StepCA)' is supported"
+// @Success 200 {object} GeneralResponse "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /certificate-authority [post]
func (controller *CertificateAuthorityController) create(c *gin.Context) {
if err := func() error {
caInfo := &service.CertificateAuthorityEditableItem{}
@@ -106,15 +108,16 @@ func (controller *CertificateAuthorityController) create(c *gin.Context) {
}
// update the CA configuration
-// @Summary Updates the certificate authority
-// @Tags CertificateAuthority
-// @Produce json
-// @Param uuid path string true "certificate authority UUID"
-// @Param certificateAuthority body service.CertificateAuthorityEditableItem true "The updated CA information"
-// @Success 200 {object} GeneralResponse "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /certificate-authority/{uuid} [put]
+//
+// @Summary Updates the certificate authority
+// @Tags CertificateAuthority
+// @Produce json
+// @Param uuid path string true "certificate authority UUID"
+// @Param certificateAuthority body service.CertificateAuthorityEditableItem true "The updated CA information"
+// @Success 200 {object} GeneralResponse "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /certificate-authority/{uuid} [put]
func (controller *CertificateAuthorityController) update(c *gin.Context) {
if err := func() error {
uuid := c.Param("uuid")
@@ -138,13 +141,14 @@ func (controller *CertificateAuthorityController) update(c *gin.Context) {
}
// getBuiltInCAConfig returns the built-in certificate authority config
-// @Summary Return the built-in certificate authority config
-// @Tags CertificateAuthority
-// @Produce json
-// @Success 200 {object} GeneralResponse{data=entity.CertificateAuthorityConfigurationStepCA} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /certificate-authority/built-in-ca [get]
+//
+// @Summary Return the built-in certificate authority config
+// @Tags CertificateAuthority
+// @Produce json
+// @Success 200 {object} GeneralResponse{data=entity.CertificateAuthorityConfigurationStepCA} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /certificate-authority/built-in-ca [get]
func (controller *CertificateAuthorityController) getBuiltInCAConfig(c *gin.Context) {
caConfig, err := controller.certificateAuthorityApp.GetBuiltInCAConfig()
if err != nil {
diff --git a/server/api/certificate.go b/server/api/certificate.go
index 0f7dcaa7..9124dc3a 100644
--- a/server/api/certificate.go
+++ b/server/api/certificate.go
@@ -61,13 +61,14 @@ func (controller *CertificateController) Route(r *gin.RouterGroup) {
}
// list returns the certificate list
-// @Summary Return issued certificate list
-// @Tags Certificate
-// @Produce json
-// @Success 200 {object} GeneralResponse{data=[]service.CertificateListItem} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /certificate [get]
+//
+// @Summary Return issued certificate list
+// @Tags Certificate
+// @Produce json
+// @Success 200 {object} GeneralResponse{data=[]service.CertificateListItem} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /certificate [get]
func (controller *CertificateController) list(c *gin.Context) {
if certList, err := controller.certificateApp.List(); err != nil {
resp := &GeneralResponse{
@@ -86,14 +87,15 @@ func (controller *CertificateController) list(c *gin.Context) {
}
// delete removes the certificate which has no participant bindings
-// @Summary Delete the certificate which has no participant bindings
-// @Tags Certificate
-// @Produce json
-// @Param uuid path string true "Certificate UUID"
-// @Success 200 {object} GeneralResponse "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /certificate/{uuid} [delete]
+//
+// @Summary Delete the certificate which has no participant bindings
+// @Tags Certificate
+// @Produce json
+// @Param uuid path string true "Certificate UUID"
+// @Success 200 {object} GeneralResponse "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /certificate/{uuid} [delete]
func (controller *CertificateController) delete(c *gin.Context) {
uuid := c.Param("uuid")
if err := controller.certificateApp.DeleteCertificate(uuid); err != nil {
diff --git a/server/api/chart.go b/server/api/chart.go
index 4835e559..4eb42b1e 100644
--- a/server/api/chart.go
+++ b/server/api/chart.go
@@ -51,14 +51,15 @@ func (controller *ChartController) Route(r *gin.RouterGroup) {
}
// list returns the chart list
-// @Summary Return chart list, optionally with the specified type
-// @Tags Chart
-// @Produce json
-// @Param type query uint8 false "if set, it should be the chart type"
-// @Success 200 {object} GeneralResponse{data=[]service.ChartListItem} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /chart [get]
+//
+// @Summary Return chart list, optionally with the specified type
+// @Tags Chart
+// @Produce json
+// @Param type query uint8 false "if set, it should be the chart type"
+// @Success 200 {object} GeneralResponse{data=[]service.ChartListItem} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /chart [get]
func (controller *ChartController) list(c *gin.Context) {
if charList, err := func() ([]service.ChartListItem, error) {
t64, err := strconv.ParseUint(c.DefaultQuery("type", "0"), 10, 8)
@@ -84,14 +85,15 @@ func (controller *ChartController) list(c *gin.Context) {
}
// get returns detailed information of a chart
-// @Summary Get chart's detailed info
-// @Tags Chart
-// @Produce json
-// @Param uuid path string true "Chart UUID"
-// @Success 200 {object} GeneralResponse{data=service.ChartDetail} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /chart/{uuid} [get]
+//
+// @Summary Get chart's detailed info
+// @Tags Chart
+// @Produce json
+// @Param uuid path string true "Chart UUID"
+// @Success 200 {object} GeneralResponse{data=service.ChartDetail} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /chart/{uuid} [get]
func (controller *ChartController) get(c *gin.Context) {
uuid := c.Param("uuid")
if chartDetail, err := controller.chartApp.GetDetail(uuid); err != nil {
diff --git a/server/api/endpoint.go b/server/api/endpoint.go
index 1690105c..85d72632 100644
--- a/server/api/endpoint.go
+++ b/server/api/endpoint.go
@@ -67,13 +67,14 @@ func (controller *EndpointController) Route(r *gin.RouterGroup) {
}
// list returns the endpoints list
-// @Summary Return endpoints list data
-// @Tags Endpoint
-// @Produce json
-// @Success 200 {object} GeneralResponse{data=[]service.EndpointListItem} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /endpoint [get]
+//
+// @Summary Return endpoints list data
+// @Tags Endpoint
+// @Produce json
+// @Success 200 {object} GeneralResponse{data=[]service.EndpointListItem} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /endpoint [get]
func (controller *EndpointController) list(c *gin.Context) {
endpointList, err := controller.endpointAppService.GetEndpointList()
if err != nil {
@@ -94,14 +95,15 @@ func (controller *EndpointController) list(c *gin.Context) {
}
// get returns detailed information of an endpoint
-// @Summary Get endpoint's detailed info
-// @Tags Endpoint
-// @Produce json
-// @Param uuid path string true "Endpoint UUID"
-// @Success 200 {object} GeneralResponse{data=service.EndpointDetail} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /endpoint/{uuid} [get]
+//
+// @Summary Get endpoint's detailed info
+// @Tags Endpoint
+// @Produce json
+// @Param uuid path string true "Endpoint UUID"
+// @Success 200 {object} GeneralResponse{data=service.EndpointDetail} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /endpoint/{uuid} [get]
func (controller *EndpointController) get(c *gin.Context) {
uuid := c.Param("uuid")
if endpointDetail, err := controller.endpointAppService.GetEndpointDetail(uuid); err != nil {
@@ -120,15 +122,16 @@ func (controller *EndpointController) get(c *gin.Context) {
}
// delete removes the endpoint
-// @Summary Delete the endpoint
-// @Tags Endpoint
-// @Produce json
-// @Param uuid path string true "Endpoint UUID"
-// @Param uninstall query bool false "if set to true, the endpoint installation will be removed too"
-// @Success 200 {object} GeneralResponse "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /endpoint/{uuid} [delete]
+//
+// @Summary Delete the endpoint
+// @Tags Endpoint
+// @Produce json
+// @Param uuid path string true "Endpoint UUID"
+// @Param uninstall query bool false "if set to true, the endpoint installation will be removed too"
+// @Success 200 {object} GeneralResponse "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /endpoint/{uuid} [delete]
func (controller *EndpointController) delete(c *gin.Context) {
uuid := c.Param("uuid")
uninstall, err := strconv.ParseBool(c.DefaultQuery("uninstall", "false"))
@@ -150,14 +153,15 @@ func (controller *EndpointController) delete(c *gin.Context) {
}
// scan finds the endpoint installation status of an infra provider
-// @Summary Scan the endpoints in an infra provider
-// @Tags Endpoint
-// @Produce json
-// @Param provider body service.EndpointScanRequest true "Provider UUID and endpoint type"
-// @Success 200 {object} GeneralResponse{data=[]service.EndpointListItem} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /endpoint/scan [post]
+//
+// @Summary Scan the endpoints in an infra provider
+// @Tags Endpoint
+// @Produce json
+// @Param provider body service.EndpointScanRequest true "Provider UUID and endpoint type"
+// @Success 200 {object} GeneralResponse{data=[]service.EndpointListItem} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /endpoint/scan [post]
func (controller *EndpointController) scan(c *gin.Context) {
if endpointDetail, err := func() ([]service.EndpointScanItem, error) {
request := &service.EndpointScanRequest{}
@@ -181,14 +185,15 @@ func (controller *EndpointController) scan(c *gin.Context) {
}
// checkKubeFATE test connection to a KubeFATE endpoint
-// @Summary Test connection to KubeFATE endpoint
-// @Tags Endpoint
-// @Produce json
-// @Param uuid path string true "Endpoint UUID"
-// @Success 200 {object} GeneralResponse "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /endpoint/{uuid}/kubefate/check [post]
+//
+// @Summary Test connection to KubeFATE endpoint
+// @Tags Endpoint
+// @Produce json
+// @Param uuid path string true "Endpoint UUID"
+// @Success 200 {object} GeneralResponse "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /endpoint/{uuid}/kubefate/check [post]
func (controller *EndpointController) checkKubeFATE(c *gin.Context) {
uuid := c.Param("uuid")
if err := controller.endpointAppService.CheckKubeFATEConnection(uuid); err != nil {
@@ -206,22 +211,23 @@ func (controller *EndpointController) checkKubeFATE(c *gin.Context) {
}
// getKubeFATEDeploymentYAML returns the yaml content for deploying KubeFATE
-// @Summary Get KubeFATE installation YAML content
-// @Tags Endpoint
-// @Produce json
-// @Param service_username query string true "username of the created KubeFATE service"
-// @Param service_password query string true "password of the created KubeFATE service"
-// @Param hostname query string true "hostname domain name for the KubeFATE ingress object"
-// @Param use_registry query bool true "use_registry is to choose to use registry or not"
-// @Param registry query string true "registry is registry address"
-// @Param use_registry_secret query bool true "use_registry_secret is to choose to use registry secret or not"
-// @Param registry_server_url query string true "registry_server_url is registry's server url"
-// @Param registry_username query string true "registry_username is registry's username"
-// @Param registry_password query string true "registry_password is registry's password"
-// @Success 200 {object} GeneralResponse{data=string} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /endpoint/kubefate/yaml [get]
+//
+// @Summary Get KubeFATE installation YAML content
+// @Tags Endpoint
+// @Produce json
+// @Param service_username query string true "username of the created KubeFATE service"
+// @Param service_password query string true "password of the created KubeFATE service"
+// @Param hostname query string true "hostname domain name for the KubeFATE ingress object"
+// @Param use_registry query bool true "use_registry is to choose to use registry or not"
+// @Param registry query string true "registry is registry address"
+// @Param use_registry_secret query bool true "use_registry_secret is to choose to use registry secret or not"
+// @Param registry_server_url query string true "registry_server_url is registry's server url"
+// @Param registry_username query string true "registry_username is registry's username"
+// @Param registry_password query string true "registry_password is registry's password"
+// @Success 200 {object} GeneralResponse{data=string} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /endpoint/kubefate/yaml [get]
func (controller *EndpointController) getKubeFATEDeploymentYAML(c *gin.Context) {
if yaml, err := func() (string, error) {
namespace := c.DefaultQuery("namespace", "")
@@ -275,14 +281,15 @@ func (controller *EndpointController) getKubeFATEDeploymentYAML(c *gin.Context)
}
// create a new endpoint
-// @Summary Create a new endpoint by install a new one or add an existing one
-// @Tags Endpoint
-// @Produce json
-// @Param provider body service.EndpointCreationRequest true "The endpoint information, currently for the type field only 'KubeFATE' is supported"
-// @Success 200 {object} GeneralResponse "Success, the returned data contains the created endpoint"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /endpoint [post]
+//
+// @Summary Create a new endpoint by install a new one or add an existing one
+// @Tags Endpoint
+// @Produce json
+// @Param provider body service.EndpointCreationRequest true "The endpoint information, currently for the type field only 'KubeFATE' is supported"
+// @Success 200 {object} GeneralResponse "Success, the returned data contains the created endpoint"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /endpoint [post]
func (controller *EndpointController) create(c *gin.Context) {
if uuid, err := func() (string, error) {
req := &service.EndpointCreationRequest{}
diff --git a/server/api/event.go b/server/api/event.go
index c94abe19..ba3df2b8 100644
--- a/server/api/event.go
+++ b/server/api/event.go
@@ -47,13 +47,14 @@ func (controller *EventController) Route(r *gin.RouterGroup) {
}
// list returns the event list of related entity
-// @Summary Return event list of related entity
-// @Tags Event
-// @Produce json
-// @Success 200 {object} GeneralResponse{data=[]service.EventListItem} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /event/{entity_uuid} [get]
+//
+// @Summary Return event list of related entity
+// @Tags Event
+// @Produce json
+// @Success 200 {object} GeneralResponse{data=[]service.EventListItem} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /event/{entity_uuid} [get]
func (controller *EventController) get(c *gin.Context) {
entity_uuid := c.Param("entity_uuid")
eventList, err := controller.EventApp.GetEventList(entity_uuid)
diff --git a/server/api/federation.go b/server/api/federation.go
index 54479594..2317b29b 100644
--- a/server/api/federation.go
+++ b/server/api/federation.go
@@ -26,6 +26,7 @@ import (
"github.com/FederatedAI/FedLCM/server/domain/valueobject"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
+ "github.com/rs/zerolog/log"
)
// FederationController handles federation related APIs
@@ -106,6 +107,13 @@ func (controller *FederationController) Route(r *gin.RouterGroup) {
fate.GET("/:uuid/exchange/:exchangeUUID", controller.getFATEExchange)
fate.GET("/:uuid/cluster/:clusterUUID", controller.getFATECluster)
+
+ fate.GET("/:uuid/exchange/:exchangeUUID/upgrade", controller.getFATEExchangeUpgrade)
+ fate.GET("/:uuid/cluster/:clusterUUID/upgrade", controller.getFATEClusterUpgrade)
+
+ fate.POST("/:uuid/exchange/:exchangeUUID/upgrade", controller.upgradeFATEExchange)
+ fate.POST("/:uuid/cluster/:clusterUUID/upgrade", controller.upgradeFATECluster)
+
}
openfl := federation.Group("openfl")
@@ -133,13 +141,14 @@ func (controller *FederationController) Route(r *gin.RouterGroup) {
}
// list returns the federation list
-// @Summary Return federation list,
-// @Tags Federation
-// @Produce json
-// @Success 200 {object} GeneralResponse{data=[]service.FederationListItem} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /federation [get]
+//
+// @Summary Return federation list,
+// @Tags Federation
+// @Produce json
+// @Success 200 {object} GeneralResponse{data=[]service.FederationListItem} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /federation [get]
func (controller *FederationController) list(c *gin.Context) {
if federationList, err := func() ([]service.FederationListItem, error) {
return controller.federationApp.List()
@@ -161,14 +170,15 @@ func (controller *FederationController) list(c *gin.Context) {
}
// createFATE creates a new FATE federation
-// @Summary Create a new FATE federation
-// @Tags Federation
-// @Produce json
-// @Param federation body service.FederationFATECreationRequest true "The federation info"
-// @Success 200 {object} GeneralResponse "Success, the data field is the created federation's uuid"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /federation/fate [post]
+//
+// @Summary Create a new FATE federation
+// @Tags Federation
+// @Produce json
+// @Param federation body service.FederationFATECreationRequest true "The federation info"
+// @Success 200 {object} GeneralResponse "Success, the data field is the created federation's uuid"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /federation/fate [post]
func (controller *FederationController) createFATE(c *gin.Context) {
if uuid, err := func() (string, error) {
creationInfo := &service.FederationFATECreationRequest{}
@@ -192,14 +202,15 @@ func (controller *FederationController) createFATE(c *gin.Context) {
}
// getFATE returns detailed information of a FATE federation
-// @Summary Get specific info of a FATE federation
-// @Tags Federation
-// @Produce json
-// @Param uuid path string true "federation UUID"
-// @Success 200 {object} GeneralResponse{data=service.FederationFATEDetail} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /federation/fate/{uuid} [get]
+//
+// @Summary Get specific info of a FATE federation
+// @Tags Federation
+// @Produce json
+// @Param uuid path string true "federation UUID"
+// @Success 200 {object} GeneralResponse{data=service.FederationFATEDetail} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /federation/fate/{uuid} [get]
func (controller *FederationController) getFATE(c *gin.Context) {
uuid := c.Param("uuid")
if info, err := controller.federationApp.GetFATEFederation(uuid); err != nil {
@@ -218,14 +229,15 @@ func (controller *FederationController) getFATE(c *gin.Context) {
}
// deleteFATE deletes the specified federation
-// @Summary Delete a FATE federation
-// @Tags Federation
-// @Produce json
-// @Param uuid path string true "federation UUID"
-// @Success 200 {object} GeneralResponse "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /federation/fate/{uuid} [delete]
+//
+// @Summary Delete a FATE federation
+// @Tags Federation
+// @Produce json
+// @Param uuid path string true "federation UUID"
+// @Success 200 {object} GeneralResponse "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /federation/fate/{uuid} [delete]
func (controller *FederationController) deleteFATE(c *gin.Context) {
uuid := c.Param("uuid")
if err := controller.federationApp.DeleteFATEFederation(uuid); err != nil {
@@ -243,21 +255,22 @@ func (controller *FederationController) deleteFATE(c *gin.Context) {
}
// getFATEExchangeDeploymentYAML returns deployment yaml content for deploying FATE exchange
-// @Summary Get FATE exchange deployment yaml
-// @Tags Federation
-// @Produce json
-// @Param chart_uuid query string true "the chart uuid"
-// @Param name query string true "name of the deployment"
-// @Param namespace query string true "namespace of the deployment"
-// @Param service_type query int true "type of the service to be exposed 1: LoadBalancer 2: NodePort"
-// @Param registry query string true "FATE registry config saved in the infra provider"
-// @Param use_registry query bool true "choose if use the customized registry config"
-// @Param use_registry_secret query bool true "choose if use the customized registry secret"
-// @Param enable_psp query bool true "choose if enable the podSecurityPolicy"
-// @Success 200 {object} GeneralResponse{data=string} "Success, the data field is the yaml content"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /federation/fate/exchange/yaml [get]
+//
+// @Summary Get FATE exchange deployment yaml
+// @Tags Federation
+// @Produce json
+// @Param chart_uuid query string true "the chart uuid"
+// @Param name query string true "name of the deployment"
+// @Param namespace query string true "namespace of the deployment"
+// @Param service_type query int true "type of the service to be exposed 1: LoadBalancer 2: NodePort"
+// @Param registry query string true "FATE registry config saved in the infra provider"
+// @Param use_registry query bool true "choose if use the customized registry config"
+// @Param use_registry_secret query bool true "choose if use the customized registry secret"
+// @Param enable_psp query bool true "choose if enable the podSecurityPolicy"
+// @Success 200 {object} GeneralResponse{data=string} "Success, the data field is the yaml content"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /federation/fate/exchange/yaml [get]
func (controller *FederationController) getFATEExchangeDeploymentYAML(c *gin.Context) {
if yaml, err := func() (string, error) {
chartUUID := c.DefaultQuery("chart_uuid", "")
@@ -314,15 +327,16 @@ func (controller *FederationController) getFATEExchangeDeploymentYAML(c *gin.Con
}
// createFATEExchange creates a new FATE exchange
-// @Summary Create a new FATE exchange
-// @Tags Federation
-// @Produce json
-// @Param uuid path string true "federation UUID"
-// @Param creationRequest body service.ParticipantFATEExchangeCreationRequest true "The creation requests"
-// @Success 200 {object} GeneralResponse "Success, the data field is the created exchange's uuid"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /federation/fate/:uuid/exchange [post]
+//
+// @Summary Create a new FATE exchange
+// @Tags Federation
+// @Produce json
+// @Param uuid path string true "federation UUID"
+// @Param creationRequest body service.ParticipantFATEExchangeCreationRequest true "The creation requests"
+// @Success 200 {object} GeneralResponse "Success, the data field is the created exchange's uuid"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /federation/fate/:uuid/exchange [post]
func (controller *FederationController) createFATEExchange(c *gin.Context) {
if exchangeUUID, err := func() (string, error) {
federationUUID := c.Param("uuid")
@@ -348,15 +362,16 @@ func (controller *FederationController) createFATEExchange(c *gin.Context) {
}
// createExternalFATEExchange creates an external FATE exchange
-// @Summary Create an external FATE exchange
-// @Tags Federation
-// @Produce json
-// @Param uuid path string true "federation UUID"
-// @Param creationRequest body domainService.ParticipantFATEExternalExchangeCreationRequest true "The creation requests"
-// @Success 200 {object} GeneralResponse "Success, the data field is the created exchange's uuid"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /federation/fate/:uuid/exchange/external [post]
+//
+// @Summary Create an external FATE exchange
+// @Tags Federation
+// @Produce json
+// @Param uuid path string true "federation UUID"
+// @Param creationRequest body domainService.ParticipantFATEExternalExchangeCreationRequest true "The creation requests"
+// @Success 200 {object} GeneralResponse "Success, the data field is the created exchange's uuid"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /federation/fate/:uuid/exchange/external [post]
func (controller *FederationController) createExternalFATEExchange(c *gin.Context) {
if exchangeUUID, err := func() (string, error) {
federationUUID := c.Param("uuid")
@@ -382,14 +397,15 @@ func (controller *FederationController) createExternalFATEExchange(c *gin.Contex
}
// getFATEParticipant returns participant list of the specified federation
-// @Summary Get participant list of the specified federation
-// @Tags Federation
-// @Produce json
-// @Param uuid path string true "federation UUID"
-// @Success 200 {object} GeneralResponse{data=service.ParticipantFATEListInFederation} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /federation/fate/{uuid}/participant [get]
+//
+// @Summary Get participant list of the specified federation
+// @Tags Federation
+// @Produce json
+// @Param uuid path string true "federation UUID"
+// @Success 200 {object} GeneralResponse{data=service.ParticipantFATEListInFederation} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /federation/fate/{uuid}/participant [get]
func (controller *FederationController) getFATEParticipant(c *gin.Context) {
if participants, err := func() (*service.ParticipantFATEListInFederation, error) {
federationUUID := c.Param("uuid")
@@ -410,16 +426,17 @@ func (controller *FederationController) getFATEParticipant(c *gin.Context) {
}
// deleteFATEExchange deletes the specified FATE exchange
-// @Summary Delete a FATE exchange
-// @Tags Federation
-// @Produce json
-// @Param uuid path string true "federation UUID"
-// @Param exchangeUUID path string true "exchange UUID"
-// @Param force query bool false "if set to true, will try to remove the exchange forcefully"
-// @Success 200 {object} GeneralResponse "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /federation/fate/{uuid}/exchange/{exchangeUUID} [delete]
+//
+// @Summary Delete a FATE exchange
+// @Tags Federation
+// @Produce json
+// @Param uuid path string true "federation UUID"
+// @Param exchangeUUID path string true "exchange UUID"
+// @Param force query bool false "if set to true, will try to remove the exchange forcefully"
+// @Success 200 {object} GeneralResponse "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /federation/fate/{uuid}/exchange/{exchangeUUID} [delete]
func (controller *FederationController) deleteFATEExchange(c *gin.Context) {
exchangeUUID := c.Param("exchangeUUID")
if err := func() error {
@@ -443,43 +460,45 @@ func (controller *FederationController) deleteFATEExchange(c *gin.Context) {
}
// getFATEClusterDeploymentYAML returns deployment yaml content for deploying FATE cluster
-// @Summary Get FATE cluster deployment yaml
-// @Tags Federation
-// @Produce json
-// @Param chart_uuid query string true "the chart uuid"
-// @Param federation_uuid query string true "the federation uuid"
-// @Param party_id query int true "party id"
-// @Param name query string true "name of the deployment"
-// @Param namespace query string true "namespace of the deployment"
-// @Param service_type query int true "type of the service to be exposed"
-// @Param use_registry query bool true "choose if use the FATE registry config saved in the infra provider"
-// @Param enable_external_spark query bool true "enable link an external Spark"
-// @Param external_spark_cores_per_node query int true "external Spark info"
-// @Param external_spark_node query int true "external Spark info"
-// @Param external_spark_master query string true "external Spark info"
-// @Param external_spark_driverHost query string true "external Spark info"
-// @Param external_spark_driverHostType query string true "external Spark info"
-// @Param external_spark_portMaxRetries query int true "external Spark info"
-// @Param external_spark_driverStartPort query int true "external Spark info"
-// @Param external_spark_blockManagerStartPort query int true "external spark info"
-// @Param external_spark_pysparkPython query string false "external spark info"
-// @Param enable_external_hdfs query bool true "enable link an external HDFS"
-// @Param external_hdfs_name_node query string true "external HDFS info"
-// @Param external_hdfs_path_prefix query string false "external HDFS info"
-// @Param enable_external_pulsar query bool true "enable link an external Pulsar"
-// @Param external_pulsar_host query string true "external Pulsar info"
-// @Param external_pulsar_mng_port query int true "external Pulsar info"
-// @Param external_pulsar_port query int true "external Pulsar info"
-// @Param external_pulsar_ssl_port query int true "external Pulsar info"
-// @Param use_registry_secret query bool true "choose if use the FATE registry secret saved in the infra provider"
-// @Param registry query string true "FATE registry config saved in the infra provider"
-// @Param enable_persistence query bool true "choose if use the persistent volume"
-// @Param storage_class query string true "provide the name of StorageClass"
-// @Param enable_psp query bool true "choose if enable the podSecurityPolicy"
-// @Success 200 {object} GeneralResponse{data=string} "Success, the data field is the yaml content"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /federation/fate/cluster/yaml [get]
+//
+// @Summary Get FATE cluster deployment yaml
+// @Tags Federation
+// @Produce json
+// @Param chart_uuid query string true "the chart uuid"
+// @Param federation_uuid query string true "the federation uuid"
+// @Param party_id query int true "party id"
+// @Param name query string true "name of the deployment"
+// @Param namespace query string true "namespace of the deployment"
+// @Param service_type query int true "type of the service to be exposed"
+// @Param use_registry query bool true "choose if use the FATE registry config saved in the infra provider"
+// @Param enable_external_spark query bool true "enable link an external Spark"
+// @Param external_spark_cores_per_node query int true "external Spark info"
+// @Param external_spark_node query int true "external Spark info"
+// @Param external_spark_master query string true "external Spark info"
+// @Param external_spark_driverHost query string true "external Spark info"
+// @Param external_spark_driverHostType query string true "external Spark info"
+// @Param external_spark_portMaxRetries query int true "external Spark info"
+// @Param external_spark_driverStartPort query int true "external Spark info"
+// @Param external_spark_blockManagerStartPort query int true "external spark info"
+// @Param external_spark_pysparkPython query string false "external spark info"
+// @Param enable_external_hdfs query bool true "enable link an external HDFS"
+// @Param external_hdfs_name_node query string true "external HDFS info"
+// @Param external_hdfs_path_prefix query string false "external HDFS info"
+// @Param enable_external_pulsar query bool true "enable link an external Pulsar"
+// @Param external_pulsar_host query string true "external Pulsar info"
+// @Param external_pulsar_mng_port query int true "external Pulsar info"
+// @Param external_pulsar_port query int true "external Pulsar info"
+// @Param external_pulsar_ssl_port query int true "external Pulsar info"
+// @Param use_registry_secret query bool true "choose if use the FATE registry secret saved in the infra provider"
+// @Param registry query string true "FATE registry config saved in the infra provider"
+// @Param enable_persistence query bool true "choose if use the persistent volume"
+// @Param storage_class query string true "provide the name of StorageClass"
+// @Param enable_psp query bool true "choose if enable the podSecurityPolicy"
+// @Param fateflow_gpu_num query int true "number of gpu to assign to fateflow pod, default 0"
+// @Success 200 {object} GeneralResponse{data=string} "Success, the data field is the yaml content"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /federation/fate/cluster/yaml [get]
func (controller *FederationController) getFATEClusterDeploymentYAML(c *gin.Context) {
if yaml, err := func() (string, error) {
chartUUID := c.DefaultQuery("chart_uuid", "")
@@ -493,6 +512,10 @@ func (controller *FederationController) getFATEClusterDeploymentYAML(c *gin.Cont
if err != nil {
return "", err
}
+ fateflowGPUNum, err := strconv.Atoi(c.DefaultQuery("fateflow_gpu_num", "0"))
+ if err != nil {
+ return "", errors.New("invalid fateflow gpu parameter")
+ }
serviceType, err := strconv.Atoi(c.DefaultQuery("service_type", "1"))
if err != nil {
return "", errors.New("invalid service type parameter")
@@ -598,6 +621,7 @@ func (controller *FederationController) getFATEClusterDeploymentYAML(c *gin.Cont
PartyID: partyID,
EnablePersistence: enablePersistence,
StorageClass: storageClass,
+ FATEFlowGPUNum: fateflowGPUNum,
ExternalSpark: domainService.ExternalSpark{
Enable: enableExternalSpark,
Cores_per_node: externalSparkCoresPerNode,
@@ -639,15 +663,16 @@ func (controller *FederationController) getFATEClusterDeploymentYAML(c *gin.Cont
}
// createFATECluster creates a new FATE cluster
-// @Summary Create a new FATE cluster
-// @Tags Federation
-// @Produce json
-// @Param uuid path string true "federation UUID"
-// @Param creationRequest body service.ParticipantFATEClusterCreationRequest true "The creation requests"
-// @Success 200 {object} GeneralResponse "Success, the data field is the created cluster's uuid"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /federation/fate/:uuid/cluster [post]
+//
+// @Summary Create a new FATE cluster
+// @Tags Federation
+// @Produce json
+// @Param uuid path string true "federation UUID"
+// @Param creationRequest body service.ParticipantFATEClusterCreationRequest true "The creation requests"
+// @Success 200 {object} GeneralResponse "Success, the data field is the created cluster's uuid"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /federation/fate/:uuid/cluster [post]
func (controller *FederationController) createFATECluster(c *gin.Context) {
if exchangeUUID, err := func() (string, error) {
federationUUID := c.Param("uuid")
@@ -673,16 +698,17 @@ func (controller *FederationController) createFATECluster(c *gin.Context) {
}
// deleteFATECluster deletes the specified FATE cluster
-// @Summary Delete a FATE cluster
-// @Tags Federation
-// @Produce json
-// @Param uuid path string true "federation UUID"
-// @Param clusterUUID path string true "cluster UUID"
-// @Param force query bool false "if set to true, will try to remove the cluster forcefully"
-// @Success 200 {object} GeneralResponse "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /federation/fate/{uuid}/cluster/{clusterUUID} [delete]
+//
+// @Summary Delete a FATE cluster
+// @Tags Federation
+// @Produce json
+// @Param uuid path string true "federation UUID"
+// @Param clusterUUID path string true "cluster UUID"
+// @Param force query bool false "if set to true, will try to remove the cluster forcefully"
+// @Success 200 {object} GeneralResponse "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /federation/fate/{uuid}/cluster/{clusterUUID} [delete]
func (controller *FederationController) deleteFATECluster(c *gin.Context) {
clusterUUID := c.Param("clusterUUID")
if err := func() error {
@@ -706,15 +732,16 @@ func (controller *FederationController) deleteFATECluster(c *gin.Context) {
}
// createExternalFATECluster creates an external FATE cluster
-// @Summary Create an external FATE cluster
-// @Tags Federation
-// @Produce json
-// @Param uuid path string true "federation UUID"
-// @Param creationRequest body service.ParticipantFATEExternalClusterCreationRequest true "The creation requests"
-// @Success 200 {object} GeneralResponse "Success, the data field is the created cluster's uuid"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /federation/fate/:uuid/cluster/external [post]
+//
+// @Summary Create an external FATE cluster
+// @Tags Federation
+// @Produce json
+// @Param uuid path string true "federation UUID"
+// @Param creationRequest body service.ParticipantFATEExternalClusterCreationRequest true "The creation requests"
+// @Success 200 {object} GeneralResponse "Success, the data field is the created cluster's uuid"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /federation/fate/:uuid/cluster/external [post]
func (controller *FederationController) createExternalFATECluster(c *gin.Context) {
if clusterUUID, err := func() (string, error) {
federationUUID := c.Param("uuid")
@@ -740,15 +767,16 @@ func (controller *FederationController) createExternalFATECluster(c *gin.Context
}
// checkFATEPartyID checks if the party ID is available
-// @Summary Check if the party ID is available
-// @Tags Federation
-// @Produce json
-// @Param uuid path string true "federation UUID"
-// @Param party_id query int true "party ID"
-// @Success 200 {object} GeneralResponse "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /federation/fate/:uuid/partyID/check [post]
+//
+// @Summary Check if the party ID is available
+// @Tags Federation
+// @Produce json
+// @Param uuid path string true "federation UUID"
+// @Param party_id query int true "party ID"
+// @Success 200 {object} GeneralResponse "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /federation/fate/:uuid/partyID/check [post]
func (controller *FederationController) checkFATEPartyID(c *gin.Context) {
if err := func() error {
federationUUID := c.Param("uuid")
@@ -772,14 +800,15 @@ func (controller *FederationController) checkFATEPartyID(c *gin.Context) {
}
// getFATEExchange returns detailed information of a FATE exchange
-// @Summary Get specific info of FATE Exchange
-// @Tags Federation
-// @Produce json
-// @Param uuid path string true "federation UUID"
-// @Success 200 {object} GeneralResponse{data=service.FATEExchangeDetail} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /federation/fate/{uuid}/exchange/{exchangeUUID} [get]
+//
+// @Summary Get specific info of FATE Exchange
+// @Tags Federation
+// @Produce json
+// @Param uuid path string true "federation UUID"
+// @Success 200 {object} GeneralResponse{data=service.FATEExchangeDetail} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /federation/fate/{uuid}/exchange/{exchangeUUID} [get]
func (controller *FederationController) getFATEExchange(c *gin.Context) {
exchangeUUID := c.Param("exchangeUUID")
if exchangeDetail, err := controller.participantAppService.GetFATEExchangeDetail(exchangeUUID); err != nil {
@@ -798,14 +827,15 @@ func (controller *FederationController) getFATEExchange(c *gin.Context) {
}
// getFATECluster returns detailed information of a FATE cluster
-// @Summary Get specific info of FATE cluster
-// @Tags Federation
-// @Produce json
-// @Param uuid path string true "federation UUID"
-// @Success 200 {object} GeneralResponse{data=service.FATEClusterDetail} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /federation/fate/{uuid}/cluster/{clusterUUID} [get]
+//
+// @Summary Get specific info of FATE cluster
+// @Tags Federation
+// @Produce json
+// @Param uuid path string true "federation UUID"
+// @Success 200 {object} GeneralResponse{data=service.FATEClusterDetail} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /federation/fate/{uuid}/cluster/{clusterUUID} [get]
func (controller *FederationController) getFATECluster(c *gin.Context) {
clusterUUID := c.Param("clusterUUID")
if clusterDetail, err := controller.participantAppService.GetFATEClusterDetail(clusterUUID); err != nil {
@@ -822,3 +852,131 @@ func (controller *FederationController) getFATECluster(c *gin.Context) {
c.JSON(http.StatusOK, resp)
}
}
+
+// getFATEExchangeUpgrade returns a FATE exchange upgradeable information
+//
+// @Summary Get specific info of FATE exchange
+// @Tags Federation
+// @Produce json
+// @Param uuid path string true "federation UUID"
+// @Param exchangeUUID path string true "exchange UUID"
+// @Success 200 {object} GeneralResponse{data=service.FATEClusterUpgradeableInfo} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /federation/fate/{uuid}/exchange/{exchangeUUID}/upgrade [get]
+func (controller *FederationController) getFATEExchangeUpgrade(c *gin.Context) {
+ exchangeUUID := c.Param("exchangeUUID")
+ if FATEExchangeUpgradeDetail, err := controller.participantAppService.GetFATEExchangeUpgrade(exchangeUUID); err != nil {
+ resp := &GeneralResponse{
+ Code: constants.RespInternalErr,
+ Message: err.Error(),
+ }
+ c.JSON(http.StatusInternalServerError, resp)
+ } else {
+ resp := &GeneralResponse{
+ Code: constants.RespNoErr,
+ Data: FATEExchangeUpgradeDetail,
+ }
+ c.JSON(http.StatusOK, resp)
+ }
+}
+
+// getFATEClusterUpgrade returns a FATE cluster upgradeable information
+//
+// @Summary Get the upgradeable information of the FATE cluster
+// @Tags Federation
+// @Produce json
+// @Param uuid path string true "federation UUID"
+// @Param clusterUUID path string true "cluster UUID"
+// @Success 200 {object} GeneralResponse{data=service.FATEClusterUpgradeableInfo} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /federation/fate/{uuid}/cluster/{clusterUUID}/upgrade [get]
+func (controller *FederationController) getFATEClusterUpgrade(c *gin.Context) {
+ clusterUUID := c.Param("clusterUUID")
+ if FATEClusterUpgradeDetail, err := controller.participantAppService.GetFATEClusterUpgrade(clusterUUID); err != nil {
+ resp := &GeneralResponse{
+ Code: constants.RespInternalErr,
+ Message: err.Error(),
+ }
+ c.JSON(http.StatusInternalServerError, resp)
+ } else {
+ resp := &GeneralResponse{
+ Code: constants.RespNoErr,
+ Data: FATEClusterUpgradeDetail,
+ }
+ c.JSON(http.StatusOK, resp)
+ }
+}
+
+// upgradeFATEExchange upgrade the FATE exchange
+//
+// @Summary Upgrade the FATE exchange
+// @Tags Federation
+// @Produce json
+// @Param uuid path string true "federation UUID"
+// @Param exchangeUUID path string true "exchange UUID"
+// @Param upgradeVersion query string true "upgrade version"
+// @Success 200 {object} GeneralResponse "Success, the data field is the created exchange's uuid"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /federation/fate/{uuid}/exchange/{exchangeUUID}/upgrade [post]
+func (controller *FederationController) upgradeFATEExchange(c *gin.Context) {
+ if exchangeUUID, err := func() (string, error) {
+ req := &domainService.ParticipantFATEExchangeUpgradeRequest{
+ ExchangeUUID: c.Param("exchangeUUID"),
+ FederationUUID: c.Param("uuid"),
+ UpgradeVersion: c.Query("upgradeVersion"),
+ }
+ log.Debug().Interface("req", req).Msg("Get request info")
+ return controller.participantAppService.UpgradeFATEExchange(req)
+ }(); err != nil {
+ resp := &GeneralResponse{
+ Code: constants.RespInternalErr,
+ Message: err.Error(),
+ }
+ c.JSON(http.StatusInternalServerError, resp)
+ } else {
+ resp := &GeneralResponse{
+ Code: constants.RespNoErr,
+ Data: exchangeUUID,
+ }
+ c.JSON(http.StatusOK, resp)
+ }
+}
+
+// createFATEExchange upgrade the FATE cluster
+//
+// @Summary Upgrade the FATE cluster
+// @Tags Federation
+// @Produce json
+// @Param uuid path string true "federation UUID"
+// @Param clusterUUID path string true "cluster UUID"
+// @Param upgradeVersion query string true "upgrade version"
+// @Success 200 {object} GeneralResponse "Success, the data field is the upgrade cluster's uuid"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /federation/fate/{uuid}/cluster/{clusterUUID}/upgrade [post]
+func (controller *FederationController) upgradeFATECluster(c *gin.Context) {
+ if exchangeUUID, err := func() (string, error) {
+ req := &domainService.ParticipantFATEClusterUpgradeRequest{
+ ClusterUUID: c.Param("clusterUUID"),
+ FederationUUID: c.Param("uuid"),
+ UpgradeVersion: c.Query("upgradeVersion"),
+ }
+ log.Debug().Interface("req", req).Msg("Get request info")
+ return controller.participantAppService.UpgradeFATECluster(req)
+ }(); err != nil {
+ resp := &GeneralResponse{
+ Code: constants.RespInternalErr,
+ Message: err.Error(),
+ }
+ c.JSON(http.StatusInternalServerError, resp)
+ } else {
+ resp := &GeneralResponse{
+ Code: constants.RespNoErr,
+ Data: exchangeUUID,
+ }
+ c.JSON(http.StatusOK, resp)
+ }
+}
diff --git a/server/api/federation_openfl.go b/server/api/federation_openfl.go
index 39abe502..d7478b58 100644
--- a/server/api/federation_openfl.go
+++ b/server/api/federation_openfl.go
@@ -25,14 +25,15 @@ import (
)
// createOpenFL creates a new OpenFL federation
-// @Summary Create a new OpenFL federation
-// @Tags Federation
-// @Produce json
-// @Param federation body service.FederationOpenFLCreationRequest true "The federation info"
-// @Success 200 {object} GeneralResponse "Success, the data field is the created federation's uuid"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /federation/openfl [post]
+//
+// @Summary Create a new OpenFL federation
+// @Tags Federation
+// @Produce json
+// @Param federation body service.FederationOpenFLCreationRequest true "The federation info"
+// @Success 200 {object} GeneralResponse "Success, the data field is the created federation's uuid"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /federation/openfl [post]
func (controller *FederationController) createOpenFL(c *gin.Context) {
if uuid, err := func() (string, error) {
creationInfo := &service.FederationOpenFLCreationRequest{}
@@ -56,14 +57,15 @@ func (controller *FederationController) createOpenFL(c *gin.Context) {
}
// getOpenFL returns detailed information of an OpenFL federation
-// @Summary Get specific info of an OpenFL federation
-// @Tags Federation
-// @Produce json
-// @Param uuid path string true "federation UUID"
-// @Success 200 {object} GeneralResponse{data=service.FederationOpenFLDetail} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /federation/openfl/{uuid} [get]
+//
+// @Summary Get specific info of an OpenFL federation
+// @Tags Federation
+// @Produce json
+// @Param uuid path string true "federation UUID"
+// @Success 200 {object} GeneralResponse{data=service.FederationOpenFLDetail} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /federation/openfl/{uuid} [get]
func (controller *FederationController) getOpenFL(c *gin.Context) {
uuid := c.Param("uuid")
if info, err := controller.federationApp.GetOpenFLFederation(uuid); err != nil {
@@ -82,14 +84,15 @@ func (controller *FederationController) getOpenFL(c *gin.Context) {
}
// deleteOpenFL deletes the specified federation
-// @Summary Delete an OpenFL federation
-// @Tags Federation
-// @Produce json
-// @Param uuid path string true "federation UUID"
-// @Success 200 {object} GeneralResponse "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /federation/openfl/{uuid} [delete]
+//
+// @Summary Delete an OpenFL federation
+// @Tags Federation
+// @Produce json
+// @Param uuid path string true "federation UUID"
+// @Success 200 {object} GeneralResponse "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /federation/openfl/{uuid} [delete]
func (controller *FederationController) deleteOpenFL(c *gin.Context) {
uuid := c.Param("uuid")
if err := controller.federationApp.DeleteOpenFLFederation(uuid); err != nil {
@@ -107,14 +110,15 @@ func (controller *FederationController) deleteOpenFL(c *gin.Context) {
}
// listOpenFLToken returns token list of the specified federation
-// @Summary Get registration token list of the specified OpenFL federation
-// @Tags Federation
-// @Produce json
-// @Param uuid path string true "federation UUID"
-// @Success 200 {object} GeneralResponse{data=[]service.RegistrationTokenOpenFLListItem} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /federation/openfl/{uuid}/token [get]
+//
+// @Summary Get registration token list of the specified OpenFL federation
+// @Tags Federation
+// @Produce json
+// @Param uuid path string true "federation UUID"
+// @Success 200 {object} GeneralResponse{data=[]service.RegistrationTokenOpenFLListItem} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /federation/openfl/{uuid}/token [get]
func (controller *FederationController) listOpenFLToken(c *gin.Context) {
if tokens, err := func() ([]service.RegistrationTokenOpenFLListItem, error) {
federationUUID := c.Param("uuid")
@@ -135,15 +139,16 @@ func (controller *FederationController) listOpenFLToken(c *gin.Context) {
}
// createOpenFLToken creates a new registration token for an OpenFL federation
-// @Summary Create a new registration token for an OpenFL federation
-// @Tags Federation
-// @Produce json
-// @Param uuid path string true "federation UUID"
-// @Param token body service.RegistrationTokenOpenFLBasicInfo true "The federation info"
-// @Success 200 {object} GeneralResponse "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /federation/openfl/{uuid}/token [post]
+//
+// @Summary Create a new registration token for an OpenFL federation
+// @Tags Federation
+// @Produce json
+// @Param uuid path string true "federation UUID"
+// @Param token body service.RegistrationTokenOpenFLBasicInfo true "The federation info"
+// @Success 200 {object} GeneralResponse "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /federation/openfl/{uuid}/token [post]
func (controller *FederationController) createOpenFLToken(c *gin.Context) {
if err := func() error {
creationInfo := &service.RegistrationTokenOpenFLBasicInfo{}
@@ -167,15 +172,16 @@ func (controller *FederationController) createOpenFLToken(c *gin.Context) {
}
// deleteOpenFLToken deletes the specified OpenFL federation registration token
-// @Summary Delete an OpenFL federation registration token
-// @Tags Federation
-// @Produce json
-// @Param uuid path string true "federation UUID"
-// @Param tokenUUID path string true "token UUID"
-// @Success 200 {object} GeneralResponse "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /federation/openfl/{uuid}/token/{tokenUUID} [delete]
+//
+// @Summary Delete an OpenFL federation registration token
+// @Tags Federation
+// @Produce json
+// @Param uuid path string true "federation UUID"
+// @Param tokenUUID path string true "token UUID"
+// @Success 200 {object} GeneralResponse "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /federation/openfl/{uuid}/token/{tokenUUID} [delete]
func (controller *FederationController) deleteOpenFLToken(c *gin.Context) {
uuid := c.Param("tokenUUID")
federationUUID := c.Param("uuid")
@@ -194,23 +200,24 @@ func (controller *FederationController) deleteOpenFLToken(c *gin.Context) {
}
// getOpenFLDirectorDeploymentYAML returns deployment yaml content for deploying OpenFL director
-// @Summary Get OpenFL director deployment yaml
-// @Tags Federation
-// @Produce json
-// @Param chart_uuid query string true "the chart uuid"
-// @Param federation_uuid query string true "the federation uuid"
-// @Param name query string true "name of the deployment"
-// @Param namespace query string true "namespace of the deployment"
-// @Param service_type query int true "type of the service to be exposed 1: LoadBalancer 2: NodePort"
-// @Param jupyter_password query string true "password to access the Jupyter Notebook"
-// @Param registry query string true "customized registry address"
-// @Param use_registry query bool true "choose if use the customized registry config"
-// @Param use_registry_secret query bool true "choose if use the customized registry secret"
-// @Param enable_psp query bool true "choose if enable the podSecurityPolicy"
-// @Success 200 {object} GeneralResponse{data=string} "Success, the data field is the yaml content"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /federation/openfl/director/yaml [get]
+//
+// @Summary Get OpenFL director deployment yaml
+// @Tags Federation
+// @Produce json
+// @Param chart_uuid query string true "the chart uuid"
+// @Param federation_uuid query string true "the federation uuid"
+// @Param name query string true "name of the deployment"
+// @Param namespace query string true "namespace of the deployment"
+// @Param service_type query int true "type of the service to be exposed 1: LoadBalancer 2: NodePort"
+// @Param jupyter_password query string true "password to access the Jupyter Notebook"
+// @Param registry query string true "customized registry address"
+// @Param use_registry query bool true "choose if use the customized registry config"
+// @Param use_registry_secret query bool true "choose if use the customized registry secret"
+// @Param enable_psp query bool true "choose if enable the podSecurityPolicy"
+// @Success 200 {object} GeneralResponse{data=string} "Success, the data field is the yaml content"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /federation/openfl/director/yaml [get]
func (controller *FederationController) getOpenFLDirectorDeploymentYAML(c *gin.Context) {
if yaml, err := func() (string, error) {
req := &domainService.ParticipantOpenFLDirectorYAMLCreationRequest{}
@@ -237,15 +244,16 @@ func (controller *FederationController) getOpenFLDirectorDeploymentYAML(c *gin.C
}
// createOpenFLDirector creates a new OpenFL director
-// @Summary Create a new OpenFL director
-// @Tags Federation
-// @Produce json
-// @Param uuid path string true "federation UUID"
-// @Param creationRequest body service.ParticipantOpenFLDirectorCreationRequest true "The creation requests"
-// @Success 200 {object} GeneralResponse "Success, the data field is the created director's uuid"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /federation/openfl/{uuid}/director [post]
+//
+// @Summary Create a new OpenFL director
+// @Tags Federation
+// @Produce json
+// @Param uuid path string true "federation UUID"
+// @Param creationRequest body service.ParticipantOpenFLDirectorCreationRequest true "The creation requests"
+// @Success 200 {object} GeneralResponse "Success, the data field is the created director's uuid"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /federation/openfl/{uuid}/director [post]
func (controller *FederationController) createOpenFLDirector(c *gin.Context) {
if uuid, err := func() (string, error) {
federationUUID := c.Param("uuid")
@@ -271,16 +279,17 @@ func (controller *FederationController) createOpenFLDirector(c *gin.Context) {
}
// deleteOpenFLDirector deletes the specified OpenFL director
-// @Summary Delete an OpenFL director
-// @Tags Federation
-// @Produce json
-// @Param uuid path string true "federation UUID"
-// @Param directorUUID path string true "director UUID"
-// @Param force query bool false "if set to true, will try to remove the director forcefully"
-// @Success 200 {object} GeneralResponse "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /federation/openfl/{uuid}/director/{directorUUID} [delete]
+//
+// @Summary Delete an OpenFL director
+// @Tags Federation
+// @Produce json
+// @Param uuid path string true "federation UUID"
+// @Param directorUUID path string true "director UUID"
+// @Param force query bool false "if set to true, will try to remove the director forcefully"
+// @Success 200 {object} GeneralResponse "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /federation/openfl/{uuid}/director/{directorUUID} [delete]
func (controller *FederationController) deleteOpenFLDirector(c *gin.Context) {
directorUUID := c.Param("directorUUID")
if err := func() error {
@@ -304,14 +313,15 @@ func (controller *FederationController) deleteOpenFLDirector(c *gin.Context) {
}
// getOpenFLParticipant returns participant list of the specified federation
-// @Summary Get participant list of the specified OpenFL federation
-// @Tags Federation
-// @Produce json
-// @Param uuid path string true "federation UUID"
-// @Success 200 {object} GeneralResponse{data=service.ParticipantOpenFLListInFederation} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /federation/openfl/{uuid}/participant [get]
+//
+// @Summary Get participant list of the specified OpenFL federation
+// @Tags Federation
+// @Produce json
+// @Param uuid path string true "federation UUID"
+// @Success 200 {object} GeneralResponse{data=service.ParticipantOpenFLListInFederation} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /federation/openfl/{uuid}/participant [get]
func (controller *FederationController) getOpenFLParticipant(c *gin.Context) {
if participants, err := func() (*service.ParticipantOpenFLListInFederation, error) {
federationUUID := c.Param("uuid")
@@ -332,15 +342,16 @@ func (controller *FederationController) getOpenFLParticipant(c *gin.Context) {
}
// getOpenFLDirector returns detailed information of a OpenFL director
-// @Summary Get specific info of OpenFL director
-// @Tags Federation
-// @Produce json
-// @Param uuid path string true "federation UUID"
-// @Param directorUUID path string true "director UUID"
-// @Success 200 {object} GeneralResponse{data=service.OpenFLDirectorDetail} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /federation/openfl/{uuid}/director/{directorUUID} [get]
+//
+// @Summary Get specific info of OpenFL director
+// @Tags Federation
+// @Produce json
+// @Param uuid path string true "federation UUID"
+// @Param directorUUID path string true "director UUID"
+// @Success 200 {object} GeneralResponse{data=service.OpenFLDirectorDetail} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /federation/openfl/{uuid}/director/{directorUUID} [get]
func (controller *FederationController) getOpenFLDirector(c *gin.Context) {
directorUUID := c.Param("directorUUID")
if directorDetail, err := controller.participantAppService.GetOpenFLDirectorDetail(directorUUID); err != nil {
@@ -359,15 +370,16 @@ func (controller *FederationController) getOpenFLDirector(c *gin.Context) {
}
// registerOpenFLEnvoy handles envoy registration request
-// @Summary Process Envoy registration request
-// @Tags Federation
-// @Produce json
-// @Param uuid path string true "federation UUID"
-// @Param registrationRequest body service.ParticipantOpenFLEnvoyRegistrationRequest true "The creation requests"
-// @Success 200 {object} GeneralResponse "Success, the data field is the created director's uuid"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /federation/openfl/envoy/register [post]
+//
+// @Summary Process Envoy registration request
+// @Tags Federation
+// @Produce json
+// @Param uuid path string true "federation UUID"
+// @Param registrationRequest body service.ParticipantOpenFLEnvoyRegistrationRequest true "The creation requests"
+// @Success 200 {object} GeneralResponse "Success, the data field is the created director's uuid"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /federation/openfl/envoy/register [post]
func (controller *FederationController) registerOpenFLEnvoy(c *gin.Context) {
if uuid, err := func() (string, error) {
req := &domainService.ParticipantOpenFLEnvoyRegistrationRequest{}
@@ -391,16 +403,17 @@ func (controller *FederationController) registerOpenFLEnvoy(c *gin.Context) {
}
// deleteOpenFLEnvoy deletes the specified OpenFL envoy
-// @Summary Delete an OpenFL envoy
-// @Tags Federation
-// @Produce json
-// @Param uuid path string true "federation UUID"
-// @Param envoyUUID path string true "envoy UUID"
-// @Param force query bool false "if set to true, will try to envoy the director forcefully"
-// @Success 200 {object} GeneralResponse "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /federation/openfl/{uuid}/envoy/{envoyUUID} [delete]
+//
+// @Summary Delete an OpenFL envoy
+// @Tags Federation
+// @Produce json
+// @Param uuid path string true "federation UUID"
+// @Param envoyUUID path string true "envoy UUID"
+// @Param force query bool false "if set to true, will try to envoy the director forcefully"
+// @Success 200 {object} GeneralResponse "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /federation/openfl/{uuid}/envoy/{envoyUUID} [delete]
func (controller *FederationController) deleteOpenFLEnvoy(c *gin.Context) {
envoyUUID := c.Param("envoyUUID")
if err := func() error {
@@ -424,15 +437,16 @@ func (controller *FederationController) deleteOpenFLEnvoy(c *gin.Context) {
}
// getOpenFLEnvoy returns detailed information of a OpenFL envoy
-// @Summary Get specific info of OpenFL envoy
-// @Tags Federation
-// @Produce json
-// @Param uuid path string true "federation UUID"
-// @Param envoyUUID path string true "envoy UUID"
-// @Success 200 {object} GeneralResponse{data=service.OpenFLEnvoyDetail} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /federation/openfl/{uuid}/envoy/{envoyUUID} [get]
+//
+// @Summary Get specific info of OpenFL envoy
+// @Tags Federation
+// @Produce json
+// @Param uuid path string true "federation UUID"
+// @Param envoyUUID path string true "envoy UUID"
+// @Success 200 {object} GeneralResponse{data=service.OpenFLEnvoyDetail} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /federation/openfl/{uuid}/envoy/{envoyUUID} [get]
func (controller *FederationController) getOpenFLEnvoy(c *gin.Context) {
envoyUUID := c.Param("envoyUUID")
if envoyDetail, err := controller.participantAppService.GetOpenFLEnvoyDetail(envoyUUID); err != nil {
@@ -451,15 +465,16 @@ func (controller *FederationController) getOpenFLEnvoy(c *gin.Context) {
}
// getOpenFLEnvoyWithToken returns detailed information of a OpenFL envoy, if the provided token is valid
-// @Summary Get specific info of OpenFL envoy, by providing the envoy uuid and token string
-// @Tags Federation
-// @Produce json
-// @Param uuid path string true "envoy UUID"
-// @Param token query string true "token string"
-// @Success 200 {object} GeneralResponse{data=service.OpenFLEnvoyDetail} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /federation/openfl/envoy/{uuid} [get]
+//
+// @Summary Get specific info of OpenFL envoy, by providing the envoy uuid and token string
+// @Tags Federation
+// @Produce json
+// @Param uuid path string true "envoy UUID"
+// @Param token query string true "token string"
+// @Success 200 {object} GeneralResponse{data=service.OpenFLEnvoyDetail} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /federation/openfl/envoy/{uuid} [get]
func (controller *FederationController) getOpenFLEnvoyWithToken(c *gin.Context) {
token := c.Query("token")
envoyUUID := c.Param("uuid")
diff --git a/server/api/infra_provider.go b/server/api/infra_provider.go
index 34d5ca61..0e0f87cd 100644
--- a/server/api/infra_provider.go
+++ b/server/api/infra_provider.go
@@ -56,13 +56,14 @@ func (controller *InfraProviderController) Route(r *gin.RouterGroup) {
}
// list returns the provider list
-// @Summary Return provider list data
-// @Tags InfraProvider
-// @Produce json
-// @Success 200 {object} GeneralResponse{data=[]service.InfraProviderListItem} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /infra [get]
+//
+// @Summary Return provider list data
+// @Tags InfraProvider
+// @Produce json
+// @Success 200 {object} GeneralResponse{data=[]service.InfraProviderListItem} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /infra [get]
func (controller *InfraProviderController) list(c *gin.Context) {
providerList, err := controller.infraProviderAppService.GetProviderList()
if err != nil {
@@ -83,14 +84,15 @@ func (controller *InfraProviderController) list(c *gin.Context) {
}
// create a new provider
-// @Summary Create a new infra provider
-// @Tags InfraProvider
-// @Produce json
-// @Param provider body service.InfraProviderCreationRequest true "The provider information, currently for the type field only 'Kubernetes' is supported"
-// @Success 200 {object} GeneralResponse "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /infra [post]
+//
+// @Summary Create a new infra provider
+// @Tags InfraProvider
+// @Produce json
+// @Param provider body service.InfraProviderCreationRequest true "The provider information, currently for the type field only 'Kubernetes' is supported"
+// @Success 200 {object} GeneralResponse "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /infra [post]
func (controller *InfraProviderController) create(c *gin.Context) {
if err := func() error {
providerInfo := &service.InfraProviderCreationRequest{}
@@ -113,14 +115,15 @@ func (controller *InfraProviderController) create(c *gin.Context) {
}
// testKubernetes test connection to Kubernetes infra provider
-// @Summary Test connection to a Kubernetes infra provider
-// @Tags InfraProvider
-// @Produce json
-// @Param permission body valueobject.KubeConfig true "The kubeconfig content"
-// @Success 200 {object} GeneralResponse "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /infra/kubernetes/connect [post]
+//
+// @Summary Test connection to a Kubernetes infra provider
+// @Tags InfraProvider
+// @Produce json
+// @Param permission body valueobject.KubeConfig true "The kubeconfig content"
+// @Success 200 {object} GeneralResponse "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /infra/kubernetes/connect [post]
func (controller *InfraProviderController) testKubernetes(c *gin.Context) {
if err := func() error {
kubeconfig := &valueobject.KubeConfig{}
@@ -143,14 +146,15 @@ func (controller *InfraProviderController) testKubernetes(c *gin.Context) {
}
// get returns detailed information of an infra provider
-// @Summary Get infra provider's detailed info
-// @Tags InfraProvider
-// @Produce json
-// @Param uuid path string true "Provider UUID"
-// @Success 200 {object} GeneralResponse{data=service.InfraProviderDetail} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /infra/{uuid} [get]
+//
+// @Summary Get infra provider's detailed info
+// @Tags InfraProvider
+// @Produce json
+// @Param uuid path string true "Provider UUID"
+// @Success 200 {object} GeneralResponse{data=service.InfraProviderDetail} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /infra/{uuid} [get]
func (controller *InfraProviderController) get(c *gin.Context) {
uuid := c.Param("uuid")
if providerDetail, err := controller.infraProviderAppService.GetProviderDetail(uuid); err != nil {
@@ -168,15 +172,16 @@ func (controller *InfraProviderController) get(c *gin.Context) {
}
}
-// delete the infra provider
-// @Summary Delete the infra provider
-// @Tags InfraProvider
-// @Produce json
-// @Param uuid path string true "Provider UUID"
-// @Success 200 {object} GeneralResponse "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /infra/{uuid} [delete]
+// delete the infra provider
+//
+// @Summary Delete the infra provider
+// @Tags InfraProvider
+// @Produce json
+// @Param uuid path string true "Provider UUID"
+// @Success 200 {object} GeneralResponse "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /infra/{uuid} [delete]
func (controller *InfraProviderController) delete(c *gin.Context) {
uuid := c.Param("uuid")
if err := controller.infraProviderAppService.DeleteProvider(uuid); err != nil {
@@ -193,16 +198,17 @@ func (controller *InfraProviderController) delete(c *gin.Context) {
}
}
-// update the provider configuration
-// @Summary Updates the infra provider
-// @Tags InfraProvider
-// @Produce json
-// @Param uuid path string true "Provider UUID"
-// @Param provider body service.InfraProviderUpdateRequest true "The updated provider information"
-// @Success 200 {object} GeneralResponse "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /infra/{uuid} [put]
+// update the provider configuration
+//
+// @Summary Updates the infra provider
+// @Tags InfraProvider
+// @Produce json
+// @Param uuid path string true "Provider UUID"
+// @Param provider body service.InfraProviderUpdateRequest true "The updated provider information"
+// @Success 200 {object} GeneralResponse "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /infra/{uuid} [put]
func (controller *InfraProviderController) update(c *gin.Context) {
if err := func() error {
uuid := c.Param("uuid")
diff --git a/server/api/user.go b/server/api/user.go
index b0d80bf9..2378a601 100644
--- a/server/api/user.go
+++ b/server/api/user.go
@@ -54,36 +54,39 @@ func (controller *UserController) Route(r *gin.RouterGroup) {
}
// login to lifecycle manager using the provided credentials
-// @Summary login to lifecycle manager
-// @Tags User
-// @Produce json
-// @Param credentials body service.LoginInfo true "credentials for login"
-// @Success 200 {object} GeneralResponse "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Router /user/login [post]
+//
+// @Summary login to lifecycle manager
+// @Tags User
+// @Produce json
+// @Param credentials body service.LoginInfo true "credentials for login"
+// @Success 200 {object} GeneralResponse "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Router /user/login [post]
func (controller *UserController) login(c *gin.Context) {
authMiddleware.LoginHandler(c)
}
// logout from the lifecycle manager
-// @Summary logout from the lifecycle manager
-// @Tags User
-// @Produce json
-// @Success 200 {object} GeneralResponse "Success"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /user/logout [post]
+//
+// @Summary logout from the lifecycle manager
+// @Tags User
+// @Produce json
+// @Success 200 {object} GeneralResponse "Success"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /user/logout [post]
func (controller *UserController) logout(c *gin.Context) {
authMiddleware.LogoutHandler(c)
}
// getCurrentUser return current user
-// @Summary Return current user in the jwt token
-// @Tags User
-// @Produce json
-// @Success 200 {object} GeneralResponse{data=string} "Success, the name of current user"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /user/current [get]
+//
+// @Summary Return current user in the jwt token
+// @Tags User
+// @Produce json
+// @Success 200 {object} GeneralResponse{data=string} "Success, the name of current user"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /user/current [get]
func (controller *UserController) getCurrentUsername(c *gin.Context) {
if username, err := func() (string, error) {
claims := jwt.ExtractClaims(c)
@@ -106,14 +109,15 @@ func (controller *UserController) getCurrentUsername(c *gin.Context) {
}
// updatePassword update user password
-// @Summary Update user password
-// @Tags User
-// @Produce json
-// @Param passwordChangeInfo body service.PwdChangeInfo string "current and new password"
-// @Success 200 {object} GeneralResponse "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /user/{userId}/password [put]
+//
+// @Summary Update user password
+// @Tags User
+// @Produce json
+// @Param passwordChangeInfo body service.PwdChangeInfo string "current and new password"
+// @Success 200 {object} GeneralResponse "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /user/{userId}/password [put]
func (controller *UserController) updatePassword(c *gin.Context) {
if err := func() error {
userId, err := strconv.Atoi(c.Param("id"))
diff --git a/server/application/service/participant_service.go b/server/application/service/participant_service.go
index 1881fb32..27abb1bf 100644
--- a/server/application/service/participant_service.go
+++ b/server/application/service/participant_service.go
@@ -20,7 +20,10 @@ import (
"github.com/FederatedAI/FedLCM/server/domain/entity"
"github.com/FederatedAI/FedLCM/server/domain/repo"
"github.com/FederatedAI/FedLCM/server/domain/service"
+ "github.com/FederatedAI/FedLCM/server/domain/utils"
+ "github.com/hashicorp/go-version"
"github.com/pkg/errors"
+ "github.com/rs/zerolog/log"
)
// ParticipantApp provide functions to manage the participants
@@ -53,9 +56,11 @@ type ParticipantFATEListItem struct {
InfraProviderName string `json:"infra_provider_name"`
InfraProviderUUID string `json:"infra_provider_uuid"`
ChartUUID string `json:"chart_uuid"`
+ Version string `json:"version"`
Namespace string `json:"namespace"`
PartyID int `json:"party_id"`
ClusterUUID string `json:"cluster_uuid"`
+ Upgradeable bool `json:"upgradeable"`
Status entity.ParticipantFATEStatus `json:"status"`
AccessInfo entity.ParticipantFATEModulesAccessMap `json:"access_info"`
IsManaged bool `json:"is_managed"`
@@ -86,6 +91,13 @@ type FATEClusterDetail struct {
SitePortalClientCertInfo entity.ParticipantComponentCertInfo `json:"site_portal_client_cert_info"`
}
+type FATEClusterUpgradeableVersionList []string
+
+type FATEClusterUpgradeableInfo struct {
+ FATEClusterVersion string `json:"version"`
+ FATEClusterUpgradeableVersionList `json:"upgradeable_version_list"`
+}
+
// CheckFATPartyID returns error if the current party id is taken in the specified federation
func (app *ParticipantApp) CheckFATPartyID(federationUUID string, partyID int) error {
return app.getFATEDomainService().CheckPartyIDConflict(federationUUID, partyID)
@@ -193,10 +205,12 @@ func (app *ParticipantApp) GetFATEParticipantList(federationUUID string) (*Parti
InfraProviderName: "Unknown",
InfraProviderUUID: "Unknown",
ChartUUID: domainParticipant.ChartUUID,
+ Version: utils.GetChartVersionFromDeploymentYAML(domainParticipant.DeploymentYAML),
Namespace: domainParticipant.Namespace,
PartyID: domainParticipant.PartyID,
ClusterUUID: domainParticipant.ClusterUUID,
Status: domainParticipant.Status,
+ Upgradeable: app.checkFATEClusterUpgrade(domainParticipant.UUID) && domainParticipant.Status == entity.ParticipantFATEStatusActive,
AccessInfo: domainParticipant.AccessInfo,
IsManaged: domainParticipant.IsManaged,
}
@@ -237,9 +251,11 @@ func (app *ParticipantApp) GetFATEExchangeDetail(uuid string) (*FATEExchangeDeta
InfraProviderName: "Unknown",
InfraProviderUUID: "Unknown",
ChartUUID: participant.ChartUUID,
+ Version: utils.GetChartVersionFromDeploymentYAML(participant.DeploymentYAML),
Namespace: participant.Namespace,
PartyID: participant.PartyID,
ClusterUUID: participant.ClusterUUID,
+ Upgradeable: app.checkFATEClusterUpgrade(participant.UUID) && participant.Status == entity.ParticipantFATEStatusActive,
Status: participant.Status,
AccessInfo: participant.AccessInfo,
IsManaged: participant.IsManaged,
@@ -280,9 +296,11 @@ func (app *ParticipantApp) GetFATEClusterDetail(uuid string) (*FATEClusterDetail
InfraProviderName: "Unknown",
InfraProviderUUID: "Unknown",
ChartUUID: participant.ChartUUID,
+ Version: utils.GetChartVersionFromDeploymentYAML(participant.DeploymentYAML),
Namespace: participant.Namespace,
PartyID: participant.PartyID,
ClusterUUID: participant.ClusterUUID,
+ Upgradeable: app.checkFATEClusterUpgrade(participant.UUID) && participant.Status == entity.ParticipantFATEStatusActive,
Status: participant.Status,
AccessInfo: participant.AccessInfo,
IsManaged: participant.IsManaged,
@@ -305,3 +323,114 @@ func (app *ParticipantApp) GetFATEClusterDetail(uuid string) (*FATEClusterDetail
}
return participantDetail, nil
}
+
+// checkFATEClusterUpgrade If the type chart corresponding to chartuuid can be upgraded, return true
+// Under what circumstances can it be upgraded: the chartlist contains charts of a higher version of the same type
+func (app *ParticipantApp) checkFATEClusterUpgrade(CLusterUUID string) bool {
+ FATEClusterVersion, UpgradeableVersionList, err := app.getFATEClusterUpgradeableVersionList(CLusterUUID)
+ if err != nil {
+ return false
+ }
+ return utils.Upgradeable(FATEClusterVersion, UpgradeableVersionList)
+}
+
+func (app *ParticipantApp) GetFATEExchangeUpgrade(ExchangeUUID string) (*FATEClusterUpgradeableInfo, error) {
+ FATEExchangeVersion, UpgradeableVersionList, err := app.getFATEClusterUpgradeableVersionList(ExchangeUUID)
+ if err != nil {
+ log.Err(err).Msg("GetFATEExchangeUpgrade error")
+ return nil, err
+ }
+
+ return &FATEClusterUpgradeableInfo{
+ FATEClusterVersion: FATEExchangeVersion,
+ FATEClusterUpgradeableVersionList: UpgradeableVersionList,
+ }, nil
+}
+
+func (app *ParticipantApp) getFATEClusterUpgradeableVersionList(ClusterUUID string) (string, []string, error) {
+ var versionlist []string
+ var ClusterChartVersion, ClusterChartName string
+ var ChartType entity.ChartType
+ participantInstance, err := app.ParticipantFATERepo.GetByUUID(ClusterUUID)
+ if err != nil {
+ return "", nil, err
+ }
+ participant := participantInstance.(*entity.ParticipantFATE)
+
+ //Check whether it is a cluster managed by fedlcm, a cluster not managed by fedlcm cannot be upgraded
+ if !participant.IsManaged {
+ return "", nil, errors.New("The cluster not managed by FedLCM cannot be upgraded.")
+ }
+
+ instance, err := app.ChartRepo.GetByUUID(participant.ChartUUID)
+ if err != nil {
+ ClusterChartVersion = utils.GetChartVersionFromDeploymentYAML(participant.DeploymentYAML)
+ ClusterChartName = utils.GetChartNameFromDeploymentYAML(participant.DeploymentYAML)
+ } else {
+ Chart := instance.(*entity.Chart)
+ ClusterChartVersion = Chart.Version
+ ClusterChartName = Chart.ChartName
+ }
+
+ if ClusterChartName == "fate-exchange" {
+ ChartType = entity.ChartTypeFATEExchange
+ }
+ if ClusterChartName == "fate" {
+ ChartType = entity.ChartTypeFATECluster
+
+ // TODO: this is a temp solution to prevent upgrading from 1.8 and lower versions as the yaml content has
+ // changed drastically since. Supporting upgrading these old versions would need further workflow.
+ minUpgradeableVersion, _ := version.NewVersion("1.9.0")
+ currentVersion, _ := version.NewVersion(ClusterChartVersion)
+ if currentVersion.LessThan(minUpgradeableVersion) {
+ return ClusterChartVersion, nil, errors.Errorf("cluster version %s is too old to be upgrade by FedLCM", currentVersion)
+ }
+ }
+
+ var domainChartList []entity.Chart
+ if ChartType == entity.ChartTypeUnknown {
+ instanceList, err := app.ChartRepo.List()
+ if err != nil {
+ return "", nil, err
+ }
+ domainChartList = instanceList.([]entity.Chart)
+ } else {
+ instanceList, err := app.ChartRepo.ListByType(ChartType)
+ if err != nil {
+ return "", nil, err
+ }
+ domainChartList = instanceList.([]entity.Chart)
+ }
+ for _, domainChart := range domainChartList {
+ versionlist = append(versionlist, domainChart.Version)
+ }
+ return ClusterChartVersion, utils.Upgradeablelist(ClusterChartVersion, versionlist), nil
+}
+
+func (app *ParticipantApp) GetFATEClusterUpgrade(ClisterUUID string) (*FATEClusterUpgradeableInfo, error) {
+ FATEClusterVersion, UpgradeableVersionList, err := app.getFATEClusterUpgradeableVersionList(ClisterUUID)
+ if err != nil {
+ return nil, err
+ }
+
+ return &FATEClusterUpgradeableInfo{
+ FATEClusterVersion: FATEClusterVersion,
+ FATEClusterUpgradeableVersionList: UpgradeableVersionList,
+ }, nil
+}
+
+func (app *ParticipantApp) UpgradeFATEExchange(req *service.ParticipantFATEExchangeUpgradeRequest) (string, error) {
+ exchange, _, err := app.getFATEDomainService().UpgradeExchange(req)
+ if err != nil {
+ return "", err
+ }
+ return exchange.UUID, err
+}
+
+func (app *ParticipantApp) UpgradeFATECluster(req *service.ParticipantFATEClusterUpgradeRequest) (string, error) {
+ cluster, _, err := app.getFATEDomainService().UpgradeCluster(req)
+ if err != nil {
+ return "", err
+ }
+ return cluster.UUID, err
+}
diff --git a/server/docs/docs.go b/server/docs/docs.go
index 47d5f827..4f5b4d19 100644
--- a/server/docs/docs.go
+++ b/server/docs/docs.go
@@ -1643,6 +1643,13 @@ const docTemplate = `{
"name": "enable_psp",
"in": "query",
"required": true
+ },
+ {
+ "type": "integer",
+ "description": "number of gpu to assign to fateflow pod, default 0",
+ "name": "fateflow_gpu_num",
+ "in": "query",
+ "required": true
}
],
"responses": {
@@ -2045,6 +2052,141 @@ const docTemplate = `{
}
}
},
+ "/federation/fate/{uuid}/cluster/{clusterUUID}/upgrade": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Federation"
+ ],
+ "summary": "Get the upgradeable information of the FATE cluster",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "federation UUID",
+ "name": "uuid",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "cluster UUID",
+ "name": "clusterUUID",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Success",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/api.GeneralResponse"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service.FATEClusterUpgradeableInfo"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "401": {
+ "description": "Unauthorized operation",
+ "schema": {
+ "$ref": "#/definitions/api.GeneralResponse"
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/api.GeneralResponse"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "integer"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "post": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Federation"
+ ],
+ "summary": "Upgrade the FATE cluster",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "federation UUID",
+ "name": "uuid",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "cluster UUID",
+ "name": "clusterUUID",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "upgrade version",
+ "name": "upgradeVersion",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Success, the data field is the upgrade cluster's uuid",
+ "schema": {
+ "$ref": "#/definitions/api.GeneralResponse"
+ }
+ },
+ "401": {
+ "description": "Unauthorized operation",
+ "schema": {
+ "$ref": "#/definitions/api.GeneralResponse"
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/api.GeneralResponse"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "integer"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
"/federation/fate/{uuid}/exchange/{exchangeUUID}": {
"get": {
"produces": [
@@ -2172,6 +2314,141 @@ const docTemplate = `{
}
}
},
+ "/federation/fate/{uuid}/exchange/{exchangeUUID}/upgrade": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Federation"
+ ],
+ "summary": "Get specific info of FATE exchange",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "federation UUID",
+ "name": "uuid",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "exchange UUID",
+ "name": "exchangeUUID",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Success",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/api.GeneralResponse"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service.FATEClusterUpgradeableInfo"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "401": {
+ "description": "Unauthorized operation",
+ "schema": {
+ "$ref": "#/definitions/api.GeneralResponse"
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/api.GeneralResponse"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "integer"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "post": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Federation"
+ ],
+ "summary": "Upgrade the FATE exchange",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "federation UUID",
+ "name": "uuid",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "exchange UUID",
+ "name": "exchangeUUID",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "upgrade version",
+ "name": "upgradeVersion",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Success, the data field is the created exchange's uuid",
+ "schema": {
+ "$ref": "#/definitions/api.GeneralResponse"
+ }
+ },
+ "401": {
+ "description": "Unauthorized operation",
+ "schema": {
+ "$ref": "#/definitions/api.GeneralResponse"
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/api.GeneralResponse"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "integer"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
"/federation/fate/{uuid}/participant": {
"get": {
"produces": [
@@ -4330,8 +4607,28 @@ const docTemplate = `{
"type": {
"type": "integer"
},
+ "upgradeable": {
+ "type": "boolean"
+ },
"uuid": {
"type": "string"
+ },
+ "version": {
+ "type": "string"
+ }
+ }
+ },
+ "service.FATEClusterUpgradeableInfo": {
+ "type": "object",
+ "properties": {
+ "FATEClusterUpgradeableVersionList": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "version": {
+ "type": "string"
}
}
},
@@ -4395,8 +4692,14 @@ const docTemplate = `{
"type": {
"type": "integer"
},
+ "upgradeable": {
+ "type": "boolean"
+ },
"uuid": {
"type": "string"
+ },
+ "version": {
+ "type": "string"
}
}
},
@@ -4802,6 +5105,9 @@ const docTemplate = `{
"externalSpark": {
"$ref": "#/definitions/service.ExternalSpark"
},
+ "fateflow_gpu_num": {
+ "type": "integer"
+ },
"federation_uuid": {
"type": "string"
},
@@ -4985,8 +5291,14 @@ const docTemplate = `{
"type": {
"type": "integer"
},
+ "upgradeable": {
+ "type": "boolean"
+ },
"uuid": {
"type": "string"
+ },
+ "version": {
+ "type": "string"
}
}
},
@@ -5057,6 +5369,9 @@ const docTemplate = `{
"labels": {
"$ref": "#/definitions/valueobject.Labels"
},
+ "less_privileged": {
+ "type": "boolean"
+ },
"name": {
"description": "optional",
"type": "string"
diff --git a/server/docs/swagger.json b/server/docs/swagger.json
index 50037e0e..acb8d9ad 100644
--- a/server/docs/swagger.json
+++ b/server/docs/swagger.json
@@ -1635,6 +1635,13 @@
"name": "enable_psp",
"in": "query",
"required": true
+ },
+ {
+ "type": "integer",
+ "description": "number of gpu to assign to fateflow pod, default 0",
+ "name": "fateflow_gpu_num",
+ "in": "query",
+ "required": true
}
],
"responses": {
@@ -2037,6 +2044,141 @@
}
}
},
+ "/federation/fate/{uuid}/cluster/{clusterUUID}/upgrade": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Federation"
+ ],
+ "summary": "Get the upgradeable information of the FATE cluster",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "federation UUID",
+ "name": "uuid",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "cluster UUID",
+ "name": "clusterUUID",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Success",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/api.GeneralResponse"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service.FATEClusterUpgradeableInfo"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "401": {
+ "description": "Unauthorized operation",
+ "schema": {
+ "$ref": "#/definitions/api.GeneralResponse"
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/api.GeneralResponse"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "integer"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "post": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Federation"
+ ],
+ "summary": "Upgrade the FATE cluster",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "federation UUID",
+ "name": "uuid",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "cluster UUID",
+ "name": "clusterUUID",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "upgrade version",
+ "name": "upgradeVersion",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Success, the data field is the upgrade cluster's uuid",
+ "schema": {
+ "$ref": "#/definitions/api.GeneralResponse"
+ }
+ },
+ "401": {
+ "description": "Unauthorized operation",
+ "schema": {
+ "$ref": "#/definitions/api.GeneralResponse"
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/api.GeneralResponse"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "integer"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
"/federation/fate/{uuid}/exchange/{exchangeUUID}": {
"get": {
"produces": [
@@ -2164,6 +2306,141 @@
}
}
},
+ "/federation/fate/{uuid}/exchange/{exchangeUUID}/upgrade": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Federation"
+ ],
+ "summary": "Get specific info of FATE exchange",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "federation UUID",
+ "name": "uuid",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "exchange UUID",
+ "name": "exchangeUUID",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Success",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/api.GeneralResponse"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/service.FATEClusterUpgradeableInfo"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "401": {
+ "description": "Unauthorized operation",
+ "schema": {
+ "$ref": "#/definitions/api.GeneralResponse"
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/api.GeneralResponse"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "integer"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "post": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "Federation"
+ ],
+ "summary": "Upgrade the FATE exchange",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "federation UUID",
+ "name": "uuid",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "exchange UUID",
+ "name": "exchangeUUID",
+ "in": "path",
+ "required": true
+ },
+ {
+ "type": "string",
+ "description": "upgrade version",
+ "name": "upgradeVersion",
+ "in": "query",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Success, the data field is the created exchange's uuid",
+ "schema": {
+ "$ref": "#/definitions/api.GeneralResponse"
+ }
+ },
+ "401": {
+ "description": "Unauthorized operation",
+ "schema": {
+ "$ref": "#/definitions/api.GeneralResponse"
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/api.GeneralResponse"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "integer"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ },
"/federation/fate/{uuid}/participant": {
"get": {
"produces": [
@@ -4322,8 +4599,28 @@
"type": {
"type": "integer"
},
+ "upgradeable": {
+ "type": "boolean"
+ },
"uuid": {
"type": "string"
+ },
+ "version": {
+ "type": "string"
+ }
+ }
+ },
+ "service.FATEClusterUpgradeableInfo": {
+ "type": "object",
+ "properties": {
+ "FATEClusterUpgradeableVersionList": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "version": {
+ "type": "string"
}
}
},
@@ -4387,8 +4684,14 @@
"type": {
"type": "integer"
},
+ "upgradeable": {
+ "type": "boolean"
+ },
"uuid": {
"type": "string"
+ },
+ "version": {
+ "type": "string"
}
}
},
@@ -4794,6 +5097,9 @@
"externalSpark": {
"$ref": "#/definitions/service.ExternalSpark"
},
+ "fateflow_gpu_num": {
+ "type": "integer"
+ },
"federation_uuid": {
"type": "string"
},
@@ -4977,8 +5283,14 @@
"type": {
"type": "integer"
},
+ "upgradeable": {
+ "type": "boolean"
+ },
"uuid": {
"type": "string"
+ },
+ "version": {
+ "type": "string"
}
}
},
@@ -5049,6 +5361,9 @@
"labels": {
"$ref": "#/definitions/valueobject.Labels"
},
+ "less_privileged": {
+ "type": "boolean"
+ },
"name": {
"description": "optional",
"type": "string"
diff --git a/server/docs/swagger.yaml b/server/docs/swagger.yaml
index 4a790170..e4955bdf 100644
--- a/server/docs/swagger.yaml
+++ b/server/docs/swagger.yaml
@@ -374,8 +374,21 @@ definitions:
type: integer
type:
type: integer
+ upgradeable:
+ type: boolean
uuid:
type: string
+ version:
+ type: string
+ type: object
+ service.FATEClusterUpgradeableInfo:
+ properties:
+ FATEClusterUpgradeableVersionList:
+ items:
+ type: string
+ type: array
+ version:
+ type: string
type: object
service.FATEExchangeDetail:
properties:
@@ -417,8 +430,12 @@ definitions:
type: integer
type:
type: integer
+ upgradeable:
+ type: boolean
uuid:
type: string
+ version:
+ type: string
type: object
service.FederationFATECreationRequest:
properties:
@@ -683,6 +700,8 @@ definitions:
$ref: '#/definitions/service.ExternalPulsar'
externalSpark:
$ref: '#/definitions/service.ExternalSpark'
+ fateflow_gpu_num:
+ type: integer
federation_uuid:
type: string
name:
@@ -806,8 +825,12 @@ definitions:
type: integer
type:
type: integer
+ upgradeable:
+ type: boolean
uuid:
type: string
+ version:
+ type: string
type: object
service.ParticipantOpenFLDirectorCreationRequest:
properties:
@@ -855,6 +878,8 @@ definitions:
type: string
labels:
$ref: '#/definitions/valueobject.Labels'
+ less_privileged:
+ type: boolean
name:
description: optional
type: string
@@ -1982,6 +2007,87 @@ paths:
summary: Get specific info of FATE cluster
tags:
- Federation
+ /federation/fate/{uuid}/cluster/{clusterUUID}/upgrade:
+ get:
+ parameters:
+ - description: federation UUID
+ in: path
+ name: uuid
+ required: true
+ type: string
+ - description: cluster UUID
+ in: path
+ name: clusterUUID
+ required: true
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: Success
+ schema:
+ allOf:
+ - $ref: '#/definitions/api.GeneralResponse'
+ - properties:
+ data:
+ $ref: '#/definitions/service.FATEClusterUpgradeableInfo'
+ type: object
+ "401":
+ description: Unauthorized operation
+ schema:
+ $ref: '#/definitions/api.GeneralResponse'
+ "500":
+ description: Internal server error
+ schema:
+ allOf:
+ - $ref: '#/definitions/api.GeneralResponse'
+ - properties:
+ code:
+ type: integer
+ type: object
+ summary: Get the upgradeable information of the FATE cluster
+ tags:
+ - Federation
+ post:
+ parameters:
+ - description: federation UUID
+ in: path
+ name: uuid
+ required: true
+ type: string
+ - description: cluster UUID
+ in: path
+ name: clusterUUID
+ required: true
+ type: string
+ - description: upgrade version
+ in: query
+ name: upgradeVersion
+ required: true
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: Success, the data field is the upgrade cluster's uuid
+ schema:
+ $ref: '#/definitions/api.GeneralResponse'
+ "401":
+ description: Unauthorized operation
+ schema:
+ $ref: '#/definitions/api.GeneralResponse'
+ "500":
+ description: Internal server error
+ schema:
+ allOf:
+ - $ref: '#/definitions/api.GeneralResponse'
+ - properties:
+ code:
+ type: integer
+ type: object
+ summary: Upgrade the FATE cluster
+ tags:
+ - Federation
/federation/fate/{uuid}/exchange/{exchangeUUID}:
delete:
parameters:
@@ -2057,6 +2163,87 @@ paths:
summary: Get specific info of FATE Exchange
tags:
- Federation
+ /federation/fate/{uuid}/exchange/{exchangeUUID}/upgrade:
+ get:
+ parameters:
+ - description: federation UUID
+ in: path
+ name: uuid
+ required: true
+ type: string
+ - description: exchange UUID
+ in: path
+ name: exchangeUUID
+ required: true
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: Success
+ schema:
+ allOf:
+ - $ref: '#/definitions/api.GeneralResponse'
+ - properties:
+ data:
+ $ref: '#/definitions/service.FATEClusterUpgradeableInfo'
+ type: object
+ "401":
+ description: Unauthorized operation
+ schema:
+ $ref: '#/definitions/api.GeneralResponse'
+ "500":
+ description: Internal server error
+ schema:
+ allOf:
+ - $ref: '#/definitions/api.GeneralResponse'
+ - properties:
+ code:
+ type: integer
+ type: object
+ summary: Get specific info of FATE exchange
+ tags:
+ - Federation
+ post:
+ parameters:
+ - description: federation UUID
+ in: path
+ name: uuid
+ required: true
+ type: string
+ - description: exchange UUID
+ in: path
+ name: exchangeUUID
+ required: true
+ type: string
+ - description: upgrade version
+ in: query
+ name: upgradeVersion
+ required: true
+ type: string
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: Success, the data field is the created exchange's uuid
+ schema:
+ $ref: '#/definitions/api.GeneralResponse'
+ "401":
+ description: Unauthorized operation
+ schema:
+ $ref: '#/definitions/api.GeneralResponse'
+ "500":
+ description: Internal server error
+ schema:
+ allOf:
+ - $ref: '#/definitions/api.GeneralResponse'
+ - properties:
+ code:
+ type: integer
+ type: object
+ summary: Upgrade the FATE exchange
+ tags:
+ - Federation
/federation/fate/{uuid}/participant:
get:
parameters:
@@ -2244,6 +2431,11 @@ paths:
name: enable_psp
required: true
type: boolean
+ - description: number of gpu to assign to fateflow pod, default 0
+ in: query
+ name: fateflow_gpu_num
+ required: true
+ type: integer
produces:
- application/json
responses:
diff --git a/server/domain/entity/chart.go b/server/domain/entity/chart.go
index 834a7a51..a6d03a21 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/entity/participant_fate.go b/server/domain/entity/participant_fate.go
index af1af115..036a34af 100644
--- a/server/domain/entity/participant_fate.go
+++ b/server/domain/entity/participant_fate.go
@@ -17,6 +17,7 @@ package entity
import (
"database/sql/driver"
"encoding/json"
+
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
)
@@ -128,6 +129,7 @@ const (
ParticipantFATEStatusRemoving
ParticipantFATEStatusReconfiguring
ParticipantFATEStatusFailed
+ ParticipantFATEStatusUpgrading
)
func (t ParticipantFATEStatus) String() string {
@@ -142,6 +144,8 @@ func (t ParticipantFATEStatus) String() string {
return "Reconfiguring"
case ParticipantFATEStatusFailed:
return "Failed"
+ case ParticipantFATEStatusUpgrading:
+ return "Upgrading"
}
return "Unknown"
}
diff --git a/server/domain/repo/chart_repo.go b/server/domain/repo/chart_repo.go
index 2734c37a..85f9547e 100644
--- a/server/domain/repo/chart_repo.go
+++ b/server/domain/repo/chart_repo.go
@@ -24,6 +24,8 @@ type ChartRepository interface {
DeleteByUUID(string) error
// GetByUUID returns an *entity.Chart of the specified uuid
GetByUUID(string) (interface{}, error)
+ // GetByNameAndNamespace returns an *entity.Chart of the specified name and version
+ GetByNameAndVersion(string, string) (interface{}, error)
// ListByType takes an entity.ChartType and returns []entity.Chart that is for the specified type
ListByType(interface{}) (interface{}, error)
}
diff --git a/server/domain/service/endpoint_service.go b/server/domain/service/endpoint_service.go
index 5cee8d3b..4ae13e4d 100644
--- a/server/domain/service/endpoint_service.go
+++ b/server/domain/service/endpoint_service.go
@@ -588,16 +588,22 @@ func (s *EndpointService) ensureEndpointExist(infraUUID string, namespace string
if err != nil {
return "", err
}
+
+ endpointName := fmt.Sprintf("kubefate-%s", u.Hostname())
+ if namespace != "" {
+ endpointName += "-" + namespace
+ }
+ endpointName = toDeploymentName(endpointName)
yaml := ""
if install {
- yaml, err = s.GetDeploymentYAML(namespace, "admin", "admin", "kubefate.net", registryConfig)
+ yaml, err = s.GetDeploymentYAML(namespace, "admin", "admin", endpointName+".kubefate.net", registryConfig)
if err != nil {
return "", err
}
}
endpointUUID, err := s.CreateKubeFATEEndpoint(infraUUID, namespace,
- fmt.Sprintf("kubefate-%s", u.Hostname()),
- fmt.Sprintf("Automatically added KubeFATE on Kubernetes %s.", u.Hostname()),
+ endpointName,
+ fmt.Sprintf("Automatically added KubeFATE on Infra %s.", provider.Name),
yaml,
install,
entity.EndpointKubeFATEIngressControllerServiceModeModeNonexistent)
diff --git a/server/domain/service/participant_fate_service.go b/server/domain/service/participant_fate_service.go
index 3a32bb71..887f11e3 100644
--- a/server/domain/service/participant_fate_service.go
+++ b/server/domain/service/participant_fate_service.go
@@ -99,6 +99,7 @@ type ParticipantFATEClusterYAMLCreationRequest struct {
PartyID int `json:"party_id"`
EnablePersistence bool `json:"enable_persistence"`
StorageClass string `json:"storage_class"`
+ FATEFlowGPUNum int `json:"fateflow_gpu_num"`
ExternalSpark ExternalSpark
ExternalHDFS ExternalHDFS
ExternalPulsar ExternalPulsar
@@ -256,6 +257,8 @@ func (s *ParticipantFATEService) GetClusterDeploymentYAML(req *ParticipantFATECl
SitePortalTLSCommonName string
EnablePersistence bool
StorageClass string
+ FATEFlowGPUEnabled bool
+ FATEFlowGPUNum int
EnablePSP bool
EnableExternalSpark bool
ExternalSparkCoresPerNode int
@@ -288,6 +291,8 @@ func (s *ParticipantFATEService) GetClusterDeploymentYAML(req *ParticipantFATECl
SitePortalTLSCommonName: fmt.Sprintf("site-%d.server.%s", req.PartyID, federation.Domain),
EnablePersistence: req.EnablePersistence,
StorageClass: req.StorageClass,
+ FATEFlowGPUEnabled: req.FATEFlowGPUNum > 0,
+ FATEFlowGPUNum: req.FATEFlowGPUNum,
EnablePSP: req.EnablePSP,
EnableExternalSpark: req.ExternalSpark.Enable,
ExternalSparkCoresPerNode: req.ExternalSpark.Cores_per_node,
diff --git a/server/domain/service/participant_fate_service_test.go b/server/domain/service/participant_fate_service_test.go
index 9a9802f5..0b583a7e 100644
--- a/server/domain/service/participant_fate_service_test.go
+++ b/server/domain/service/participant_fate_service_test.go
@@ -70,7 +70,7 @@ func TestCreateExchange_PosWithNewCert(t *testing.T) {
exchange, wg, err := service.CreateExchange(&ParticipantFATEExchangeCreationRequest{
ParticipantFATEExchangeYAMLCreationRequest: ParticipantFATEExchangeYAMLCreationRequest{
- ChartUUID: "242bf84c-548c-43d4-9f34-15f6d4dc0f33", // from the chart test repo
+ ChartUUID: "fd30a219-c9d2-4f6a-9146-f06c05a666f2", // from the chart test repo
Name: "test-exchange",
Namespace: "test-ns",
ServiceType: entity.ParticipantDefaultServiceTypeLoadBalancer,
@@ -79,10 +79,10 @@ func TestCreateExchange_PosWithNewCert(t *testing.T) {
Description: "",
EndpointUUID: "",
DeploymentYAML: `chartName: fate-exchange
-chartVersion: v1.9.1-fedlcm-v0.2.0
+chartVersion: v1.10.0-fedlcm-v0.3.0
fmlManagerServer:
image: federatedai/fml-manager-server
- imageTag: v0.2.0
+ imageTag: v0.3.0
type: NodePort
modules:
- trafficServer
@@ -226,7 +226,7 @@ func TestParticipantFATEService_GetClusterDeploymentYAML(t *testing.T) {
args: args{
req: &ParticipantFATEClusterYAMLCreationRequest{
ParticipantFATEExchangeYAMLCreationRequest: ParticipantFATEExchangeYAMLCreationRequest{
- ChartUUID: "7a51112a-b0ad-4c26-b2c0-1e6f7eca6073", // from the chart test repo
+ ChartUUID: "d81d2b48-930d-4c5e-b522-322b93e8ef39", // from the chart test repo
Name: "test-fate",
Namespace: "test-fate-ns",
ServiceType: entity.ParticipantDefaultServiceTypeNodePort,
@@ -235,6 +235,7 @@ func TestParticipantFATEService_GetClusterDeploymentYAML(t *testing.T) {
PartyID: 8888,
EnablePersistence: false,
StorageClass: "",
+ FATEFlowGPUNum: 0,
ExternalSpark: ExternalSpark{
Enable: true,
Cores_per_node: 8,
@@ -261,114 +262,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,
},
{
@@ -413,7 +306,7 @@ client:
args: args{
req: &ParticipantFATEClusterYAMLCreationRequest{
ParticipantFATEExchangeYAMLCreationRequest: ParticipantFATEExchangeYAMLCreationRequest{
- ChartUUID: "8d1b15c1-cc7e-460b-8563-fa732457a049", // from the chart test repo
+ ChartUUID: "73acbbc0-4cdf-46bf-b48f-25fe1e03b91f", // from the chart test repo
Name: "test-fate",
Namespace: "test-fate-ns",
ServiceType: entity.ParticipantDefaultServiceTypeNodePort,
@@ -422,261 +315,12 @@ client:
PartyID: 7777,
EnablePersistence: false,
StorageClass: "",
+ FATEFlowGPUNum: 0,
ExternalSpark: ExternalSpark{},
ExternalHDFS: ExternalHDFS{},
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 +330,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)
- }
})
}
}
@@ -714,7 +355,7 @@ func Test_getPulsarInformationFromYAML(t *testing.T) {
args: args{
yamlStr: `algorithm: Basic
chartName: fate
-chartVersion: v1.9.0
+chartVersion: v1.10.0
client:
accessMode: ReadWriteOnce
existingClaim: ""
diff --git a/server/domain/service/participant_fate_upgrade_service.go b/server/domain/service/participant_fate_upgrade_service.go
new file mode 100644
index 00000000..b157659b
--- /dev/null
+++ b/server/domain/service/participant_fate_upgrade_service.go
@@ -0,0 +1,344 @@
+// Copyright 2022 VMware, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package service
+
+import (
+ "sync"
+
+ "github.com/FederatedAI/FedLCM/server/domain/entity"
+ "github.com/FederatedAI/FedLCM/server/domain/utils"
+ "github.com/FederatedAI/KubeFATE/k8s-deploy/pkg/modules"
+ "github.com/pkg/errors"
+ "github.com/rs/zerolog"
+ "github.com/rs/zerolog/log"
+ "sigs.k8s.io/yaml"
+)
+
+// ParticipantFATEExchangeUpgradeRequest is the exchange upgrade request
+type ParticipantFATEExchangeUpgradeRequest struct {
+ ExchangeUUID string `json:"exchange_uuid"`
+ FederationUUID string `json:"federation_uuid"`
+ UpgradeVersion string `json:"upgrade_version"`
+}
+
+// ParticipantFATEClusterUpgradeRequest is the cluster upgrade request
+type ParticipantFATEClusterUpgradeRequest struct {
+ ClusterUUID string `json:"cluster_uuid"`
+ FederationUUID string `json:"federation_uuid"`
+ UpgradeVersion string `json:"upgrade_version"`
+}
+
+// UpgradeExchange upgrade the FATE exchange, the returned *sync.WaitGroup can be used to wait for the completion of the async goroutine
+func (s *ParticipantFATEService) UpgradeExchange(req *ParticipantFATEExchangeUpgradeRequest) (*entity.ParticipantFATE, *sync.WaitGroup, error) {
+
+ participantInstance, err := s.ParticipantFATERepo.GetByUUID(req.ExchangeUUID)
+ if err != nil {
+ return nil, nil, err
+ }
+ exchange := participantInstance.(*entity.ParticipantFATE)
+
+ //Check whether it is a cluster managed by fedlcm, a cluster not managed by fedlcm cannot be upgraded
+ if !exchange.IsManaged {
+ return nil, nil, errors.New("The cluster not managed by FedLCM cannot be upgraded.")
+ }
+
+ if err := s.EndpointService.TestKubeFATE(exchange.EndpointUUID); err != nil {
+ return nil, nil, err
+ }
+
+ ClusterChartVersion := utils.GetChartVersionFromDeploymentYAML(exchange.DeploymentYAML)
+ ClusterChartName := utils.GetChartNameFromDeploymentYAML(exchange.DeploymentYAML)
+
+ // checkUpgradeable
+ if utils.CompareVersion(ClusterChartVersion, req.UpgradeVersion) >= 0 {
+ return nil, nil, errors.Errorf("the version passed in cannot be upgraded, currentVersion %s, upgradeVersion: %s", ClusterChartVersion, req.UpgradeVersion)
+ }
+
+ instance, err := s.ChartRepo.GetByNameAndVersion(ClusterChartName, req.UpgradeVersion)
+ if err != nil {
+ log.Err(err).Strs("chartName and version", []string{ClusterChartName, req.UpgradeVersion}).Msg("GetByNameAndVersion err")
+ return nil, nil, errors.Wrapf(err, "faile to get chart")
+ }
+ upgradeChart := instance.(*entity.Chart)
+ if upgradeChart.Type != entity.ChartTypeFATEExchange {
+ return nil, nil, errors.Errorf("chart %s is not for FATE exchange deployment", upgradeChart.UUID)
+ }
+
+ var m map[string]interface{}
+ err = yaml.Unmarshal([]byte(exchange.DeploymentYAML), &m)
+ if err != nil {
+ return nil, nil, errors.Wrapf(err, "failed to unmarshal deployment yaml")
+ }
+
+ m["chartVersion"] = upgradeChart.Version
+
+ finalYAMLBytes, err := yaml.Marshal(m)
+ if err != nil {
+ return nil, nil, errors.Wrapf(err, "failed to get final yaml content")
+ }
+ previousDeploymentYAML := exchange.DeploymentYAML
+ exchange.DeploymentYAML = string(finalYAMLBytes)
+ log.Debug().Str("exchange.DeploymentYAML", exchange.DeploymentYAML).Msg("show DeploymentYAML")
+
+ exchange.Status = entity.ParticipantFATEStatusUpgrading
+
+ err = s.ParticipantFATERepo.UpdateInfoByUUID(exchange)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ _ = s.EventService.CreateEvent(entity.EventTypeLogMessage, entity.EntityTypeExchange, exchange.UUID, "start upgrading exchange", entity.EventLogLevelInfo)
+
+ wg := &sync.WaitGroup{}
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ operationLog := log.Logger.With().Timestamp().Str("action", "upgrading fate exchange").Str("uuid", exchange.UUID).Logger().
+ Hook(zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, message string) {
+ eventLvl := entity.EventLogLevelInfo
+ if level == zerolog.ErrorLevel {
+ eventLvl = entity.EventLogLevelError
+ }
+ _ = s.EventService.CreateEvent(entity.EventTypeLogMessage, entity.EntityTypeExchange, exchange.UUID, message, eventLvl)
+ }))
+ operationLog.Info().Msgf("upgrading FATE exchange %s with UUID %s", exchange.Name, exchange.UUID)
+ if err := func() error {
+ _, kfClient, kfClientCloser, err := s.buildKubeFATEMgrAndClient(exchange.EndpointUUID)
+ if kfClientCloser != nil {
+ defer kfClientCloser()
+ }
+ if err != nil {
+ return err
+ }
+
+ if upgradeChart.Private {
+ operationLog.Info().Msgf("making sure the chart is uploaded, name: %s, version: %s", upgradeChart.ChartName, upgradeChart.Version)
+ if err := kfClient.EnsureChartExist(upgradeChart.ChartName, upgradeChart.Version, upgradeChart.ArchiveContent); err != nil {
+ return errors.Wrapf(err, "error uploading FedLCM private chart")
+ }
+ }
+
+ jobUUID, err := kfClient.SubmitClusterUpdateJob(exchange.DeploymentYAML)
+ if err != nil {
+ return errors.Wrapf(err, "fail to submit cluster upgrade request")
+ }
+ exchange.JobUUID = jobUUID
+ if err := s.ParticipantFATERepo.UpdateInfoByUUID(exchange); err != nil {
+ return errors.Wrap(err, "failed to update exchange's job uuid")
+ }
+ operationLog.Info().Msgf("kubefate job created, uuid: %s", exchange.JobUUID)
+ clusterUUID, err := kfClient.WaitClusterUUID(jobUUID)
+ if err != nil {
+ return errors.Wrapf(err, "fail to get cluster uuid")
+ }
+ if err := s.ParticipantFATERepo.UpdateInfoByUUID(exchange); err != nil {
+ return errors.Wrap(err, "failed to update exchange cluster uuid")
+ }
+ operationLog.Info().Msgf("kubefate-managed cluster upgraded, uuid: %s", exchange.ClusterUUID)
+
+ job, err := kfClient.WaitJob(jobUUID)
+ if err != nil {
+ return err
+ }
+ if job.Status != modules.JobStatusSuccess {
+ return errors.Errorf("job is %s, job info: %v", job.Status.String(), job)
+ }
+ exchange.ClusterUUID = clusterUUID
+ exchange.ChartUUID = upgradeChart.UUID
+ operationLog.Info().Msgf("kubefate job succeeded")
+
+ exchange.Status = entity.ParticipantFATEStatusActive
+ if err := s.BuildIngressInfoMap(exchange); err != nil {
+ return errors.Wrapf(err, "failed to get ingress info")
+ }
+ return s.ParticipantFATERepo.UpdateInfoByUUID(exchange)
+ }(); err != nil {
+ operationLog.Error().Msgf(errors.Wrapf(err, "failed to upgrade FATE exchange").Error())
+ // we still mark the exchange to be active as kubefate can roll back the failed upgrade
+ exchange.Status = entity.ParticipantFATEStatusActive
+ exchange.DeploymentYAML = previousDeploymentYAML
+ if updateErr := s.ParticipantFATERepo.UpdateInfoByUUID(exchange); updateErr != nil {
+ operationLog.Error().Msgf(errors.Wrapf(updateErr, "failed to update FATE exchange info").Error())
+ }
+ return
+ }
+ operationLog.Info().Msgf("FATE exchange %s(%s) upgraded", exchange.Name, exchange.UUID)
+ }()
+
+ return exchange, wg, nil
+}
+
+// UpgradeCluster upgrade a FATE cluster with exchange's access info, and will update exchange's route table
+func (s *ParticipantFATEService) UpgradeCluster(req *ParticipantFATEClusterUpgradeRequest) (*entity.ParticipantFATE, *sync.WaitGroup, error) {
+
+ instance, err := s.ParticipantFATERepo.GetExchangeByFederationUUID(req.FederationUUID)
+ if err != nil {
+ return nil, nil, errors.Wrapf(err, "failed to check exchange existence status")
+ }
+ exchange := instance.(*entity.ParticipantFATE)
+
+ if exchange.Status != entity.ParticipantFATEStatusActive {
+ return nil, nil, errors.Errorf("exchange %v is not in active status", exchange.UUID)
+ }
+ if exchange.IsManaged {
+ if err := s.EndpointService.TestKubeFATE(exchange.EndpointUUID); err != nil {
+ return nil, nil, err
+ }
+ }
+
+ participantInstance, err := s.ParticipantFATERepo.GetByUUID(req.ClusterUUID)
+ if err != nil {
+ return nil, nil, err
+ }
+ cluster := participantInstance.(*entity.ParticipantFATE)
+
+ //Check whether it is a cluster managed by fedlcm, a cluster not managed by fedlcm cannot be upgraded
+ if !exchange.IsManaged {
+ return nil, nil, errors.New("The cluster not managed by FedLCM cannot be upgraded.")
+ }
+
+ if err := s.EndpointService.TestKubeFATE(cluster.EndpointUUID); err != nil {
+ return nil, nil, err
+ }
+
+ ClusterChartVersion := utils.GetChartVersionFromDeploymentYAML(cluster.DeploymentYAML)
+ ClusterChartName := utils.GetChartNameFromDeploymentYAML(cluster.DeploymentYAML)
+
+ // checkUpgradeable
+ if utils.CompareVersion(ClusterChartVersion, req.UpgradeVersion) >= 0 {
+ return nil, nil, errors.Errorf("the version passed in cannot be upgraded, currentVersion %s, upgradeVersion: %s", ClusterChartVersion, req.UpgradeVersion)
+ }
+
+ instance, err = s.ChartRepo.GetByNameAndVersion(ClusterChartName, req.UpgradeVersion)
+ if err != nil {
+ log.Err(err).Strs("chartName and version", []string{ClusterChartName, req.UpgradeVersion}).Msg("GetByNameAndVersion err")
+ return nil, nil, errors.Wrapf(err, "faile to get chart")
+ }
+ upgradeChart := instance.(*entity.Chart)
+ if upgradeChart.Type != entity.ChartTypeFATECluster {
+ return nil, nil, errors.Errorf("chart %s is not for FATE deployment", upgradeChart.UUID)
+ }
+
+ var m map[string]interface{}
+ err = yaml.Unmarshal([]byte(cluster.DeploymentYAML), &m)
+ if err != nil {
+ return nil, nil, errors.Wrapf(err, "failed to unmarshal deployment yaml")
+ }
+
+ m["chartVersion"] = upgradeChart.Version
+
+ finalYAMLBytes, err := yaml.Marshal(m)
+ if err != nil {
+ return nil, nil, errors.Wrapf(err, "failed to get final yaml content")
+ }
+ previousDeploymentYAML := cluster.DeploymentYAML
+ cluster.DeploymentYAML = string(finalYAMLBytes)
+ log.Debug().Str("cluster.DeploymentYAML", cluster.DeploymentYAML).Msg("show DeploymentYAML")
+
+ cluster.Status = entity.ParticipantFATEStatusUpgrading
+
+ err = s.ParticipantFATERepo.UpdateInfoByUUID(cluster)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ _ = s.EventService.CreateEvent(entity.EventTypeLogMessage, entity.EntityTypeCluster, cluster.UUID, "start upgrading cluster", entity.EventLogLevelInfo)
+
+ wg := &sync.WaitGroup{}
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ operationLog := log.Logger.With().Timestamp().Str("action", "upgrading fate cluster").Str("uuid", cluster.UUID).Logger().
+ Hook(zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, message string) {
+ eventLvl := entity.EventLogLevelInfo
+ if level == zerolog.ErrorLevel {
+ eventLvl = entity.EventLogLevelError
+ }
+ _ = s.EventService.CreateEvent(entity.EventTypeLogMessage, entity.EntityTypeCluster, cluster.UUID, message, eventLvl)
+ }))
+ operationLog.Info().Msgf("upgrading FATE cluster %s with UUID %s", cluster.Name, cluster.UUID)
+ if err := func() error {
+ _, kfClient, closer, err := s.buildKubeFATEMgrAndClient(cluster.EndpointUUID)
+ if closer != nil {
+ defer closer()
+ }
+ if err != nil {
+ return err
+ }
+ if upgradeChart.Private {
+ operationLog.Info().Msgf("making sure the chart is uploaded, name: %s, version: %s", upgradeChart.ChartName, upgradeChart.Version)
+ if err := kfClient.EnsureChartExist(upgradeChart.ChartName, upgradeChart.Version, upgradeChart.ArchiveContent); err != nil {
+ return errors.Wrapf(err, "error uploading FedLCM private chart")
+ }
+ }
+
+ jobUUID, err := kfClient.SubmitClusterUpdateJob(cluster.DeploymentYAML)
+ if err != nil {
+ return errors.Wrapf(err, "fail to submit cluster upgrade request")
+ }
+ cluster.JobUUID = jobUUID
+ if err := s.ParticipantFATERepo.UpdateInfoByUUID(cluster); err != nil {
+ return errors.Wrap(err, "failed to update cluster's job uuid")
+ }
+ operationLog.Info().Msgf("kubefate job created, uuid: %s", cluster.JobUUID)
+ clusterUUID, err := kfClient.WaitClusterUUID(jobUUID)
+ if err != nil {
+ return errors.Wrapf(err, "fail to get cluster uuid")
+ }
+ if err := s.ParticipantFATERepo.UpdateInfoByUUID(cluster); err != nil {
+ return errors.Wrap(err, "failed to update cluster uuid")
+ }
+ operationLog.Info().Msgf("kubefate-managed cluster created, uuid: %s", cluster.ClusterUUID)
+
+ job, err := kfClient.WaitJob(jobUUID)
+ if err != nil {
+ return err
+ }
+ if job.Status != modules.JobStatusSuccess {
+ return errors.Errorf("job is %s, job info: %v", job.Status.String(), job)
+ }
+
+ cluster.ClusterUUID = clusterUUID
+ cluster.ChartUUID = upgradeChart.UUID
+ operationLog.Info().Msgf("kubefate job succeeded")
+
+ cluster.Status = entity.ParticipantFATEStatusActive
+ if err := s.BuildIngressInfoMap(cluster); err != nil {
+ return errors.Wrapf(err, "failed to get ingress info")
+ }
+ if err := s.ParticipantFATERepo.UpdateInfoByUUID(cluster); err != nil {
+ return errors.Wrap(err, "failed to save cluster info")
+ }
+ if exchange.IsManaged {
+ operationLog.Info().Msg("rebuilding exchange route table")
+ if err := s.rebuildRouteTable(exchange); err != nil {
+ operationLog.Error().Msg(errors.Wrap(err, "error rebuilding route table while upgrade cluster").Error())
+ }
+ }
+ return nil
+ }(); err != nil {
+ operationLog.Error().Msgf(errors.Wrap(err, "failed to upgrade FATE cluster").Error())
+ // we still mark the cluster to be active as kubefate can roll back the failed upgrade
+ cluster.Status = entity.ParticipantFATEStatusActive
+ cluster.DeploymentYAML = previousDeploymentYAML
+ if updateErr := s.ParticipantFATERepo.UpdateInfoByUUID(cluster); updateErr != nil {
+ operationLog.Error().Msgf(errors.Wrap(err, "failed to update FATE cluster info").Error())
+ }
+ return
+ }
+ operationLog.Info().Msgf("FATE cluster %s(%s) upgraded", cluster.Name, cluster.UUID)
+ }()
+ return cluster, wg, nil
+}
diff --git a/server/domain/service/participant_openfl_service.go b/server/domain/service/participant_openfl_service.go
index 52cc8f1b..a3d63a0b 100644
--- a/server/domain/service/participant_openfl_service.go
+++ b/server/domain/service/participant_openfl_service.go
@@ -1,4 +1,4 @@
-// Copyright 2022 VMware, Inc.
+// Copyright 2022-2023 VMware, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -23,7 +23,6 @@ import (
"encoding/hex"
"encoding/pem"
"fmt"
- "github.com/rs/zerolog/log"
"math/rand"
"net/url"
"strings"
@@ -39,7 +38,9 @@ import (
"github.com/Masterminds/sprig/v3"
"github.com/pkg/errors"
"github.com/rs/zerolog"
+ "github.com/rs/zerolog/log"
uuid "github.com/satori/go.uuid"
+ "github.com/spf13/viper"
corev1 "k8s.io/api/core/v1"
apierr "k8s.io/apimachinery/pkg/api/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -96,6 +97,7 @@ type ParticipantOpenFLEnvoyRegistrationRequest struct {
SkipCommonPythonFiles bool `json:"skip_common_python_files"`
RegistryConfig valueobject.KubeRegistryConfig `json:"registry_config"`
EnablePSP bool `json:"enable_psp"`
+ LessPrivileged bool `json:"less_privileged"`
// internal
federation *entity.FederationOpenFL
@@ -626,6 +628,10 @@ func (s *ParticipantOpenFLService) HandleRegistrationRequest(req *ParticipantOpe
return nil, errors.Errorf("chart %s is not for OpenFL envoy deployment", req.chart.UUID)
}
+ if req.Namespace == "" {
+ req.Namespace = fmt.Sprintf("%s-envoy", toDeploymentName(req.federation.Name))
+ }
+
infraProvider, err := s.configEnvoyInfra(req)
if err != nil {
return nil, errors.Wrapf(err, "failed to prepare the envoy infra provider")
@@ -645,16 +651,16 @@ func (s *ParticipantOpenFLService) HandleRegistrationRequest(req *ParticipantOpe
}
//TODO: check if same name envoy exists in the same infra
- if req.Namespace == "" {
- req.Namespace = fmt.Sprintf("%s-envoy", toDeploymentName(req.federation.Name))
- }
- K8sClient, err := kubernetes.NewKubernetesClient("", infraProvider.Config.KubeConfigContent, infraProvider.Config.IsInCluster)
- if err != nil {
- return nil, err
- }
- _, err = K8sClient.GetClientSet().CoreV1().Namespaces().Get(context.TODO(), req.Namespace, v1.GetOptions{})
- if err == nil {
- return nil, errors.Errorf("namespace %s exists. cannot override", req.Namespace)
+ // When using less privileged permission, the namespace should be created before-hard
+ if !req.LessPrivileged {
+ K8sClient, err := kubernetes.NewKubernetesClient("", infraProvider.Config.KubeConfigContent, infraProvider.Config.IsInCluster)
+ if err != nil {
+ return nil, err
+ }
+ _, err = K8sClient.GetClientSet().CoreV1().Namespaces().Get(context.TODO(), req.Namespace, v1.GetOptions{})
+ if err == nil {
+ return nil, errors.Errorf("namespace %s exists. cannot override", req.Namespace)
+ }
}
deploymentYAML, err := s.GetOpenFLEnvoyYAML(req)
@@ -716,8 +722,11 @@ func (s *ParticipantOpenFLService) HandleRegistrationRequest(req *ParticipantOpe
req.operationLog = &operationLog
operationLog.Info().Msgf("creating envoy %s with UUID %s", req.Name, envoy.UUID)
if err := func() (err error) {
- // TODO: check the namespace passed here
- endpointUUID, err := s.EndpointService.ensureEndpointExist(infraProvider.UUID, "", req.RegistryConfig)
+ kfNamespace := ""
+ if req.LessPrivileged {
+ kfNamespace = req.Namespace
+ }
+ endpointUUID, err := s.EndpointService.ensureEndpointExist(infraProvider.UUID, kfNamespace, req.RegistryConfig)
if err != nil {
return err
}
@@ -802,6 +811,11 @@ func (s *ParticipantOpenFLService) GetOpenFLEnvoyYAML(req *ParticipantOpenFLEnvo
data.EnvoyConfig = federation.ShardDescriptorConfig.EnvoyConfigYaml
}
+ if registryOverride := viper.GetString("lifecyclemanager.openfl.envoy.registry.override"); registryOverride != "" {
+ data.UseRegistry = true
+ data.Registry = registryOverride
+ }
+
t, err := template.New("openfl-envoy").Funcs(sprig.TxtFuncMap()).Parse(chart.InitialYamlTemplate)
if err != nil {
return "", err
@@ -824,7 +838,7 @@ func (s *ParticipantOpenFLService) RemoveEnvoy(uuid string, force bool) error {
}
if !force && envoy.Status != entity.ParticipantOpenFLStatusActive {
- return errors.Errorf("director cannot be removed when in status: %v", envoy.Status)
+ return errors.Errorf("envoy cannot be removed when in status: %v", envoy.Status)
}
envoy.Status = entity.ParticipantOpenFLStatusRemoving
@@ -937,6 +951,9 @@ func (s *ParticipantOpenFLService) configEnvoyInfra(req *ParticipantOpenFLEnvoyR
kubeconfig := valueobject.KubeConfig{
KubeConfigContent: req.KubeConfig,
}
+ if req.LessPrivileged {
+ kubeconfig.NamespacesList = []string{req.Namespace}
+ }
if err := kubeconfig.Validate(); err != nil {
return nil, err
}
@@ -960,6 +977,9 @@ func (s *ParticipantOpenFLService) configEnvoyInfra(req *ParticipantOpenFLEnvoyR
Config: kubeconfig,
Repo: s.InfraRepo,
}
+ if req.LessPrivileged {
+ infraProvider.Name = infraProvider.Name + "-" + req.Namespace
+ }
if err := s.InfraRepo.ProviderExists(infraProvider); err == nil {
log.Info().Msgf("creating infra provider during envoy registration, name: %s", infraProvider.Name)
diff --git a/server/domain/utils/upgrade.go b/server/domain/utils/upgrade.go
new file mode 100644
index 00000000..7b793acf
--- /dev/null
+++ b/server/domain/utils/upgrade.go
@@ -0,0 +1,87 @@
+package utils
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/pkg/errors"
+ "github.com/rs/zerolog/log"
+ "gopkg.in/yaml.v2"
+)
+
+// Upgradeable If the versionlist contains a version number higher than version, then return true
+func Upgradeable(version string, versionlist []string) bool {
+ upgradeablelist := Upgradeablelist(version, versionlist)
+ return len(upgradeablelist) > 0
+}
+
+// Upgradeablelist If the versionlist contains a version number higher than version, then return the upgradeable list
+func Upgradeablelist(version string, versionlist []string) []string {
+ upgradeablelist := make([]string, 0)
+
+ for _, item := range versionlist {
+ if CompareVersion(item, version) > 0 && typeVersion(item, version) {
+ upgradeablelist = append(upgradeablelist, item)
+ }
+ }
+ return upgradeablelist
+}
+
+// CompareVersion compare version1 and version2
+// If version1 is larger, return 1
+// If version2 is larger, return -1
+// otherwise return 0
+func CompareVersion(version1, version2 string) int {
+ n, m := len(version1), len(version2)
+ i, j := 0, 0
+ for i < n || j < m {
+ x := 0
+ for ; i < n && version1[i] != '.'; i++ {
+ x = x*10 + int(version1[i]-'0')
+ }
+ i++
+ y := 0
+ for ; j < m && version2[j] != '.'; j++ {
+ y = y*10 + int(version2[j]-'0')
+ }
+ j++
+ if x > y {
+ return 1
+ }
+ if x < y {
+ return -1
+ }
+ }
+ return 0
+}
+
+// typeVersion Determine whether version1 and version2 are of the same type.
+// Examples:
+// - v1.10.0 & v1.9.1 is true
+// - v1.10.0-fedlcm-v0.3.0 & v1.9.1-fedlcm-v0.2.0 is true
+// - v1.10.0-fedlcm-v0.3.0 & v1.10.0 is false
+func typeVersion(version1, version2 string) bool {
+ return len(strings.Split(version1, "-fedlcm-")) == len(strings.Split(version2, "-fedlcm-"))
+}
+
+func GetChartVersionFromDeploymentYAML(deploymentYAML string) string {
+ var m map[string]interface{}
+ err := yaml.Unmarshal([]byte(deploymentYAML), &m)
+ if err != nil {
+ log.Warn().AnErr("UnmarshalError", errors.Wrapf(err, "failed to unmarshal deployment yaml")).Msg("GetChartVersionFromDeploymentYAML")
+ return ""
+ }
+
+ return fmt.Sprint(m["chartVersion"])
+}
+
+func GetChartNameFromDeploymentYAML(deploymentYAML string) string {
+ var m map[string]interface{}
+ err := yaml.Unmarshal([]byte(deploymentYAML), &m)
+ if err != nil {
+ log.Warn().AnErr("UnmarshalError", errors.Wrapf(err, "failed to unmarshal deployment yaml")).Msg("GetChartVersionFromDeploymentYAML")
+ return ""
+ }
+
+ return fmt.Sprint(m["chartName"])
+}
diff --git a/server/domain/utils/upgrade_test.go b/server/domain/utils/upgrade_test.go
new file mode 100644
index 00000000..0f0ce4ea
--- /dev/null
+++ b/server/domain/utils/upgrade_test.go
@@ -0,0 +1,83 @@
+package utils
+
+import (
+ "reflect"
+ "testing"
+)
+
+func TestUpgradeablelist(t *testing.T) {
+ type args struct {
+ version string
+ versionlist []string
+ }
+ tests := []struct {
+ name string
+ args args
+ want []string
+ }{
+ {
+ name: "mix",
+ args: args{
+ version: "v1.10.0",
+ versionlist: []string{"v1.10.0", "v1.9.1", "v1.9.0"},
+ },
+ want: make([]string, 0),
+ },
+ {
+ name: "min",
+ args: args{
+ version: "v1.9.0",
+ versionlist: []string{"v1.10.0", "v1.9.1", "v1.9.0"},
+ },
+ want: []string{"v1.10.0", "v1.9.1"},
+ },
+ {
+ name: "middle",
+ args: args{
+ version: "v1.9.1",
+ versionlist: []string{"v1.10.0", "v1.9.1", "v1.9.0"},
+ },
+ want: []string{"v1.10.0"},
+ },
+ {
+ name: "two type min",
+ args: args{
+ version: "v1.9.1",
+ versionlist: []string{"v1.10.0", "v1.10.0-fedlcm-v0.3.0", "v1.9.1", "v1.9.1-fedlcm-v0.2.0"},
+ },
+ want: []string{"v1.10.0"},
+ },
+ {
+ name: "two type max",
+ args: args{
+ version: "v1.10.0",
+ versionlist: []string{"v1.10.0", "v1.10.0-fedlcm-v0.3.0", "v1.9.1", "v1.9.1-fedlcm-v0.2.0"},
+ },
+ want: []string{},
+ },
+ {
+ name: "two type max 1",
+ args: args{
+ version: "v1.10.0-fedlcm-v0.3.0",
+ versionlist: []string{"v1.10.0", "v1.10.0-fedlcm-v0.3.0", "v1.9.1", "v1.9.1-fedlcm-v0.2.0"},
+ },
+ want: []string{},
+ },
+ {
+ name: "two type min 1",
+ args: args{
+ version: "v1.9.1-fedlcm-v0.2.0",
+ versionlist: []string{"v1.10.0", "v1.10.0-fedlcm-v0.3.0", "v1.9.1", "v1.9.1-fedlcm-v0.2.0"},
+ },
+ want: []string{"v1.10.0-fedlcm-v0.3.0"},
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := Upgradeablelist(tt.args.version, tt.args.versionlist)
+ if !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("Upgradeablelist() = %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 5e45d4c9..93fb6a6a 100644
--- a/server/infrastructure/gorm/chart_mock_repo.go
+++ b/server/infrastructure/gorm/chart_mock_repo.go
@@ -1,4 +1,4 @@
-// Copyright 2022 VMware, Inc.
+// Copyright 2022-2023 VMware, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -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
}
@@ -62,6 +64,15 @@ func (r *ChartMockRepo) GetByUUID(uuid string) (interface{}, error) {
}
}
+func (r *ChartMockRepo) GetByNameAndVersion(chartName, version string) (interface{}, error) {
+ for _, chart := range chartMap {
+ if chart.ChartName == chartName && chart.Version == version {
+ return &chart, nil
+ }
+ }
+ return nil, errors.New("chart not found")
+}
+
func (r *ChartMockRepo) ListByType(instance interface{}) (interface{}, error) {
t := instance.(entity.ChartType)
var chartList []entity.Chart
@@ -75,33 +86,32 @@ func (r *ChartMockRepo) ListByType(instance interface{}) (interface{}, error) {
var (
chartMap = map[string]entity.Chart{
- "4ad46829-a827-4632-b169-c8675360321e": {
+ "19c7c751-0de0-4780-95d6-e152440ab287": {
Model: gorm.Model{
ID: 1,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
- UUID: "4ad46829-a827-4632-b169-c8675360321e",
- Name: "chart for FATE exchange v1.8.0",
- Description: "This chart is for deploying FATE exchange v1.8.0",
+ UUID: "19c7c751-0de0-4780-95d6-e152440ab287",
+ Name: "chart for FATE exchange v1.11.1",
+ Description: "This chart is for deploying FATE exchange v1.11.1",
Type: entity.ChartTypeFATEExchange,
ChartName: "fate-exchange",
- Version: "v1.8.0",
- AppVersion: "v1.8.0",
+ Version: "v1.11.1",
+ AppVersion: "v1.11.1",
Chart: `apiVersion: v1
-appVersion: v1.8.0
+appVersion: v1.11.1
description: A Helm chart for fate exchange
name: fate-exchange
-version: v1.8.0`,
+version: v1.11.1`,
InitialYamlTemplate: `name: {{.Name}}
namespace: {{.Namespace}}
chartName: fate-exchange
-chartVersion: v1.8.0
+chartVersion: v1.11.1
partyId: 0
{{- if .UseRegistry}}
registry: {{.Registry}}
{{- end }}
-imageTag: "1.8.0-release"
# pullPolicy:
# persistence: false
podSecurityPolicy:
@@ -141,16 +151,16 @@ partyName: fate-exchange
image:
registry: federatedai
isThridParty:
- tag: 1.8.0-release
+ tag: 1.11.1-release
pullPolicy: IfNotPresent
imagePullSecrets:
# - name:
-
-partyId: 9999
-partyName: fate-9999
podSecurityPolicy:
enabled: false
+
+persistence:
+ enabled: false
partyList:
- partyId: 8888
@@ -166,7 +176,8 @@ modules:
ip: rollsite
type: ClusterIP
nodePort: 30001
- loadBalancerIP:
+ loadBalancerIP:
+ enableTLS: false
nodeSelector:
tolerations:
affinity:
@@ -228,7 +239,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 +258,9 @@ podSecurityPolicy:
enabled: {{ .enabled | default false }}
{{- end }}
+persistence:
+ enabled: {{ .persistence | default "false" }}
+
partyList:
{{- with .rollsite }}
{{- range .partyList }}
@@ -274,6 +287,7 @@ modules:
{{ toYaml . | indent 6 }}
{{- end }}
type: {{ .type }}
+ enableTLS: {{ .enableTLS | default false }}
nodePort: {{ .nodePort }}
partyList:
{{- range .partyList }}
@@ -335,24 +349,24 @@ modules:
ArchiveContent: nil,
Private: false,
},
- "7a51112a-b0ad-4c26-b2c0-1e6f7eca6073": {
+ "d81d2b48-930d-4c5e-b522-322b93e8ef39": {
Model: gorm.Model{
ID: 2,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
- UUID: "7a51112a-b0ad-4c26-b2c0-1e6f7eca6073",
- Name: "chart for FATE cluster v1.8.0",
- Description: "This is chart for installing FATE cluster v1.8.0",
+ UUID: "d81d2b48-930d-4c5e-b522-322b93e8ef39",
+ Name: "chart for FATE cluster v1.11.1",
+ Description: "This is chart for installing FATE cluster v1.11.1",
Type: entity.ChartTypeFATECluster,
ChartName: "fate",
- Version: "v1.8.0",
- AppVersion: "v1.8.0",
+ Version: "v1.11.1",
+ AppVersion: "v1.11.1",
Chart: `apiVersion: v1
-appVersion: v1.8.0
+appVersion: v1.11.1
description: A Helm chart for fate-training
name: fate
-version: v1.8.0
+version: v1.11.1
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 +375,11 @@ sources:
InitialYamlTemplate: `name: {{.Name}}
namespace: {{.Namespace}}
chartName: fate
-chartVersion: v1.8.0
-partyId: {{.PartyID}}
+chartVersion: v1.11.1
{{- if .UseRegistry}}
registry: {{.Registry}}
{{- end }}
-# imageTag: "1.8.0-release"
+partyId: {{.PartyID}}
persistence: {{ .EnablePersistence }}
# pullPolicy:
podSecurityPolicy:
@@ -375,8 +388,7 @@ podSecurityPolicy:
imagePullSecrets:
- name: {{.ImagePullSecretsName}}
{{- end }}
-
-# ingressClassName: nginx
+ingressClassName: nginx
modules:
- mysql
@@ -394,7 +406,19 @@ modules:
{{- end }}
- nginx
-backend: spark_pulsar
+computing: Spark
+federation: Pulsar
+storage: HDFS
+{{- if .FATEFlowGPUEnabled }}
+algorithm: NN
+device: GPU
+{{- else }}
+algorithm: Basic
+device: CPU
+{{- end }}
+
+skippedKeys:
+- route_table
ingress:
fateboard:
@@ -414,74 +438,43 @@ 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
+ {{- if .FATEFlowGPUEnabled }}
+ resources:
+ requests:
+ nvidia.com/gpu: {{.FATEFlowGPUNum}}
+ # cpu: "2"
+ # memory: "4Gi"
+ limits:
+ nvidia.com/gpu: {{.FATEFlowGPUNum}}
+ # cpu: "4"
+ # memory: "8Gi"
+ {{- else }}
# resources:
- # logLevel: INFO
+ # requests:
+ # cpu: "2"
+ # memory: "4Gi"
+ # limits:
+ # cpu: "4"
+ # memory: "8Gi"
+ {{- end }}
{{- if .EnableExternalSpark }}
spark:
cores_per_node: {{.ExternalSparkCoresPerNode}}
@@ -494,25 +487,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 +513,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.11.1-release"
+ replicas: 1
# resources:
+ # requests:
+ # cpu: "1"
+ # memory: "2Gi"
+ # limits:
+ # cpu: "1"
+ # memory: "2Gi"
# nodeSelector:
# tolerations:
# affinity:
# type: ClusterIP
- # nodePort: 30977
worker:
+ # image: "federatedai/spark-worker"
+ # imageTag: "1.11.1-release"
replicas: 2
# resources:
# requests:
@@ -585,11 +603,1689 @@ 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.11.1-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, GPU
+device: IPCL
+
+istio:
+ enabled: false
+
+podSecurityPolicy:
+ enabled: false
+
+ingressClassName: nginx
+
+ingress:
+ fateboard:
+ # annotations:
+ hosts:
+ - name: fateboard.example.com
+ path: /
+ tls: []
+ # - secretName: my-tls-secret
+ # hosts:
+ # - fateboard.example.com
+ client:
+ # annotations:
+ hosts:
+ - name: notebook.example.com
+ path: /
+ tls: []
+ spark:
+ # annotations:
+ hosts:
+ - name: spark.example.com
+ path: /
+ tls: []
+ rabbitmq:
+ # annotations:
+ hosts:
+ - name: rabbitmq.example.com
+ path: /
+ tls: []
+ pulsar:
+ # annotations:
+ hosts:
+ - name: pulsar.example.com
+ path: /
+ tls: []
+
+exchange:
+ partyIp: 192.168.1.1
+ partyPort: 30001
+
+exchangeList:
+- id: 9991
+ ip: 192.168.1.1
+ port: 30910
+
+partyList:
+- partyId: 8888
+ partyIp: 192.168.8.1
+ partyPort: 30081
+- partyId: 10000
+ partyIp: 192.168.10.1
+ partyPort: 30101
+
+persistence:
+ enabled: false
+
+modules:
+ rollsite:
+ include: true
+ ip: rollsite
+ type: ClusterIP
+ nodePort: 30091
+ loadBalancerIP:
+ 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
+ nodeSelector:
+ tolerations:
+ affinity:
+ subPath: "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
+ 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
+ nodeSelector:
+ tolerations:
+ affinity:
+
+ 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:
+ 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,
+ },
+ "9dff91c4-2566-4c24-a0dc-631fa8808378": {
+ Model: gorm.Model{
+ ID: 3,
+ CreatedAt: time.Now(),
+ UpdatedAt: time.Now(),
+ },
+ UUID: "9dff91c4-2566-4c24-a0dc-631fa8808378",
+ 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.10.0",
+ AppVersion: "v1.10.0",
+ Chart: `apiVersion: v1
+appVersion: v1.10.0
+description: A Helm chart for fate exchange
+name: fate-exchange
+version: v1.10.0`,
+ InitialYamlTemplate: `name: {{.Name}}
+namespace: {{.Namespace}}
+chartName: fate-exchange
+chartVersion: v1.10.0
+partyId: 0
+{{- if .UseRegistry}}
+registry: {{.Registry}}
+{{- end }}
+# pullPolicy:
+# persistence: false
+podSecurityPolicy:
+ enabled: {{.EnablePSP}}
+{{- if .UseImagePullSecrets}}
+imagePullSecrets:
+ - name: {{.ImagePullSecretsName}}
+{{- end }}
+modules:
+ - trafficServer
+ - nginx
+
+trafficServer:
+ type: {{.ServiceType}}
+ route_table:
+ sni:
+ # replicas: 1
+ # nodeSelector:
+ # tolerations:
+ # affinity:
+ # nodePort:
+ # loadBalancerIP:
+
+nginx:
+ type: {{.ServiceType}}
+ route_table:
+ # replicas: 1
+ # nodeSelector:
+ # tolerations:
+ # affinity:
+ # httpNodePort:
+ # grpcNodePort:
+ # loadBalancerIP: `,
+ Values: `partyId: 1
+partyName: fate-exchange
+
+image:
+ registry: federatedai
+ isThridParty:
+ tag: 1.10.0-release
+ pullPolicy: IfNotPresent
+ imagePullSecrets:
+# - name:
+
+partyId: 9999
+partyName: fate-9999
+
+podSecurityPolicy:
+ enabled: false
+
+partyList:
+- partyId: 8888
+ partyIp: 192.168.8.1
+ partyPort: 30081
+- partyId: 10000
+ partyIp: 192.168.10.1
+ partyPort: 30101
+
+modules:
+ rollsite:
+ include: false
+ ip: rollsite
+ type: ClusterIP
+ nodePort: 30001
+ loadBalancerIP:
+ 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`,
+ 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 }}
+
+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 }}`,
+ ArchiveContent: nil,
+ Private: false,
+ },
+ "7a1e3009-ce43-47b5-9ce9-a5256c399151": {
+ Model: gorm.Model{
+ ID: 4,
+ CreatedAt: time.Now(),
+ UpdatedAt: time.Now(),
+ },
+ UUID: "7a1e3009-ce43-47b5-9ce9-a5256c399151",
+ 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.10.0",
+ AppVersion: "v1.10.0",
+ Chart: `apiVersion: v1
+appVersion: v1.10.0
+description: A Helm chart for fate-training
+name: fate
+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:
+ - https://github.com/FederatedAI/KubeFATE
+ - https://github.com/FederatedAI/FATE`,
+ InitialYamlTemplate: `name: {{.Name}}
+namespace: {{.Namespace}}
+chartName: fate
+chartVersion: v1.10.0
+{{- if .UseRegistry}}
+registry: {{.Registry}}
+{{- end }}
+partyId: {{.PartyID}}
+persistence: {{ .EnablePersistence }}
+# pullPolicy:
+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
+
+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 }}
+
+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 }}`,
Values: `image:
registry: federatedai
isThridParty:
- tag: 1.8.0-release
+ tag: 1.10.0-release
pullPolicy: IfNotPresent
imagePullSecrets:
# - name:
@@ -597,6 +2293,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 +2373,8 @@ modules:
ip: rollsite
type: ClusterIP
nodePort: 30091
- loadBalancerIP:
+ loadBalancerIP:
+ enableTLS: false
nodeSelector:
tolerations:
affinity:
@@ -695,6 +2403,7 @@ modules:
affinity:
python:
include: true
+ replicas: 1
type: ClusterIP
httpNodePort: 30097
grpcNodePort: 30092
@@ -703,13 +2412,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 +2446,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,10 +2469,11 @@ modules:
tolerations:
affinity:
subPath: "client"
- existingClaim: ""
- storageClass: "nodemanager-0"
+ existingClaim:
+ storageClass:
accessMode: ReadWriteOnce
size: 1Gi
+ notebook_hashed_password:
clustermanager:
include: true
ip: clustermanager
@@ -763,40 +2483,20 @@ modules:
affinity:
nodemanager:
include: true
- list:
- - name: nodemanager-0
- nodeSelector:
- tolerations:
- affinity:
- sessionProcessorsPerNode: 2
- subPath: "nodemanager-0"
- existingClaim: ""
- storageClass: "nodemanager-0"
- accessMode: ReadWriteOnce
- size: 1Gi
- - name: nodemanager-1
- nodeSelector:
- tolerations:
- affinity:
- sessionProcessorsPerNode: 2
- subPath: "nodemanager-1"
- existingClaim: ""
- storageClass: "nodemanager-1"
- accessMode: ReadWriteOnce
- size: 1Gi
-
- client:
- include: true
- ip: client
- type: ClusterIP
+ replicas: 2
nodeSelector:
tolerations:
affinity:
- subPath: "client"
- existingClaim: ""
- storageClass: "client"
+ sessionProcessorsPerNode: 2
+ subPath: "nodemanager"
+ storageClass:
accessMode: ReadWriteOnce
size: 1Gi
+ existingClaim:
+ resources:
+ requests:
+ cpu: "2"
+ memory: "4Gi"
mysql:
include: true
@@ -810,11 +2510,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,12 +2523,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
@@ -844,17 +2551,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 +2567,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,11 +2625,17 @@ modules:
include: true
nodeSelector:
tolerations:
+ env:
+ confs:
affinity:
type: ClusterIP
httpNodePort: 30094
httpsNodePort: 30099
- loadBalancerIP:
+ loadBalancerIP:
+ existingClaim:
+ accessMode: ReadWriteOnce
+ storageClass:
+ size: 1Gi
publicLB:
enabled: false
# exchange:
@@ -939,7 +2658,6 @@ modules:
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 +2667,12 @@ modules:
partyId: {{ .partyId | int64 | toString }}
partyName: {{ .name }}
+computing: {{ .computing }}
+federation: {{ .federation }}
+storage: {{ .storage }}
+algorithm: {{ .algorithm }}
+device: {{ .device }}
+
{{- $partyId := (.partyId | int64 | toString) }}
{{- with .ingress }}
@@ -1084,6 +2808,7 @@ modules:
type: {{ .type | default "ClusterIP" }}
nodePort: {{ .nodePort }}
loadBalancerIP: {{ .loadBalancerIP }}
+ enableTLS: {{ .enableTLS | default false}}
{{- with .nodeSelector }}
nodeSelector:
{{ toYaml . | indent 6 }}
@@ -1111,6 +2836,10 @@ modules:
{{- end }}
concurrency: {{ .concurrency }}
{{- end }}
+ {{- with .resources }}
+ resources:
+{{ toYaml . | indent 6 }}
+ {{- end }}
{{- end }}
@@ -1132,6 +2861,10 @@ modules:
{{- end }}
{{- with .affinity }}
affinity:
+{{ toYaml . | indent 6 }}
+ {{- end }}
+ {{- with .resources }}
+ resources:
{{ toYaml . | indent 6 }}
{{- end }}
{{- end }}
@@ -1139,17 +2872,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 +2897,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 +2924,9 @@ modules:
host: {{ .host }}
mng_port: {{ .mng_port }}
port: {{ .port }}
+ topic_ttl: {{ .topic_ttl }}
+ cluster: {{ .cluster }}
+ tenant: {{ .tenant }}
{{- end }}
{{- with .rabbitmq }}
rabbitmq:
@@ -1204,6 +2942,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 +2958,8 @@ modules:
{{- with .clustermanager }}
ip: clustermanager
type: "ClusterIP"
- {{- with .nodeSelector }}
+ enableTLS: {{ .enableTLS | default false }}
+ {{- with .nodeSelector }}
nodeSelector:
{{ toYaml . | indent 6 }}
{{- end }}
@@ -1222,6 +2969,10 @@ modules:
{{- end }}
{{- with .affinity }}
affinity:
+{{ toYaml . | indent 6 }}
+ {{- end }}
+ {{- with .resources }}
+ resources:
{{ toYaml . | indent 6 }}
{{- end }}
{{- end }}
@@ -1230,28 +2981,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 +3027,7 @@ modules:
affinity:
{{ toYaml . | indent 6 }}
{{- end }}
+ notebook_hashed_password: {{ .notebook_hashed_password | default "" }}
{{- end }}
@@ -1323,6 +3075,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 +3114,7 @@ modules:
{{ toYaml . | indent 8 }}
{{- end }}
type: {{ .master.type }}
+ nodePort: {{ .master.nodePort }}
{{- end }}
{{- if .worker }}
worker:
@@ -1398,8 +3163,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 +3182,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 +3222,10 @@ modules:
rabbitmq:
include: {{ has "rabbitmq" .modules }}
{{- with .rabbitmq }}
+ {{- with .resources }}
+ resources:
+{{ toYaml . | indent 6 }}
+ {{- end }}
{{- with .nodeSelector }}
nodeSelector:
{{ toYaml . | indent 6 }}
@@ -1486,6 +3256,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,7 +3302,7 @@ modules:
existingClaim: {{ .existingClaim }}
storageClass: {{ .storageClass | default "" }}
accessMode: {{ .accessMode | default "ReadWriteOnce" }}
- size: {{ .size }}
+ size: {{ .size | default "1Gi" }}
{{- end }}
externalMysqlIp: {{ .externalMysqlIp }}
@@ -1531,28 +3313,28 @@ externalMysqlPassword: {{ .externalMysqlPassword }}`,
ArchiveContent: nil,
Private: false,
},
- "242bf84c-548c-43d4-9f34-15f6d4dc0f33": {
+ "fd30a219-c9d2-4f6a-9146-f06c05a666f2": {
Model: gorm.Model{
- ID: 3,
+ ID: 5,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
- UUID: "242bf84c-548c-43d4-9f34-15f6d4dc0f33",
- Name: "chart for FATE exchange v1.9.1 with fml-manager service v0.2.0",
- Description: "This chart is for deploying FATE exchange v1.9.1 with fml-manager v0.2.0",
+ UUID: "fd30a219-c9d2-4f6a-9146-f06c05a666f2",
+ Name: "chart for FATE exchange v1.11.1 with fml-manager service v0.3.0",
+ Description: "This chart is for deploying FATE exchange v1.11.1 with fml-manager v0.3.0",
Type: entity.ChartTypeFATEExchange,
ChartName: "fate-exchange",
- Version: "v1.9.1-fedlcm-v0.2.0",
- AppVersion: "exchangev1.9.1 & fedlcmv0.2.0",
+ Version: "v1.11.1-fedlcm-v0.3.0",
+ AppVersion: "exchangev1.11.1 & fedlcmv0.3.0",
Chart: `apiVersion: v1
-appVersion: "exchangev1.9.1 & fedlcmv0.2.0"
+appVersion: "exchangev1.11.1 & 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.11.1-fedlcm-v0.3.0`,
InitialYamlTemplate: `name: {{.Name}}
namespace: {{.Namespace}}
chartName: fate-exchange
-chartVersion: v1.9.1-fedlcm-v0.2.0
+chartVersion: v1.11.1-fedlcm-v0.3.0
partyId: 0
{{- if .UseRegistry}}
registry: {{.Registry}}
@@ -1631,13 +3413,10 @@ partyName: fate-exchange
image:
registry: federatedai
isThridParty:
- tag: 1.9.1-release
+ tag: 1.11.1-release
pullPolicy: IfNotPresent
- imagePullSecrets:
-# - name:
-
-partyId: 9999
-partyName: fate-9999
+ imagePullSecrets:
+# - name:
podSecurityPolicy:
enabled: false
@@ -1737,12 +3516,12 @@ modules:
fmlManagerServer:
include: true
image: federatedai/fml-manager-server
- imageTag: v0.2.0
+ imageTag: v0.3.0
# nodeSelector:
# tolerations:
# affinity:
type: ClusterIP
- # nodePort:
+ # nodePort:
# loadBalancerIP: 192.168.0.1
postgresHost: postgres
postgresPort: 5432
@@ -1931,28 +3710,29 @@ modules:
caCert: {{ .caCert | default "/var/lib/fml_manager/cert/ca.crt" }}
tlsEnabled: {{ .tlsEnabled | default "true" }}
{{- end }}`,
- ArchiveContent: mock.FATEExchange191WithManagerChartArchiveContent,
+ ArchiveContent: mock.FATEExchange_1_11_1_WithManagerChartArchiveContent,
Private: true,
},
- "8d1b15c1-cc7e-460b-8563-fa732457a049": {
+ "73acbbc0-4cdf-46bf-b48f-25fe1e03b91f": {
Model: gorm.Model{
- ID: 4,
+ ID: 6,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
- UUID: "8d1b15c1-cc7e-460b-8563-fa732457a049",
- Name: "chart for FATE cluster v1.9.1 with site-portal v0.2.0",
- Description: "This is chart for installing FATE cluster v1.9.1 with site-portal v0.2.0",
+ UUID: "73acbbc0-4cdf-46bf-b48f-25fe1e03b91f",
+ Name: "chart for FATE cluster v1.11.1 with site-portal v0.3.0",
+ Description: "This is chart for installing FATE cluster v1.11.1 with site-portal v0.3.0",
Type: entity.ChartTypeFATECluster,
ChartName: "fate",
- Version: "v1.9.1-fedlcm-v0.2.0",
- AppVersion: "fatev1.9.1+fedlcmv0.2.0",
- ArchiveContent: mock.FATE191WithPortalChartArchiveContent,
+ Version: "v1.11.1-fedlcm-v0.3.0",
+ AppVersion: "fatev1.11.1+fedlcmv0.3.0",
+ ArchiveContent: mock.FATE_1_11_1_WithPortalChartArchiveContent,
Chart: `apiVersion: v1
-appVersion: "fatev1.9.1+fedlcmv0.2.0"
+appVersion: "fatev1.11.1+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.11.1-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
@@ -1960,7 +3740,7 @@ sources:
InitialYamlTemplate: `name: {{.Name}}
namespace: {{.Namespace}}
chartName: fate
-chartVersion: v1.9.1-fedlcm-v0.2.0
+chartVersion: v1.11.1-fedlcm-v0.3.0
{{- if .UseRegistry}}
registry: {{.Registry}}
{{- end }}
@@ -1973,6 +3753,7 @@ podSecurityPolicy:
imagePullSecrets:
- name: {{.ImagePullSecretsName}}
{{- end }}
+ingressClassName: nginx
modules:
- mysql
@@ -1996,8 +3777,13 @@ modules:
computing: Spark
federation: Pulsar
storage: HDFS
+{{- if .FATEFlowGPUEnabled }}
+algorithm: NN
+device: GPU
+{{- else }}
algorithm: Basic
device: CPU
+{{- end }}
skippedKeys:
- route_table
@@ -2029,19 +3815,33 @@ ingress:
python:
# type: ClusterIP
+ # replicas: 1
# httpNodePort:
# grpcNodePort:
# loadBalancerIP:
# serviceAccountName: ""
- # resources:
# nodeSelector:
# tolerations:
# affinity:
+ # failedTaskAutoRetryTimes:
+ # failedTaskAutoRetryDelay:
# logLevel: INFO
existingClaim: ""
storageClass: {{ .StorageClass }}
accessMode: ReadWriteOnce
+ # dependent_distribution: false
size: 10Gi
+ {{- if .FATEFlowGPUEnabled }}
+ resources:
+ requests:
+ nvidia.com/gpu: {{.FATEFlowGPUNum}}
+ # cpu: "2"
+ # memory: "4Gi"
+ limits:
+ nvidia.com/gpu: {{.FATEFlowGPUNum}}
+ # cpu: "4"
+ # memory: "8Gi"
+ {{- else }}
# resources:
# requests:
# cpu: "2"
@@ -2049,6 +3849,7 @@ python:
# limits:
# cpu: "4"
# memory: "8Gi"
+ {{- end }}
{{- if .EnableExternalSpark }}
spark:
cores_per_node: {{.ExternalSparkCoresPerNode}}
@@ -2087,31 +3888,48 @@ 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
+ 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
- storageClass: {{ .StorageClass }}
- # nodeSelector:
- # tolerations:
- # affinity:
+# notebook_hashed_password: ""
+
mysql:
subPath: "mysql"
@@ -2132,7 +3950,7 @@ mysql:
spark:
master:
# image: "federatedai/spark-master"
- # imageTag: "1.9.1-release"
+ # imageTag: "1.11.1-release"
replicas: 1
# resources:
# requests:
@@ -2147,7 +3965,7 @@ spark:
# type: ClusterIP
worker:
# image: "federatedai/spark-worker"
- # imageTag: "1.9.1-release"
+ # imageTag: "1.11.1-release"
replicas: 2
# resources:
# requests:
@@ -2203,6 +4021,20 @@ pulsar:
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}}
@@ -2276,7 +4108,7 @@ sitePortalServer:
image:
registry: federatedai
isThridParty:
- tag: 1.9.1-release
+ tag: 1.11.1-release
pullPolicy: IfNotPresent
imagePullSecrets:
# - name:
@@ -2292,7 +4124,7 @@ federation: Eggroll
storage: Eggroll
# Algorithm: Basic, NN
algorithm: Basic
-# Device: CPU, IPCL
+# Device: CPU, IPCL, GPU
device: IPCL
istio:
@@ -2338,12 +4170,12 @@ ingress:
path: /
tls: []
frontend:
- # annotations:
+ # annotations:
hosts:
- name: frontend.example.com
path: /
tls: []
-
+
exchange:
partyIp: 192.168.1.1
partyPort: 30001
@@ -2400,6 +4232,7 @@ modules:
affinity:
python:
include: true
+ replicas: 1
type: ClusterIP
httpNodePort: 30097
grpcNodePort: 30092
@@ -2408,9 +4241,12 @@ modules:
nodeSelector:
tolerations:
affinity:
+ failedTaskAutoRetryTimes:
+ failedTaskAutoRetryDelay:
logLevel: INFO
# subPath: ""
existingClaim:
+ dependent_distribution: false
claimName: python-data
storageClass:
accessMode: ReadWriteOnce
@@ -2439,12 +4275,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
@@ -2457,6 +4302,7 @@ modules:
storageClass:
accessMode: ReadWriteOnce
size: 1Gi
+ notebook_hashed_password:
clustermanager:
include: true
ip: clustermanager
@@ -2464,7 +4310,7 @@ modules:
nodeSelector:
tolerations:
affinity:
- nodemanager:
+ nodemanager:
include: true
replicas: 2
nodeSelector:
@@ -2481,20 +4327,8 @@ modules:
cpu: "2"
memory: "4Gi"
- client:
- include: true
- ip: client
- type: ClusterIP
- nodeSelector:
- tolerations:
- affinity:
- subPath: "client"
- existingClaim:
- storageClass:
- accessMode: ReadWriteOnce
- size: 1Gi
- mysql:
+ mysql:
include: true
type: ClusterIP
nodeSelector:
@@ -2511,6 +4345,7 @@ modules:
storageClass:
accessMode: ReadWriteOnce
size: 1Gi
+
serving:
ip: 192.168.9.1
port: 30095
@@ -2518,12 +4353,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
@@ -2647,7 +4488,7 @@ modules:
# tolerations:
# affinity:
type: ClusterIP
- # nodePort:
+ # nodePort:
# loadBalancerIP:
user: site_portal
password: site_portal
@@ -2657,28 +4498,28 @@ modules:
storageClass: ""
accessMode: ReadWriteOnce
size: 1Gi
-
+
frontend:
include: false
image: federatedai/site-portal-frontend
- imageTag: v0.2.0
+ imageTag: v0.3.0
# nodeSelector:
# tolerations:
# affinity:
type: ClusterIP
-
- # nodePort:
+
+ # nodePort:
# loadBalancerIP:
-
+
sitePortalServer:
include: false
image: site-portal-server
- imageTag: v0.2.0
+ imageTag: v0.3.0
# nodeSelector:
# tolerations:
# affinity:
type: ClusterIP
- # nodePort:
+ # nodePort:
# loadBalancerIP:
existingClaim: ""
storageClass: "sitePortalServer"
@@ -2941,6 +4782,7 @@ modules:
python:
include: {{ has "python" .modules }}
{{- with .python }}
+ replicas: {{ .replicas | default 1 }}
{{- with .resources }}
resources:
{{ toYaml . | indent 6 }}
@@ -2950,6 +4792,7 @@ modules:
httpNodePort: {{ .httpNodePort }}
grpcNodePort: {{ .grpcNodePort }}
loadBalancerIP: {{ .loadBalancerIP }}
+ dependent_distribution: {{ .dependent_distribution }}
serviceAccountName: {{ .serviceAccountName }}
{{- with .nodeSelector }}
nodeSelector:
@@ -2963,6 +4806,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" }}
@@ -2988,6 +4833,9 @@ modules:
host: {{ .host }}
mng_port: {{ .mng_port }}
port: {{ .port }}
+ topic_ttl: {{ .topic_ttl }}
+ cluster: {{ .cluster }}
+ tenant: {{ .tenant }}
{{- end }}
{{- with .rabbitmq }}
rabbitmq:
@@ -3003,6 +4851,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 }}
@@ -3037,7 +4893,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 }}
@@ -3079,6 +4936,7 @@ modules:
affinity:
{{ toYaml . | indent 6 }}
{{- end }}
+ notebook_hashed_password: {{ .notebook_hashed_password | default "" }}
{{- end }}
@@ -3126,6 +4984,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:
@@ -3452,23 +5322,23 @@ externalMysqlPassword: {{ .externalMysqlPassword }}
UpdatedAt: time.Now(),
},
UUID: "516a10e2-0b96-417c-812b-ee45ed197e81",
- Name: "chart for FedLCM private OpenFL Director v0.1.0",
- Description: "This chart is for deploying FedLCM's OpenFL director v0.1.0 services, based from OpenFL 1.3 release",
+ Name: "chart for FedLCM private OpenFL Director v0.3.0",
+ Description: "This chart is for deploying FedLCM's OpenFL director v0.3.0 services, based from OpenFL 1.5 release",
Type: entity.ChartTypeOpenFLDirector,
ChartName: "openfl-director",
- Version: "v0.1.0",
- AppVersion: "openfl-director-v0.1.0",
+ Version: "v0.3.0",
+ AppVersion: "openfl-director-v1.5",
Chart: `apiVersion: v1
-appVersion: "openfl-director-v0.1.0"
+appVersion: "openfl-director-v1.5"
description: A Helm chart for openfl director, based on official OpenFL container image
name: openfl-director
-version: v0.1.0
+version: v0.3.0
sources:
- - https://github.com/FederatedAI/KubeFATE.git`,
+ - https://github.com/securefederatedai/openfl`,
InitialYamlTemplate: `name: {{.Name}}
namespace: {{.Namespace}}
chartName: openfl-director
-chartVersion: v0.1.0
+chartVersion: v0.3.0
{{- if .UseRegistry}}
registry: {{.Registry}}
{{- end }}
@@ -3493,7 +5363,7 @@ ingress:
director:
image: fedlcm-openfl
- imageTag: v0.1.0
+ imageTag: v0.3.0
type: {{.ServiceType}}
sampleShape: "{{.SampleShape}}"
targetShape: "{{.TargetShape}}"
@@ -3506,7 +5376,7 @@ director:
notebook:
image: fedlcm-openfl
- imageTag: v0.1.0
+ imageTag: v0.3.0
type: {{.ServiceType}}
password: {{.JupyterPassword}}
# nodeSelector:
@@ -3534,7 +5404,7 @@ ingress:
modules:
director:
image: fedlcm-openfl
- imageTag: v0.1.0
+ imageTag: v0.3.0
sampleShape: "['1']"
targetShape: "['1']"
envoyHealthCheckPeriod: 60
@@ -3547,7 +5417,7 @@ modules:
notebook:
image: fedlcm-openfl
- imageTag: v0.1.0
+ imageTag: v0.3.0
# password:
# nodeSelector:
# tolerations:
@@ -3633,7 +5503,7 @@ modules:
nodePort: {{ .nodePort }}
loadBalancerIP: {{ .loadBalancerIP }}
{{- end }}`,
- ArchiveContent: mock.FedLCMOpenFLDirector010ChartArchiveContent,
+ ArchiveContent: mock.FedLCMOpenFLDirector030ChartArchiveContent,
Private: true,
},
"c62b27a6-bf0f-4515-840a-2554ed63aa56": {
@@ -3643,23 +5513,23 @@ modules:
UpdatedAt: time.Now(),
},
UUID: "c62b27a6-bf0f-4515-840a-2554ed63aa56",
- Name: "chart for FedLCM private OpenFL Envoy v0.1.0",
- Description: "This chart is for deploying OpenFL envoy built for FedLCM, based from OpenFL 1.3 release",
+ Name: "chart for FedLCM private OpenFL Envoy v0.3.0",
+ Description: "This chart is for deploying OpenFL envoy built for FedLCM, based from OpenFL 1.5 release",
Type: entity.ChartTypeOpenFLEnvoy,
ChartName: "openfl-envoy",
- Version: "v0.1.0",
- AppVersion: "openfl-envoy-v0.1.0",
+ Version: "v0.3.0",
+ AppVersion: "openfl-envoy-v1.5",
Chart: `apiVersion: v1
-appVersion: "openfl-envoy-v0.1.0"
+appVersion: "openfl-envoy-v1.5"
description: A Helm chart for openfl envoy
name: openfl-envoy
-version: v0.1.0
+version: v0.3.0
sources:
- https://github.com/FederatedAI/KubeFATE.git`,
InitialYamlTemplate: `name: {{.Name}}
namespace: {{.Namespace}}
chartName: openfl-envoy
-chartVersion: v0.1.0
+chartVersion: v0.3.0
{{- if .UseRegistry}}
registry: {{.Registry}}
{{- end }}
@@ -3674,7 +5544,7 @@ modules:
envoy:
image: fedlcm-openfl
- imageTag: v0.1.0
+ imageTag: v0.3.0
directorFqdn: {{.DirectorFQDN}}
directorIp: {{.DirectorIP}}
directorPort: {{.DirectorPort}}
@@ -3698,7 +5568,7 @@ podSecurityPolicy:
modules:
envoy:
image: fedlcm-openfl
- imageTag: v0.1.0
+ imageTag: v0.3.0
directorFqdn: director
directorIp: 192.168.1.1
directorPort: 50051
@@ -3746,7 +5616,7 @@ modules:
{{ toYaml . | indent 6 }}
{{- end }}
{{- end }}`,
- ArchiveContent: mock.FedLCMOpenFLEnvoy010ChartArchiveContent,
+ ArchiveContent: mock.FedLCMOpenFLEnvoy030ChartArchiveContent,
Private: true,
},
}
diff --git a/server/infrastructure/gorm/mock/chart_fate_191.go b/server/infrastructure/gorm/mock/chart_fate_191.go
deleted file mode 100644
index 62aaa4f9..00000000
--- a/server/infrastructure/gorm/mock/chart_fate_191.go
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright 2022 VMware, Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT 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 (
- FATE191WithPortalChartArchiveContent = getFATE191WithPortalChartArchiveContent()
- FATEExchange191WithManagerChartArchiveContent = getFATEExchange191WithManagerChartArchiveContent()
-)
-
-func getFATE191WithPortalChartArchiveContent() []byte {
- base64Content := `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`
- content, _ := base64.StdEncoding.DecodeString(base64Content)
- return content
-}
-func getFATEExchange191WithManagerChartArchiveContent() []byte {
- base64Content := `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`
- content, _ := base64.StdEncoding.DecodeString(base64Content)
- return content
-}
diff --git a/server/infrastructure/gorm/mock/chart_fate_1_11_1.go b/server/infrastructure/gorm/mock/chart_fate_1_11_1.go
new file mode 100644
index 00000000..9a105782
--- /dev/null
+++ b/server/infrastructure/gorm/mock/chart_fate_1_11_1.go
@@ -0,0 +1,33 @@
+// Copyright 2022-2023 VMware, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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 (
+ FATE_1_11_1_WithPortalChartArchiveContent = getFATE_1_11_1_WithPortalChartArchiveContent()
+ FATEExchange_1_11_1_WithManagerChartArchiveContent = getFATEExchange_1_11_1_WithManagerChartArchiveContent()
+)
+
+func getFATE_1_11_1_WithPortalChartArchiveContent() []byte {
+ base64Content := `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`
+ content, _ := base64.StdEncoding.DecodeString(base64Content)
+ return content
+}
+func getFATEExchange_1_11_1_WithManagerChartArchiveContent() []byte {
+ base64Content := `H4sIFAAAAAAA/ykAK2FIUjBjSE02THk5NWIzVjBkUzVpWlM5Nk9WVjZNV2xqYW5keVRRbz1IZWxtAOx9aXPbttZwP/NX4JVy37R5KmqNbCuTueM6buqpY3tsp72dTkcDkZCEmiJZALKjpv7vz2AjAW5a7bT3ET/YIpZzsBwcnI3AGDLUQJ+8KQwnqHkyhYS5CzgLvtrh02q1Wv1eT/xvtVrZ/63O6/ZX7V6v02+3O/1+/6tWu9s/6H8FWrtsRNkzpwySr1pb48p27h/ywBj/hAjFUTgA920HxnHyqqnivu22224b/H8wRn7gze5bbtdtOT6iHsExE2WPwQ8omAGP0w8YRwRwukogABj6YDwLGjMYwgkiTghnaAAs2nPu02YIfA2JraHQfemB+i997PV/D4M5ortmAEvWf7/T6WTWf6/da+3X/3M8MSRsceYPQNsRPy8KVqaDZ3CCBg4ABE0wZWQx4KwAEciQD7EDAKa3U4L9Kw6Bl2NwMgBqGRMUIEiRA0A8D4KrKMDeYgDOxhcRuyKIopBxABzD1TwIbpBHEKMDpw5AAwg+4Thx5N8gb04wW6j6DgAohKMA+byxAUWOE3P+QRkKPVSYzdt2jikbOA2Q9Prw8PCQt0y8xwPQPuq47f6he+i2dfJVRNgAdFutw7ZZs81nuahqu5Wv2261HWcW+fMAUTGOURBQzNAAOAAAgEMvmPtIt1UkxYOklEhgixgNwEkwpwyRsyuRFkY+SprXaou0IIL+dzCAoceLDUSaHIvb8xsTA698gwLksYjIYiwK+JziKKQyAY7HOMRySgGog2QMAaZgTpEPWAS8KBzjyZwgwKYIeLKBAIfjiMwEMBCNAQwCURsjCtgUMvB7hEOAQ1En2SZ8FAfRYoZCBmaRj1zw9c9TFAI0mfChAA9QYYVU4oKBNw8kDhROcIgAZCKH4Rn6RjTamHbZh/zkG33Lk4CRaRJCDpYmhxJgiigKoLXFvPHWfxqUEYOc+ws12yJpylh8kUy/mP+uyJiQ2MtkHBYRBsgTAViNCkg0Z2jIOEk9Lx3cxJDcbUQFRpMHyTSIKVOdlimc7Y2D6GFgpDXEiA7jZLo6SSbPnkaUFU6zymYsNuoeGIBjEn1aVGI63AJTN+nm0dHR0bq9bB1V9fKoCnXraJ1Oto6qOrkEEe8jI5w8vRtE7hFZsoCqmOfBfo0YawQk1ENDnK6YBhj/4YeK2bmcjtyITIzZZfMwRME1h5TOYsvtDDhJHuXgcNJcA0x3wGf9yOG7a0TZhCCamXFG5uUTLqWYpGqadiukla7bVZOX3xnr+RmvZ+Z8ThEZcCVnqJUcILYfSh8i4udz/FE+rQ7ofHQF2XQAajWVgj5hynA4OQkgnhnplEUETtBJACk1kqHnIUo/RHw0rhH0fyaYocvQQ7oa/pOP6XvMR3E8Cz5I9IXrJxlNNXKGzNc0tLkGFZUz46nUti1GtGgO6+myVe/ZJZuSi+Qderp/EJzFmnz9IrnA6163YyW/K5ghnfexeLY1wNJZZ4HCdtjrSXKTg3eCeGLzHpJmgEdNo1rTQ4Q1ZSnXU7u/fP0RLVaoc4cWoo4XYBSyZYhkqQSRfF2CSNVJEMGlSGCCgAX0VEvqLznBvfzSGtH/rcfW/xmaxQFkiDaHUxRwjcpl8damgCX6f7vTy+r//c5BZ6//P8fz+XPzlbTdEfTHHBPkgwCOUEDBq+bjo/P5cwP4aMwFhprYqGVmDTQeH5UV7/Nn4P4k7UaJBQH8Bf6YRwwB8PjoJDpStuSZb5WLHkLOVO/mI8RROUo8ylojpohgJvYjDu9aWhdcvoFhD3E4yuBg54tWPT46wkIps6SxW2fwnqLQFx1zxKjMIPOm51WDYZR4khHJtck5IQgyKT1yXFxcFPJdFDKuIiMitgY+ENDzonnIuNQ5p8jJdUCVO5bFeBNlF3gZPE6blTW9uMqwIgpzDv75M4cJ5wEr6LXuREBRUYWa+lHL9tb4/aVXyH/3U8b/Dflu671gKf8/6Nr8v9Pq9Xt7/v8cj1MHJ1G8IHgyZaDTah81Oq1OB/z04QES9C04Cz3XqYNz7KGQ65Pz0EdEsJzjGHpTpHO+BcptBDpuC3zNC9RUVu2bN04dLKI5mMEFCCPG+RFgU0zBGAdCo0Ux4/qtF83iAHNJHjxgNhVoFBDeiF8UiGjEIA4BBF4ULzQHVOUAZI60EwyazYeHBxeKZnL9shnIIrR5fnZyenFz2ui4LacOPoYBojTd/kYLAOM4wJ5Q3gP4ACIC4IQgqcLjEDwQzDWybwGNxoyPk1MHPqaM4NGcWYOkW4WpVSAKAQxB7fgGnN3UwHfHN2c33zp18PPZ7Q+XH2/Bz8fX18cXt2enN+DyGpxcXrw7uz27vLgBl9+D44tfwI9nF+++BQizKSIAfYoJb39EAObDh3w+VjcIWQ0YR7JBNEYeHmMPBDCczOEEgUl0j0iIwwmIEZlhyieRAhj6Th0EeIaZVNHynXLVNnnGVT4KhNLHdxPic2AsEmWhx+YwAMq3xyeLYobiiDAYFGyqiT7qCkWSugxO5Lak1Elzj/jSS+e/4lmF/2u1tiE9GGu7B5fw/4PXB60M/++0+6/3/P85Hpv95zj/P473r8n99/x/O/5vqgrKv+lqduEqQyKX7c0wExjHtHnfdu5w6A/Au8SO7cwQgz5kcOAA6fk1zXVS8ZRWP860PkRzPzCL8MYohLaq6oK/AA59FDLQ423hYyCd2WKW6QC0HQAoI5ChiWV7vEae0Ld4tmW9NBQ/bR1frU2Wxmg0rA+EcqT5r8JijIfwTlgItxiGQ6BUMT0UwmwXhXxtIWJgaOTnQT/KJmxosPn550XAX4m293VMcMjGoPYv2vwXrYGkpijo6uACUNMQauAb8Pg4WIrjFk4MNLV21+3WdBeN1l4Z8QcGTIk9DU6wa6LwfmC86hG5ury5fX99ejP8eHN6beQDIGJoqgdmThFJFH0LWw781fHNzc+X1+/WRaEdDyuieffdugj8UTFoLtvRgQWtkdJWxtSusEXBfMYpOczXnPFU6RJJ7Mm6DX8ETb48rBpZztEoKJE4WYw8LliKjae0w6YPI+1yxjn4+TNg0S9wFhQuN4mGy69WQjVew0+SVrOcJ0+AVHti0jqpb2Z7dHLFZWN+0gr5aKBCnP0KnHnzliBnzWCL7V9uWp8gLhcyzS6Ogwe40OxP0qtBqspmxuWbxASWRiMlFjNjnWQZa5ZM0Sxmi3eYDMDnRwuNsKStAShpB/tJtFq6Ea0F4fGki6zlMkcSliPSYLcpuxa4LcZrTEuj0XAywaZSAFCW2+fe/Q0+pcewxry4kew9qhNxlmMxSCaI5RiZogH0R8VKXsQI1HRwTg3Uzg3fZc3mKRJ+1XzoUhWLICYRi7woGIDbkytHSzVVQEUTRf2MHThX0va7yjpZX2ytClUOQM3JdMGWubYTsHqaBlW/YOhXrtWvzcVcvRa4jKJo+apotWXJvpzQ9eLdPbWnYQFUBXc0KukgLS+ngiAazYmnmR5XlhBNd2sVjFBNWxT/uSJtmbENIPEa6PVVa9TWqWqmSC6ngiVsdpovVtmXkhYmtGu8WD/1ry+t9+tnFfuPCiTYOCq82v7T7rS6vaz9v9tt7+0/z/Hs7T97+8+O7T/ZqLId2IEKo83KdsmCwn8j29AqrXsyK9GGQ7O2vagkOtDQ7gq31zzlbGpByjeg3JZUjNW2KRmIa+DrdMCKHVbuN9/syvyUM6esuObSuDobXoE1ZpUhUXGLNiwuzWTU0UIEh63DVqaaEILA7rq1rgkpG5JYaEMyiShTKBXk0rRllsIfLm9uV7Sy5fptRrGuajm8vN4anZjz1dCtYQctRfdxdbvoygbLUmTvSgyY25pgywdzJZPs9x/OPxxfHL8/vR7ent8MTy+Ovzs/3Ri3sV4KsW657EoafXN6/dPp9fDkdHMKTMOi1xouhfnH01+2Q/wjWqyF9+T87PTidqsep/HZm2DeosdJjPd6eI+36y1cu6fbMDS9f5UtgtXM4zmwz+wTyHfrGX0DOeR7H0GFj2ALxppzLxRJ1Rl5hIpBsCUfmXZRVTWl/A3M88+ulBWZ7Lmy3yiQ9i3rvSGAmtZ7I7ncep+fsl1Y8Qs5yZrW/Mwo0OXDoL85sodhI/n/nzhgpe6P4iZXmapzNTZ1hywHtIZbZBvbguEg+bvZqPfP0z1l9n9xIMGODoJZYv/v9jvZ859eH/S7e/v/czxF4f+tffj/3vy/sflfcg7D5i/lyBNx9MEHGC93hwsIDXlWQoVIKYqtLkVqHMY5B4K/DcBfArQy9Q5S4cE8uaKhjqaQSFMh2ziS4qjbMs2cxjkXR912S7mIc9/7PT4+KUZQcNyHhqxzyoAflgLvyy8xiDiJ4sUdWnwLXtzDAAzelpCDeR5G+hkgrykGoFx1syQfCYsThp408acu4ilBGI0if/FGJD1E5A6RYUwiD1GKKACdN0YFREhEhkE04ULahDbFuxtEkzdL8jkWhj0kyxnFmj66b1LmR3MGxIEepSUQISrdbFGMfT3SAqPsaox9o5CEeI9CRsHnZGpUV70oDJEnFytotzo9WfExU51PsFFZLx35zPAMCdmXvklKqGUxFCKxZo0cTTPyGGINygiCM6OVqgtDeaYJADPOrcHLFwTNIoaG0PcJaAD9Kqbu1xcMz9AwiDwY/AZqL1R4SQ3UXgiC5ORaAy8zRnrzefmCMsjmFLzgNDAcLRiiQ8qpSIEgaIwIVzcqoajCvFFDOEFh2oRPvD8PkPjI57+WtWYey3EZ6mYlCaL/1ZVV94d8UIyaBNE4CikS6VUQ0slLfsh4HoOiZYIkaT5DmQmkKPTF9qyeKEyB1pkXD8MontOplZcUuEMohgG+ly0VK6L/Oq0vthLqRQTRIQ6HUwT5ex5MffInjm3UwRwOY+jdwQkaxpBNQe1FTNAYf2oGc9j8txvM4Zs3tTcGeWM2VAtktBjy6qJXL3lxnolhgP9EvN7LDHY97JIl6qN6PmcsLFzHAu3OgdtyW257wBnyG3Esye9zyvguj8N7GGAf8Gnn+zSkAII4gB4C0yjwLccoACOl+OUbq3OMpgJ7eVuNFtx65Ua3v2ijNQ+658u8+Urw9xzzUv36XMRiOD2KzTPDZWy2UrVixLLT5gKQ8pKUjajfBHkI3yN/OTSKhCwlF3HNXv/V3EMyoqRC2ogayCXr9lhZaisQqGtZujY4gWAEzIsbBjNIxjLHEATV2KQUiFhHcNTuHL6xMgQEqx2cC7RpUSmd2y3MjSGlBq12LVSPikpy1ss1Qku0eLUjKTMfQGIcd9Auk45UYR2JuLtok5LWP1mAyeqjtXZMSVYOzoeRZIJAHh+lFDXIFWFwsqv4DC+azWDoZ2MOmiMcNkeQTjPpDS+T8FeGEXAtu4FBjdY5G2xmlRWZOot8PMY8MV9gUpMjJXZEM5fvjRlkyJtGoKYOlBP7aTTO6UeAzgV7+H+1TO0oRiFBlC1AYwJe+hDNxBfe4zcmc1vlc6CMGlNYom2WWDXag1NtMybR78hjtMkpsSm4iqSLphjLVKfIdC+ri9LsTOmviEogrN8UY1pXbEups0626ZndkxLpc/okJca9I3Kdj5UalZTtaTONva4KDDMbeAyfaK8t8wva3i+Dy5jeLyO53KelKHsXjiwJyjy+dzOXH5fwsx1sF3ew/QU6aB5DvAsXndHAKr+cLLapM66k9hoeuDXlrS/vdCvz/8Q03tk1AEvO/2j12/2s/6fX25//9yzPWse9mew+FtnN+/YIMahZ/1W2urUJlH7WRuPVmb7eSwoP2TM3BYLvcYAm6RUAfN2e43Cuzlknc477eh4e0+NwwXPncRwgribC4D2J5jEtKUj4T3ECbHH+mIrqJbnGhtwAL1+9/LJn3ZWtfxJtcNBPybNs/b8+6GXXf7+1P//tWZ6V13+OAZAR9Fw4Z9OI4D+FaO/eHVIXR6nh5ToK0EocgFPb7lgA0ddsNACMsVrKUt749aXkWy9/c5TgrD5rTbN9qgZAlMSIqrL3iIx0OVl2TlEGDm8BL/NrSdN++9sdYFa1/kc49HE42ZoNLN3/D7LxHwft7v78r2d5Nt7/V1z+30kaWokL2FruDvlBFKBrNBbfwSt+UNF+BwCDey0VN+YjYdKR7MZSgHU/Uv25DIguQWPoFZxbLJJB5oTcHc1/xfoXdw9teuaf+SyJ/3rd7WT3/4OD/l7+f5ZnH/+1j//abfyX5hxbhYAl7GdpFFhySdragWDqWjE3JlGMxHU7Oqqobl+9pgsmPfMiccotZBF5W8XTcxWnEWVvq2HHEWFvj7oHrZJsjsPFfg7vmV+KVXh0XOnRiSGbvs26liw/0e80CpfDuUOLt8tLPUwxQwGm7G3iwi2u9Pv9zI3EXaL0beM//xn8z0eK3rffnwDxckVwyN6fvEMM4oDaaZChGwZnMU8OosnEG8iILYkh2cfciecG0SR1audHdk6n7gx+cgliZPG2W1EqiMKJKtapKDaCzJvK84Vc6Zp/2y6ZVujDmImvD0N/NB+Ls3LeimuWWo5jmG5LF1tyxV+WCryIIFerNPIHclVkiBbu3iY3/SypJe554ULTqjXlTS8G1en7X2I0q6qWr5O5Aae43h1alNTTl9OU4FO33mzY2uTOnFxV+zadyuoFbc9crGPYoDNxrHzNauaVhmjUjCK1QSZ2I7l6IZuRyfy1MFIlXyU18Fves+S+sswhARY2HNeUQV5dnAgeH2vflpfnPLIm5eTkLsVyBEXH5pU0QS+nFZAnLLoEYfZ0gPTJp/5mpTzayNNAW4vfi2s4MxiMIeTYdzu1TzxPG4xKwRjXCrfF4oHQ4c+bj0QCYRVy6ReTS0G/89C2n7RdUPbSKUrejMmqpXJlKQ8awkDOQ7KbACO6a4vgLkPU2p38+PRnBJW36ckCt9YahsLYLS7bHgcY0jTYoCGuMa4lsl8aQcQLC4uHGZeQEqlKrAoHs6Ro+ZQdwHJ9eXt5cnk+/O7j99+fXt8Mr365/eHyYnj24er89MPpxe0xV68swlQnC8QLNlXCsHxWiTlTm/xzR501QCbcLBNslg01o4iBxicraXbnYwIacWG0khaoLenaqs6iuTddva7634BzH7NENtdPEIIGHVufVewI8mbNFBrKEzSyEO4WTUREfqdS1U5EyFbALdi/w3sIGu+CaNL73dX32wqjwfc4QG9fvPh8+v799eX5+fCHyw+njzLSTZTuGJo3aHgxyJUN8Kj5alAMgq8B9wGNYHjn5tSp08nkBjP0XRQxzpdj0PDy0AWYvBHA6N1qwYsHOwpN1COe084z226BhYRugalyBFbCZ8RCLgG2tha7WZeE8lTYC80aeIl801YLOkxa+8zhlQne54ywTC0U+yDLDYIsS1dNZZxl3vq5xfJpLCH9ZUfHFNdTw7vZyTFPKpaXHeyeFS7jLPu2gyYPlgdNpgtyF3GTFlvZVdik3cSqyMmk5KbBk+UA1oifXF/z+fIhlP/op8z/q5ihukV2uxCQZfEf3W4u/qPfO9j7f5/j2Tj+I8vmdbTDlwz0+LsFV/0DnrL1zwgX7zx1jNR2USBL4j86B9n4z0679Xof//Uszz7+Yx//sdv4D8U58mf/rx0EkuFBS0NB7PIOqI67My/aFzL1Cpfyq6LRQ4jIANzNR2gMlQlaudTlqToJR02CTSgNhrN5wDBXo5SdLD15iLIhjt++EqV4gSFv91vxgagrKsRoJjLv0MLMEz5j4ZsmyIuITzOQTy4vvj97DxQgkSU+PXM9sSzEYTBnF7egVVqaoHtEKJJnAiTCQHWdOQmGBM1g7Iq/w2RtrVotJuJ6JSQOfhlOfbKkpuhScjyKnP5hvq3V9VU1oT6Cm9vrs4v34oRQ8WeA4/s+6PW6A0qDFYDpow8saL1eF/T7r1uN/kGrBbp8m2h0+b9qgJQG7smxpAPOMvn8a5AeTOhjpfriI29VtxnFTH6EvBy/jr7YHgZvfa4bNMRNGmIhXwhlblteQUP8T2UUahj0EqYhlh3IxwEUc1wzLoSGODUaNMD4Dz+Uzea/TCMRm4chCoairiwhU655AshfcLWFTzY3+LuZpY2maYN5WvOAj8zcPNlBHwWDtdpwpTallcdrcy9zYSvXWnzrzmzZ7FbMMFjpUJJVXMGqu8U34Gzk+C1ZQkmPtjyNRLBzq9WiqkrRN6as6SHLXP9SUCI9lxqs4UJDzMu01RaCCh1ABTJlqTOrAtyylhSJe1u3ZylQu1X5uSxop9gnlzYrWyyPKJUBlvUQEdbg+/+WLbeF480xG+NbAXLT9qUC+q6aZ0Ms9dvZ+84Knsod+gxt3M/prbQx712Wm7gsl3CkSsdlmcZeicFmLuvAN2uWNb9gVS3zeBaAWM/L+U/SO0pkiipvaq/XtZ2oxfdKpKmVfs88p7Kdnyv7OjNsZ5nD0y6+qddzCZQ1XJ+bEceayo7lKP0qZ/93pyiY4UkYEbRDG/My/1/rdfb7z157f/7D8zx1oTNwTd2dYOa8Ei6gV86rpv7F//7bcV6hT7skif3zN3ns9S/inmlDCxgN9AnO4q0Pglmy/nu9192s/7/X6u/X/3M86iovSyQwjiKwM7wpJPpCsXyGIRu57bbbboyRH3izxn3L7botJ9mi2o62kYir+FNTx8DJyczA0VLHbKFr3aGFE3NclKFQNjKgyMGU4Uh81ysN/zo9F9JQUEbt5jynDhpmfFoDWDu8SNHHGDaAvtpTvGSvcnKc9DvigVNPTHw6TkwkpaJRt9VqtUVaVu5oH3Xcdv9QfD1bB7rxt+c3uvl1LQGcY8okqkYqEojPSEWilhPiFGZbA1V5ui1t1RZbR6xnVbe6pVQ5dXu0ZH5qKe2uD3KlYTtYadgMw/hAdVkY1+tKf5DWcTFarghDichEZWoD+bW0j6eQOwM+VEcZGEdHR0crg+gOuq3W0REfPHlc8ZMNmnnopBw4Cds8q1GmH246oLzng6TH6kaTOkiVNHnziIZ0lBCffIzrR/igdK1M4wYSnnmYZKY3nGyM6aAKU0dlCtJYo3fG2ipA2m5VdK/dWq97y1BV9K/d6nDa06xM8kCb3HiKRW48ISU3/janQrVMr9UWqbG68jif44/yaYmlq1YT7+iTcANPTgKIZ0kqZRGBE3QSQEqTRHmC/YfIFy4U6P9MMEOXoYdkFfwnX3LvMe9olk1v2OHM+tIwxBoSb1WrR4yNcaX3wNxJ0iy5IF/3uh0r+V3B2JlXaJfnXpXOh7rgMbkksg7SG5gr7k23P8vXlX5EixXqyM/a6yC9+LiqkvUpva60BJH1/XwdyDuHKyvABEF6M+sAvGRkjl5+aVHtSZ5K+f957v9rt7qvc/f/7eN/n+exDEfGQTKJpUnmhCrCVkro0v+tZXien3g9/0p84bUx8jkDRT7E6hsITG+nBPtXHLaoh2YxW1i1GSIhJAvxeX5AUQ3U+NpT9eOMy9Twkhp4z8YXEbsiSF6ZkpgApfG+2F6/oq2+k0LT53DopTNwUhxa6NchyTLVPgkjEcR5P3BsJKaG0ljZQQ18JuoUdE7FETNYqfdw+DpCKh07MeZZPKaulQVh5FlTL+dO1E71kvIRUnEtmYMtGqCMOi1Vxj6PwsmoMrnDJ0pG01ABE6VNxjOpEEYBagqp8Zk60GZgM0Im30M7p8gXt8o3g/2CMBwbcoGnbbmfbTnYrBttiROtDGBqt09N85Yum5Lk7flNEVGCnCsh6y0wiA3YIVMFZ6ZUk9cyEltGZiVDat9lOCgmMJFXRV2iwJ60qknLitDKhF5llWFRpOhSBls3FsWKrjYwVWFguHMLb8gsuAzTug7TnCVRaQUHtfWaMcIU0phVporWrIJ7mtuG5qqZVxENFUV/bhHgeb1qfGfBq3AGpzaCApLS2VXUpMvoHCOMTvy00m/hxMi6Tc9PyQy+IXmcSC/32VVtT6r6Vdpm+GiJ21aTvVLbAuTeJd90rj+S6f5IpyTWGZ6sXpJd3LbUiJ3cTEpgWLYbAchISbppGHN4mfQ9gSMMOqI+/rOMfh1QaOspINxssSoCzpbdjpD3xLnOUq7mn1mDGy9TFNYBMqY3qWqlKWYbUpaWqWpqaWmKWfV1r9vJVXs3siu9G1lqU2qPytX8mCxiM2XV2lf2Ys+kLoOSGAfFNAW5rh72et2ksGk3FCs0eTerrGJRzIAUxr4U4o9osRbAO7RIAJomRw4xfV8JYmqLzEBMmpi8rtbE1FCZAoRG8+DqwydNmObUnZoKe/pugjNsLOYC/dLGqf2zf/bP/tk/T/b8bwAAAP//7BQucQDUAAA=`
+ content, _ := base64.StdEncoding.DecodeString(base64Content)
+ return content
+}
diff --git a/server/infrastructure/gorm/mock/chart_openfl_010.go b/server/infrastructure/gorm/mock/chart_openfl_010.go
deleted file mode 100644
index c82f60e5..00000000
--- a/server/infrastructure/gorm/mock/chart_openfl_010.go
+++ /dev/null
@@ -1,78 +0,0 @@
-// Copyright 2022 VMware, Inc.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package mock
-
-import "encoding/base64"
-
-var (
- FedLCMOpenFLDirector010ChartArchiveContent = getFedLCMOpenFLDirector010ChartArchiveContent()
-
- FedLCMOpenFLEnvoy010ChartArchiveContent = getFedLCMOpenFLEnvoy010ChartArchiveContent()
-)
-
-func getFedLCMOpenFLDirector010ChartArchiveContent() []byte {
- base64Content := `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`
- content, _ := base64.StdEncoding.DecodeString(base64Content)
- return content
-}
-
-func getFedLCMOpenFLEnvoy010ChartArchiveContent() []byte {
- base64Content := `H4sIFAAAAAAA/ykAK2FIUjBjSE02THk5NWIzVjBkUzVpWlM5Nk9WVjZNV2xqYW5keVRRbz1IZWxtAOxccXPiNhbfv/0p3kBv9m5nMYYE0qNzc0OTbJfpLsmFdDs7bScV9gN0EZIryUkoy3e/kWWDbSDkkpTd6/n9kYD09PQky7/3nqSHCJGPWA35jZjVjydEandGpuzFc5LneV778DD+73le8b/XbB++aBw2263D9qHXPHrhNQ7azfYL8J5Viy0UKU3kC+/JfRUH9z9CJKQfUCoqeAduGg4Jw+XX7NKo3Xhuw/WcAJUvaahjhi68RTYF36waGAmZtIC4hcPJFPNCHCUi6aPqODWYaB2qTr0+pnoSDV1fTOtvMEBJNAbdXv37aIhvupen7phq52apoFXic8/Zn4ly7/8NYRGqZweAXe9/68grvP+HXvuwfP/3QfYtte94w3HolIyx4wBIHFOl5awDo/S1JNQBCCPGzgWj/qwDvVFf6HOJCrl2AOK25xFjA/QlatVxqgA1iHtwnFAEA/QjSfUsae8AICdDhkEHRoQpdJypCCJmAAKsSuZDIjhWhPnTml2xq4pLMl4igykMqERfC/nmt4B3lt9yVb2wA42/N91G+2u34TZydedC6g60PK9ly8l4vCpqxkWxaseCj+hYWQ2rwEWAA2SxiLRMC2Zmjgq+ZCOjEeVUzzqf+7mnlHv/NU5DRjSq+tUEWYhSuTp8OhTseP+bzcZR4f1vN5uN8v3fB1XhWIQzSccTDU2v2YQP72+JxNfQ477rVJ0qvKM+coUBRDxACXqC0A2JP8G05jUkLgM0XQ/+ahgqSVXlb984VZiJCKZkBlxoiBSCnlAFI8oQ8M7HUAPl4ItpyCjhPsIt1ZO4m0SI61ThYyJCDDWhHAj4IpyBGGX5gOhYYeNadOr129tbl8SKukKO68wyqfq73vFpf3Baa7pezP4DZ6gUSPwtohIDGM6AhCGjvoEmYOQWhAQylogBaGF0vZVUUz5+DUqMtJktpwqBQUs6jHRuolLNqMoxCA6EQ6U7gN6gAt92B73Ba6cKP/Yu3579cAk/di8uuv3L3ukAzi7g+Kx/0rvsnfUHcPYGuv2P8H2vf/IakOoJSsC7UBr9hQRqphADM18DxJwCxjsz31WIPh1RHxjh44iMEcbiBiWnfAwhyilV5kEqIDxwqsDolGoLYOuDch1nPq+/sj7gcvYYGSJT8Kq+WDjzeQ0CHFGOUMnijGuZKlBbLBI3cT4H94P1PkwBfILfIqERYLFwxC1H2YHraIgjotHxWaS0Kcn5lhOUVMd2wsi6QIZEoTtAeUN9NGKkLcrX901ni4UT+7C2ygZBaYUZA/IgVtWOd0q0P3n3kGFmOB861rXunGOJRNuHGXMna94XXEvBGEpQyRiJ74uIa7NII4XOmm4JX9eymQFarQwPHS11WjPUbmKlY2ZjxuZzI5NETOcHkurPFG7irSQfKsWBZj5/bjj8v6Mt9t9+92MfZ0rCp4UEO+x/4/CgaP+P2kel/d8Llfa/tP+Ptv+FzaOAaLIM3a4seMTI0YFPznwOWnwkU7a0GUmw51pjmQ2q4BNQHiDXcGhsxTXlQQds3XsSOlPUJO3LuhIdI55yn0XBNl/DLQg1xskyJuGvA5ANhq36n/v1/MPpXvwPMGRiNkX+tE3hHfjfOmi18vjf9BqNEv/3QiX+l/j/LPhPwlDVbxoJWJ8skWMjWq+D7yPwO4PWjhmX3baMn5zqQMMBULkNuUw4ZgvWDUAKgEmDjOaGWK7temuAVA1DJkIjlKPMtKiZdT4lPFgV2eL6kPL6kKhJobzmFwoqIQ2BcqUJY1CTEM70RPB6snrNhCtX3+lv4Pzj5duz/nn38u0/vpqvviw6v4a3wa912w5Gd1Z7MCiooVZTEyKDWhzO/Vz56rT/4ezjVb/7/vTnCtRq6R5pbSKUNvUnvYvT48uzi6s3/zrp51lCIfMs52cXlzGLFELXfJS6FhI9AfOpbsqufOL6sRKhpDdEY+0aZxkeU+pe48wwRENG/aIQu2CsiKwRtyxrXkklM7XIb4qPxC6v1QzkqgHis5L1YD7xLIpichO1U1LeM8puaO+Ub2Z5s/zKzg7OzSNbLCobe+h+992jhCf75wW5yZb+fG7RPm0aF7vp2QMsFvM5hJJyPYLKX1TdoEBchjywHzb3GYuBT6udh9zRQQUWi879bS/JONs89ol0JTf3y9OO9DQkI9EOY3VUkn9qWdzKTKRg0RTfi4hrVVyKU1N6TvSkA3U7hvqtkNcqJD7W19Z14QmtO7UFBokkOONs1gEtIyxUqmhoO76/mx06Jih1j2aWI1FQPVzDHR3HuHDfhKDUmWrTkPLxCZUbhCV8Bvu6jBKFOWA3xUby2sN7xHtNwwfAQS9cNZIY43e6FLvslszSSbQLq2CEklAqr+u9K+WeygcJ3PqAH8BUu++Zqfi0Md+xLesXG31uZ/sLpC3xX6ieuOeXpV37f0eN4vl/66DdLOO/fVAZ/5Xx3xPO/x54crRY5GLFMK6t3zSGqEkaMp4XW++MHEMVPj5u3OC4p9GbCTQow/HqYoqxKO8oj+6sAjJi2IGLiHdVlxsnTkVhyOLoi7DvpIhCtYVRmo8/KJRb6kcqbr6lNmPKa/Dy1cvlydnjD8624L8UDJ/NAOzC/3Z7Df9bzVaJ//ugEv9L/N87/ssh8V0S6YmQ9PdYvnv9tXKpWG0gXgiGOw2AQalntQAyvX1YAxLSBMltSPHTS2u1Xv7iJBFXcpl5VR2oZPQxJ0WV8N6gHKZ8ljdSWJDTj0NHgJ/WtfolA/PPHsLcg/9DygPKx083Azv9/1ajeP7vtcvzn71Qif8l/n+p+P+tBaCdZiB/se15DYJgeIEj019qEO5R3wHIWK/7Io1o+G/0dWJqbJtBfhSrrbEtRwzxZmNI/A03GuPi7C277YZjC/4nU5pcKXyiCdh5/6t5UMT/1lGZ/7UXKvG/xP+94/8S5QuY9xmB/g90sb9oyuG/O0E2pWMuJD5nHzvw3/NaxfzfQ+/AK/F/H1SNb6Qg106cavsq9gFeOa/q6Sfz95+O8wrvnnVNlPRl0Ib831rqBtbwjkzDp28E7/L/ms120f87KPd/90P5/N9MTJHL/m/YBKn+ekp/XP6hkKC/Sh6uZLKHK87W5OH11OHVof90loq7xtmD0ogzWcS15J6NU03yiatb0omrG7KJq9uSiV3byE3fD19Ms8yF9GI3W5dNL66uZRdXi8nFVeP+hESSafoFwI8CchWg8YRUB376xZaL+EcZCLsKWTSm/Mo41IIj16oD84XliS/3XaW/4SDkUuTy3iNMOVX6qsjovjfFA1N6sixMGxfUA5CEX1/dCskCRX/HDjReQzy0fI50FfIJ0mY2vrTs6D8/3Yf/z3UAuAv/W0fF+L915JX7v3uhVUCURkKbfgPC1C9vZebvVS7R3QZYYeFGZOYSZKZdFvyThiYAs3dBi8bA1q+biExG0SrOa66kJeGcs5K8ZjxM/f0WxYwhjWJXA4jtTCFs3PLjFave7UXrZPdudQE2ua2aLY+N0LLqkozT2rw5MhybLhFm7VCWZ3VjMG+NsjzJXd38T18YhtU93g2/gZF5FJvzuNqwzAdOpyNrDZZ7mjkLsfEB5yQlU58XnLEqaWXO0DxSbGqclrOTGqv/XuDTrmyUVFJJJT0L/ScAAP//dkPwfgBQAAA=`
- content, _ := base64.StdEncoding.DecodeString(base64Content)
- return content
-}
-
-const (
- DefaultEnvoyConfig = `
-shard_descriptor:
- template: dummy_shard_descriptor.FedLCMDummyShardDescriptor`
-)
-
-var DefaultEnvoyPythonConfig = map[string]string{"dummy_shard_descriptor.py": `
-from typing import Iterable
-from typing import List
-import logging
-
-from openfl.interface.interactive_api.shard_descriptor import DummyShardDescriptor
-
-logger = logging.getLogger(__name__)
-
-class FedLCMDummyShardDescriptor(DummyShardDescriptor):
- """Dummy shard descriptor class."""
-
- def __init__(self) -> None:
- """Initialize DummyShardDescriptor."""
- super().__init__(['1'], ['1'], 128)
-
- @property
- def sample_shape(self) -> List[str]:
- """Return the sample shape info."""
- return ['1']
-
- @property
- def target_shape(self) -> List[str]:
- """Return the target shape info."""
- return ['1']
-
- @property
- def dataset_description(self) -> str:
- logger.info(
- f'Sample shape: {self.sample_shape}, '
- f'target shape: {self.target_shape}'
- )
- """Return the dataset description."""
- return 'This is dummy data shard descriptor provided by FedLCM project. You should implement your own data ' \
- 'loader to load your local data for each Envoy.'
-`}
diff --git a/server/infrastructure/gorm/mock/chart_openfl_030.go b/server/infrastructure/gorm/mock/chart_openfl_030.go
new file mode 100644
index 00000000..433a5a4f
--- /dev/null
+++ b/server/infrastructure/gorm/mock/chart_openfl_030.go
@@ -0,0 +1,78 @@
+// Copyright 2022-2023 VMware, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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 (
+ FedLCMOpenFLDirector030ChartArchiveContent = getFedLCMOpenFLDirector030ChartArchiveContent()
+
+ FedLCMOpenFLEnvoy030ChartArchiveContent = getFedLCMOpenFLEnvoy030ChartArchiveContent()
+)
+
+func getFedLCMOpenFLDirector030ChartArchiveContent() []byte {
+ base64Content := `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`
+ content, _ := base64.StdEncoding.DecodeString(base64Content)
+ return content
+}
+
+func getFedLCMOpenFLEnvoy030ChartArchiveContent() []byte {
+ base64Content := `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`
+ content, _ := base64.StdEncoding.DecodeString(base64Content)
+ return content
+}
+
+const (
+ DefaultEnvoyConfig = `
+shard_descriptor:
+ template: dummy_shard_descriptor.FedLCMDummyShardDescriptor`
+)
+
+var DefaultEnvoyPythonConfig = map[string]string{"dummy_shard_descriptor.py": `
+from typing import Iterable
+from typing import List
+import logging
+
+from openfl.interface.interactive_api.shard_descriptor import DummyShardDescriptor
+
+logger = logging.getLogger(__name__)
+
+class FedLCMDummyShardDescriptor(DummyShardDescriptor):
+ """Dummy shard descriptor class."""
+
+ def __init__(self) -> None:
+ """Initialize DummyShardDescriptor."""
+ super().__init__(['1'], ['1'], 128)
+
+ @property
+ def sample_shape(self) -> List[str]:
+ """Return the sample shape info."""
+ return ['1']
+
+ @property
+ def target_shape(self) -> List[str]:
+ """Return the target shape info."""
+ return ['1']
+
+ @property
+ def dataset_description(self) -> str:
+ logger.info(
+ f'Sample shape: {self.sample_shape}, '
+ f'target shape: {self.target_shape}'
+ )
+ """Return the dataset description."""
+ return 'This is dummy data shard descriptor provided by FedLCM project. You should implement your own data ' \
+ 'loader to load your local data for each Envoy.'
+`}
diff --git a/server/infrastructure/gorm/participant_fate_repo.go b/server/infrastructure/gorm/participant_fate_repo.go
index bed39129..e929c643 100644
--- a/server/infrastructure/gorm/participant_fate_repo.go
+++ b/server/infrastructure/gorm/participant_fate_repo.go
@@ -95,7 +95,7 @@ func (r *ParticipantFATERepo) UpdateDeploymentYAMLByUUID(instance interface{}) e
func (r *ParticipantFATERepo) UpdateInfoByUUID(instance interface{}) error {
participant := instance.(*entity.ParticipantFATE)
return db.Where("uuid = ?", participant.UUID).
- Select("cluster_uuid", "status", "access_info", "cert_config", "extra_attribute", "ingress_info", "job_uuid").
+ Select("cluster_uuid", "status", "access_info", "cert_config", "extra_attribute", "ingress_info", "job_uuid", "deployment_yaml", "chart_uuid").
Updates(participant).Error
}
diff --git a/server/main.go b/server/main.go
index a304801c..a6477b9f 100644
--- a/server/main.go
+++ b/server/main.go
@@ -39,13 +39,14 @@ import (
)
// main starts the API server
-// @title lifecycle manager API service
-// @version v1
-// @description backend APIs of lifecycle manager service
-// @termsOfService http://swagger.io/terms/
-// @contact.name FedLCM team
-// @BasePath /api/v1
-// @in header
+//
+// @title lifecycle manager API service
+// @version v1
+// @description backend APIs of lifecycle manager service
+// @termsOfService http://swagger.io/terms/
+// @contact.name FedLCM team
+// @BasePath /api/v1
+// @in header
func main() {
viper.AutomaticEnv()
replacer := strings.NewReplacer(".", "_")
diff --git a/site-portal/.env b/site-portal/.env
index 0ad57b42..89e0a1d7 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 fc162028..41b70531 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}
diff --git a/site-portal/server/api/job.go b/site-portal/server/api/job.go
index dbf30ba9..951df937 100644
--- a/site-portal/server/api/job.go
+++ b/site-portal/server/api/job.go
@@ -81,14 +81,14 @@ func (controller *JobController) Route(r *gin.RouterGroup) {
}
// approveJob approves the job
-// @Summary Approve a pending job
-// @Tags Job
-// @Produce json
-// @Param uuid path string true "Job UUID"
-// @Success 200 {object} GeneralResponse{} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /job/{uuid}/approve [post]
+// @Summary Approve a pending job
+// @Tags Job
+// @Produce json
+// @Param uuid path string true "Job UUID"
+// @Success 200 {object} GeneralResponse{} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /job/{uuid}/approve [post]
func (controller *JobController) approveJob(c *gin.Context) {
if err := func() error {
jobUUID := c.Param("uuid")
@@ -108,14 +108,14 @@ func (controller *JobController) approveJob(c *gin.Context) {
}
// rejectJob rejects the job
-// @Summary Disapprove a pending job
-// @Tags Job
-// @Produce json
-// @Param uuid path string true "Job UUID"
-// @Success 200 {object} GeneralResponse{} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /job/{uuid}/reject [post]
+// @Summary Disapprove a pending job
+// @Tags Job
+// @Produce json
+// @Param uuid path string true "Job UUID"
+// @Success 200 {object} GeneralResponse{} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /job/{uuid}/reject [post]
func (controller *JobController) rejectJob(c *gin.Context) {
if err := func() error {
jobUUID := c.Param("uuid")
@@ -135,14 +135,14 @@ func (controller *JobController) rejectJob(c *gin.Context) {
}
// refreshJob retrieve the latest job status
-// @Summary Refresh the latest job status
-// @Tags Job
-// @Produce json
-// @Param uuid path string true "Job UUID"
-// @Success 200 {object} GeneralResponse{} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /job/{uuid}/refresh [post]
+// @Summary Refresh the latest job status
+// @Tags Job
+// @Produce json
+// @Param uuid path string true "Job UUID"
+// @Success 200 {object} GeneralResponse{} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /job/{uuid}/refresh [post]
func (controller *JobController) refreshJob(c *gin.Context) {
if err := func() error {
jobUUID := c.Param("uuid")
@@ -162,14 +162,14 @@ func (controller *JobController) refreshJob(c *gin.Context) {
}
// get returns detailed information of a job
-// @Summary Get job's detailed info
-// @Tags Job
-// @Produce json
-// @Param uuid path string true "Job UUID"
-// @Success 200 {object} GeneralResponse{data=service.JobDetail} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /job/{uuid} [get]
+// @Summary Get job's detailed info
+// @Tags Job
+// @Produce json
+// @Param uuid path string true "Job UUID"
+// @Success 200 {object} GeneralResponse{data=service.JobDetail} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /job/{uuid} [get]
func (controller *JobController) get(c *gin.Context) {
if job, err := func() (*service.JobDetail, error) {
jobUUID := c.Param("uuid")
@@ -190,14 +190,14 @@ func (controller *JobController) get(c *gin.Context) {
}
// generateConf returns job configuration and DSL content
-// @Summary Get a job's config template, used in json template mode
-// @Tags Job
-// @Produce json
-// @Param request body service.JobSubmissionRequest true "Job requests, not all fields are required: only need to fill related ones according to job type"
-// @Success 200 {object} GeneralResponse{data=service.JobConf} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /job/conf/create [post]
+// @Summary Get a job's config template, used in json template mode
+// @Tags Job
+// @Produce json
+// @Param request body service.JobSubmissionRequest true "Job requests, not all fields are required: only need to fill related ones according to job type"
+// @Success 200 {object} GeneralResponse{data=service.JobConf} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /job/conf/create [post]
func (controller *JobController) generateConf(c *gin.Context) {
if conf, err := func() (*service.JobConf, error) {
request := &service.JobSubmissionRequest{}
@@ -228,14 +228,14 @@ func (controller *JobController) generateConf(c *gin.Context) {
}
// createRemoteJob creates a new job
-// @Summary Create a new job that is created by other site, only called by FML manager
-// @Tags Job
-// @Produce json
-// @Param request body service.RemoteJobCreationRequest true "Job info"
-// @Success 200 {object} GeneralResponse{} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /job/internal/create [post]
+// @Summary Create a new job that is created by other site, only called by FML manager
+// @Tags Job
+// @Produce json
+// @Param request body service.RemoteJobCreationRequest true "Job info"
+// @Success 200 {object} GeneralResponse{} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /job/internal/create [post]
func (controller *JobController) createRemoteJob(c *gin.Context) {
if err := func() error {
request := &service.RemoteJobCreationRequest{}
@@ -258,15 +258,15 @@ func (controller *JobController) createRemoteJob(c *gin.Context) {
}
// handleJobResponse process job approval response
-// @Summary Handle job response from other sites, only called by FML manager
-// @Tags Job
-// @Produce json
-// @Param uuid path string true "Job UUID"
-// @Param context body service.JobApprovalContext true "Approval context, containing the sender UUID and approval status"
-// @Success 200 {object} GeneralResponse{} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /job/internal/{uuid}/response [post]
+// @Summary Handle job response from other sites, only called by FML manager
+// @Tags Job
+// @Produce json
+// @Param uuid path string true "Job UUID"
+// @Param context body service.JobApprovalContext true "Approval context, containing the sender UUID and approval status"
+// @Success 200 {object} GeneralResponse{} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /job/internal/{uuid}/response [post]
func (controller *JobController) handleJobResponse(c *gin.Context) {
if err := func() error {
jobUUID := c.Param("uuid")
@@ -290,15 +290,15 @@ func (controller *JobController) handleJobResponse(c *gin.Context) {
}
// handleJobStatusUpdate process job status update
-// @Summary Handle job status updates, only called by FML manager
-// @Tags Job
-// @Produce json
-// @Param uuid path string true "Job UUID"
-// @Param context body service.JobStatusUpdateContext true "Job status update context, containing the latest job and participant status"
-// @Success 200 {object} GeneralResponse{} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /job/internal/{uuid}/status [post]
+// @Summary Handle job status updates, only called by FML manager
+// @Tags Job
+// @Produce json
+// @Param uuid path string true "Job UUID"
+// @Param context body service.JobStatusUpdateContext true "Job status update context, containing the latest job and participant status"
+// @Success 200 {object} GeneralResponse{} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /job/internal/{uuid}/status [post]
func (controller *JobController) handleJobStatusUpdate(c *gin.Context) {
if err := func() error {
jobUUID := c.Param("uuid")
@@ -322,14 +322,14 @@ func (controller *JobController) handleJobStatusUpdate(c *gin.Context) {
}
// getPredictingJobParticipant returns participant list for creating a predicting job from a model
-// @Summary Get allowed participants for a predicting job from a model
-// @Tags Job
-// @Produce json
-// @Param modelUUID query string true "UUID of a trained model"
-// @Success 200 {object} GeneralResponse{data=[]service.JobParticipantInfoBase} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /job/predict/participant [get]
+// @Summary Get allowed participants for a predicting job from a model
+// @Tags Job
+// @Produce json
+// @Param modelUUID query string true "UUID of a trained model"
+// @Success 200 {object} GeneralResponse{data=[]service.JobParticipantInfoBase} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /job/predict/participant [get]
func (controller *JobController) getPredictingJobParticipant(c *gin.Context) {
if data, err := func() ([]service.JobParticipantInfoBase, error) {
modelUUID := c.Query("modelUUID")
@@ -350,13 +350,13 @@ func (controller *JobController) getPredictingJobParticipant(c *gin.Context) {
}
// downloadDataResult returns predict/PSI job result data
-// @Summary the result data of a Predicting or PSI job, XXX: currently it will return an error message due to a bug in FATE
-// @Tags Job
-// @Produce json
-// @Param uuid path string true "Job UUID"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /job/{uuid}/data-result/download [get]
+// @Summary the result data of a Predicting or PSI job, XXX: currently it will return an error message due to a bug in FATE
+// @Tags Job
+// @Produce json
+// @Param uuid path string true "Job UUID"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /job/{uuid}/data-result/download [get]
func (controller *JobController) downloadDataResult(c *gin.Context) {
if req, err := func() (*http.Request, error) {
jobUUID := c.Param("uuid")
@@ -378,14 +378,14 @@ func (controller *JobController) downloadDataResult(c *gin.Context) {
}
// delete a job
-// @Summary Delete the job. The job will be marked as delete in this site, but still viewable in other sites
-// @Tags Job
-// @Produce json
-// @Param uuid path string true "Job UUID"
-// @Success 200 {object} GeneralResponse{} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /job/{uuid} [delete]
+// @Summary Delete the job. The job will be marked as delete in this site, but still viewable in other sites
+// @Tags Job
+// @Produce json
+// @Param uuid path string true "Job UUID"
+// @Success 200 {object} GeneralResponse{} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /job/{uuid} [delete]
func (controller *JobController) delete(c *gin.Context) {
if err := func() error {
jobUUID := c.Param("uuid")
@@ -405,13 +405,13 @@ func (controller *JobController) delete(c *gin.Context) {
}
// getJobComponents returns all the components for a model and their default configs
-// @Summary Get all the components and their default configs. The returned format is json
-// @Tags Job
-// @Produce json
-// @Success 200 {object} GeneralResponse{data=string} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /job/components [get]
+// @Summary Get all the components and their default configs. The returned format is json
+// @Tags Job
+// @Produce json
+// @Success 200 {object} GeneralResponse{data=string} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /job/components [get]
func (controller *JobController) getJobComponents(c *gin.Context) {
resp := &GeneralResponse{
Code: constants.RespNoErr,
@@ -421,14 +421,14 @@ func (controller *JobController) getJobComponents(c *gin.Context) {
}
// generateDslFromDag returns the DSL json file from the DAG the user draw
-// @Summary Generate the DSL json file from the DAG the user draw, should be called by UI only
-// @Tags Job
-// @Produce json
-// @Param rawJson body service.JobRawDagJson true "The raw json, the value should be a serialized json string"
-// @Success 200 {object} GeneralResponse{data=string} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /job/generateDslFromDag [post]
+// @Summary Generate the DSL json file from the DAG the user draw, should be called by UI only
+// @Tags Job
+// @Produce json
+// @Param rawJson body service.JobRawDagJson true "The raw json, the value should be a serialized json string"
+// @Success 200 {object} GeneralResponse{data=string} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /job/generateDslFromDag [post]
func (controller *JobController) generateDslFromDag(c *gin.Context) {
if rawJson, err := func() (string, error) {
request := &service.JobRawDagJson{}
@@ -461,14 +461,14 @@ func (controller *JobController) generateDslFromDag(c *gin.Context) {
}
// generateConfFromDag returns the conf json file from the DAG the user draw
-// @Summary Generate the conf json file from the DAG the user draw, the conf file can be consumed by Fateflow
-// @Tags Job
-// @Produce json
-// @Param generateJobConfRequest body service.GenerateJobConfRequest true "The request for generate the conf json file"
-// @Success 200 {object} GeneralResponse{data=string} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /job/generateConfFromDag [post]
+// @Summary Generate the conf json file from the DAG the user draw, the conf file can be consumed by Fateflow
+// @Tags Job
+// @Produce json
+// @Param generateJobConfRequest body service.GenerateJobConfRequest true "The request for generate the conf json file"
+// @Success 200 {object} GeneralResponse{data=string} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /job/generateConfFromDag [post]
func (controller *JobController) generateConfFromDag(c *gin.Context) {
if generateJobConfRequest, err := func() (*service.GenerateJobConfRequest, error) {
request := &service.GenerateJobConfRequest{}
diff --git a/site-portal/server/api/local_data.go b/site-portal/server/api/local_data.go
index 58808786..7eedd20f 100644
--- a/site-portal/server/api/local_data.go
+++ b/site-portal/server/api/local_data.go
@@ -62,16 +62,16 @@ func (controller *LocalDataController) Route(r *gin.RouterGroup) {
}
// upload uploads a local csv data
-// @Summary Upload a local csv data
-// @Tags LocalData
-// @Produce json
-// @Param file formData file true "The csv file"
-// @Param name formData string true "Data name"
-// @Param description formData string true "Data description"
-// @Success 200 {object} GeneralResponse{} "Success, the data field is the data UUID"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /data [post]
+// @Summary Upload a local csv data
+// @Tags LocalData
+// @Produce json
+// @Param file formData file true "The csv file"
+// @Param name formData string true "Data name"
+// @Param description formData string true "Data description"
+// @Success 200 {object} GeneralResponse{} "Success, the data field is the data UUID"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /data [post]
func (controller *LocalDataController) upload(c *gin.Context) {
if uuid, err := func() (string, error) {
f, err := c.FormFile("file")
@@ -100,13 +100,13 @@ func (controller *LocalDataController) upload(c *gin.Context) {
}
// list returns all data records
-// @Summary List all data records
-// @Tags LocalData
-// @Produce json
-// @Success 200 {object} GeneralResponse{data=[]service.LocalDataListItem} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /data [get]
+// @Summary List all data records
+// @Tags LocalData
+// @Produce json
+// @Success 200 {object} GeneralResponse{data=[]service.LocalDataListItem} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /data [get]
func (controller *LocalDataController) list(c *gin.Context) {
if dataList, err := controller.localDataApp.List(); err != nil {
resp := &GeneralResponse{
@@ -124,14 +124,14 @@ func (controller *LocalDataController) list(c *gin.Context) {
}
// get returns detailed information of a data record
-// @Summary Get data record's detailed info
-// @Tags LocalData
-// @Produce json
-// @Param uuid path string true "Data UUID"
-// @Success 200 {object} GeneralResponse{data=service.LocalDataDetail} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /data/{uuid} [get]
+// @Summary Get data record's detailed info
+// @Tags LocalData
+// @Produce json
+// @Param uuid path string true "Data UUID"
+// @Success 200 {object} GeneralResponse{data=service.LocalDataDetail} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /data/{uuid} [get]
func (controller *LocalDataController) get(c *gin.Context) {
uuid := c.Param("uuid")
if dataDetail, err := controller.localDataApp.Get(uuid); err != nil {
@@ -150,13 +150,13 @@ func (controller *LocalDataController) get(c *gin.Context) {
}
// download returns the original csv data file
-// @Summary Download data file
-// @Tags LocalData
-// @Produce json
-// @Param uuid path string true "Data UUID"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /data/{uuid}/file [get]
+// @Summary Download data file
+// @Tags LocalData
+// @Produce json
+// @Param uuid path string true "Data UUID"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /data/{uuid}/file [get]
func (controller *LocalDataController) download(c *gin.Context) {
uuid := c.Param("uuid")
if path, err := controller.localDataApp.GetFilePath(uuid); err != nil {
@@ -181,14 +181,14 @@ func (controller *LocalDataController) download(c *gin.Context) {
}
// delete removes the data
-// @Summary Delete the data file, both the local copy and the FATE table
-// @Tags LocalData
-// @Produce json
-// @Param uuid path string true "Data UUID"
-// @Success 200 {object} GeneralResponse "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /data/{uuid} [delete]
+// @Summary Delete the data file, both the local copy and the FATE table
+// @Tags LocalData
+// @Produce json
+// @Param uuid path string true "Data UUID"
+// @Success 200 {object} GeneralResponse "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /data/{uuid} [delete]
func (controller *LocalDataController) delete(c *gin.Context) {
uuid := c.Param("uuid")
if err := controller.localDataApp.DeleteData(uuid); err != nil {
@@ -206,15 +206,15 @@ func (controller *LocalDataController) delete(c *gin.Context) {
}
// putIdMetaInfo update data record ID meta info
-// @Summary Update data record's ID meta info
-// @Tags LocalData
-// @Produce json
-// @Param info body service.LocalDataIDMetaInfoUpdateRequest true "The meta info"
-// @Param uuid path string true "Data UUID"
-// @Success 200 {object} GeneralResponse "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /data/{uuid}/idmetainfo [put]
+// @Summary Update data record's ID meta info
+// @Tags LocalData
+// @Produce json
+// @Param info body service.LocalDataIDMetaInfoUpdateRequest true "The meta info"
+// @Param uuid path string true "Data UUID"
+// @Success 200 {object} GeneralResponse "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /data/{uuid}/idmetainfo [put]
func (controller *LocalDataController) putIdMetaInfo(c *gin.Context) {
if err := func() error {
uuid := c.Param("uuid")
@@ -241,14 +241,14 @@ func (controller *LocalDataController) putIdMetaInfo(c *gin.Context) {
}
// getColumns returns a list of the data's headers
-// @Summary Get data headers
-// @Tags LocalData
-// @Produce json
-// @Param uuid path string true "Data UUID"
-// @Success 200 {object} GeneralResponse{data=[]string} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /data/{uuid}/columns [get]
+// @Summary Get data headers
+// @Tags LocalData
+// @Produce json
+// @Param uuid path string true "Data UUID"
+// @Success 200 {object} GeneralResponse{data=[]string} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /data/{uuid}/columns [get]
func (controller *LocalDataController) getColumns(c *gin.Context) {
uuid := c.Param("uuid")
if columns, err := controller.localDataApp.GetColumns(uuid); err != nil {
@@ -267,14 +267,14 @@ func (controller *LocalDataController) getColumns(c *gin.Context) {
}
// associate creates a local data item with existing flow data table
-// @Summary Associate flow data table to a local data
-// @Tags LocalData
-// @Produce json
-// @Param project body service.LocalDataAssociateRequest true "Local data association request"
-// @Success 200 {object} GeneralResponse{} "Success, the data field is the data UUID"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /data/associate [post]
+// @Summary Associate flow data table to a local data
+// @Tags LocalData
+// @Produce json
+// @Param project body service.LocalDataAssociateRequest true "Local data association request"
+// @Success 200 {object} GeneralResponse{} "Success, the data field is the data UUID"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /data/associate [post]
func (controller *LocalDataController) associate(c *gin.Context) {
if uuid, err := func() (string, error) {
request := &service.LocalDataAssociateRequest{}
diff --git a/site-portal/server/api/model.go b/site-portal/server/api/model.go
index bf53a451..04a15ba4 100644
--- a/site-portal/server/api/model.go
+++ b/site-portal/server/api/model.go
@@ -67,13 +67,13 @@ func (controller *ModelController) Route(r *gin.RouterGroup) {
}
// list returns a list of models
-// @Summary Get model list
-// @Tags Model
-// @Produce json
-// @Success 200 {object} GeneralResponse{data=[]service.ModelListItem} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /model [get]
+// @Summary Get model list
+// @Tags Model
+// @Produce json
+// @Success 200 {object} GeneralResponse{data=[]service.ModelListItem} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /model [get]
func (controller *ModelController) list(c *gin.Context) {
if data, err := func() ([]service.ModelListItem, error) {
return controller.modelApp.List("")
@@ -93,14 +93,14 @@ func (controller *ModelController) list(c *gin.Context) {
}
// get returns detailed information of a model
-// @Summary Get model's detailed info
-// @Tags Model
-// @Produce json
-// @Param uuid path string true "Model UUID"
-// @Success 200 {object} GeneralResponse{data=service.ModelDetail} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /model/{uuid} [get]
+// @Summary Get model's detailed info
+// @Tags Model
+// @Produce json
+// @Param uuid path string true "Model UUID"
+// @Success 200 {object} GeneralResponse{data=service.ModelDetail} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /model/{uuid} [get]
func (controller *ModelController) get(c *gin.Context) {
if data, err := func() (*service.ModelDetail, error) {
modelUUID := c.Param("uuid")
@@ -121,14 +121,14 @@ func (controller *ModelController) get(c *gin.Context) {
}
// delete deletes the specified model
-// @Summary Delete the model
-// @Tags Model
-// @Produce json
-// @Param uuid path string true "Model UUID"
-// @Success 200 {object} GeneralResponse "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /model/{uuid} [delete]
+// @Summary Delete the model
+// @Tags Model
+// @Produce json
+// @Param uuid path string true "Model UUID"
+// @Success 200 {object} GeneralResponse "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /model/{uuid} [delete]
func (controller *ModelController) delete(c *gin.Context) {
modelUUID := c.Param("uuid")
if err := controller.modelApp.Delete(modelUUID); err != nil {
@@ -146,14 +146,14 @@ func (controller *ModelController) delete(c *gin.Context) {
}
// create process model creation event
-// @Summary Handle model creation event, called by the job context only
-// @Tags Model
-// @Produce json
-// @Param request body service.ModelCreationRequest true "Creation Request"
-// @Success 200 {object} GeneralResponse{} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /model/internal/event/create [post]
+// @Summary Handle model creation event, called by the job context only
+// @Tags Model
+// @Produce json
+// @Param request body service.ModelCreationRequest true "Creation Request"
+// @Success 200 {object} GeneralResponse{} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /model/internal/event/create [post]
func (controller *ModelController) create(c *gin.Context) {
if err := func() error {
request := &service.ModelCreationRequest{}
@@ -176,15 +176,15 @@ func (controller *ModelController) create(c *gin.Context) {
}
// deployModel publish the model to online serving system
-// @Summary Publish model to online serving system
-// @Tags Model
-// @Produce json
-// @Param uuid path string true "Model UUID"
-// @Param request body service.ModelDeploymentRequest true "Creation Request"
-// @Success 200 {object} GeneralResponse{data=entity.ModelDeployment} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /model/{uuid}/publish [post]
+// @Summary Publish model to online serving system
+// @Tags Model
+// @Produce json
+// @Param uuid path string true "Model UUID"
+// @Param request body service.ModelDeploymentRequest true "Creation Request"
+// @Success 200 {object} GeneralResponse{data=entity.ModelDeployment} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /model/{uuid}/publish [post]
func (controller *ModelController) deployModel(c *gin.Context) {
if deployment, err := func() (*entity.ModelDeployment, error) {
modelUUID := c.Param("uuid")
@@ -210,14 +210,14 @@ func (controller *ModelController) deployModel(c *gin.Context) {
}
// getSupportedDeployments returns list of the deployment type this model can use
-// @Summary Get list of deployment types (KFServing, FATE-Serving, etc.) this model can use
-// @Tags Model
-// @Produce json
-// @Param uuid path string true "Model UUID"
-// @Success 200 {object} GeneralResponse{data=[]entity.ModelDeploymentType} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /model/{uuid}/supportedDeploymentTypes [get]
+// @Summary Get list of deployment types (KFServing, FATE-Serving, etc.) this model can use
+// @Tags Model
+// @Produce json
+// @Param uuid path string true "Model UUID"
+// @Success 200 {object} GeneralResponse{data=[]entity.ModelDeploymentType} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /model/{uuid}/supportedDeploymentTypes [get]
func (controller *ModelController) getSupportedDeployments(c *gin.Context) {
if types, err := func() ([]entity.ModelDeploymentType, error) {
modelUUID := c.Param("uuid")
diff --git a/site-portal/server/api/project.go b/site-portal/server/api/project.go
index 93dcb7ac..6bcbae98 100644
--- a/site-portal/server/api/project.go
+++ b/site-portal/server/api/project.go
@@ -130,14 +130,14 @@ func (controller *ProjectController) Route(r *gin.RouterGroup) {
}
// create Create a new local project
-// @Summary Create a new project
-// @Tags Project
-// @Produce json
-// @Param project body service.ProjectCreationRequest true "Basic project info"
-// @Success 200 {object} GeneralResponse{} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /project [post]
+// @Summary Create a new project
+// @Tags Project
+// @Produce json
+// @Param project body service.ProjectCreationRequest true "Basic project info"
+// @Success 200 {object} GeneralResponse{} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /project [post]
func (controller *ProjectController) create(c *gin.Context) {
if err := func() error {
claims := jwt.ExtractClaims(c)
@@ -166,13 +166,13 @@ func (controller *ProjectController) create(c *gin.Context) {
}
// list returns all projects
-// @Summary List all project
-// @Tags Project
-// @Produce json
-// @Success 200 {object} GeneralResponse{data=service.ProjectList} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /project [get]
+// @Summary List all project
+// @Tags Project
+// @Produce json
+// @Success 200 {object} GeneralResponse{data=service.ProjectList} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /project [get]
func (controller *ProjectController) list(c *gin.Context) {
if projectList, err := controller.projectApp.List(); err != nil {
resp := &GeneralResponse{
@@ -190,15 +190,15 @@ func (controller *ProjectController) list(c *gin.Context) {
}
// listParticipants returns all participants
-// @Summary List participants
-// @Tags Project
-// @Produce json
-// @Param uuid path string true "project UUID"
-// @Param all query bool false "if set to true, returns all sites, including not joined ones"
-// @Success 200 {object} GeneralResponse{data=[]service.ProjectParticipant} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /project/{uuid}/participant [get]
+// @Summary List participants
+// @Tags Project
+// @Produce json
+// @Param uuid path string true "project UUID"
+// @Param all query bool false "if set to true, returns all sites, including not joined ones"
+// @Success 200 {object} GeneralResponse{data=[]service.ProjectParticipant} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /project/{uuid}/participant [get]
func (controller *ProjectController) listParticipants(c *gin.Context) {
queryAll, err := strconv.ParseBool(c.DefaultQuery("all", "false"))
if err != nil {
@@ -221,15 +221,15 @@ func (controller *ProjectController) listParticipants(c *gin.Context) {
}
// inviteParticipant sends project invitation to other projects
-// @Summary Invite other site to this project
-// @Tags Project
-// @Produce json
-// @Param uuid path string true "project UUID"
-// @Param info body service.ProjectParticipantBase true "target site information"
-// @Success 200 {object} GeneralResponse{} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /project/{uuid}/invitation [post]
+// @Summary Invite other site to this project
+// @Tags Project
+// @Produce json
+// @Param uuid path string true "project UUID"
+// @Param info body service.ProjectParticipantBase true "target site information"
+// @Success 200 {object} GeneralResponse{} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /project/{uuid}/invitation [post]
func (controller *ProjectController) inviteParticipant(c *gin.Context) {
if err := func() error {
projectUUID := c.Param("uuid")
@@ -253,14 +253,14 @@ func (controller *ProjectController) inviteParticipant(c *gin.Context) {
}
// handleInvitation process a project invitation
-// @Summary Process project invitation, called by FML manager only
-// @Tags Project
-// @Produce json
-// @Param invitation body service.ProjectInvitationRequest true "invitation request"
-// @Success 200 {object} GeneralResponse{} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /project/internal/invitation [post]
+// @Summary Process project invitation, called by FML manager only
+// @Tags Project
+// @Produce json
+// @Param invitation body service.ProjectInvitationRequest true "invitation request"
+// @Success 200 {object} GeneralResponse{} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /project/internal/invitation [post]
func (controller *ProjectController) handleInvitation(c *gin.Context) {
if err := func() error {
invitationRequest := &service.ProjectInvitationRequest{}
@@ -283,15 +283,15 @@ func (controller *ProjectController) handleInvitation(c *gin.Context) {
}
// toggleAutoApproval changes project auto-approval status
-// @Summary Change a project's auto-approval status
-// @Tags Project
-// @Produce json
-// @Param status body service.ProjectAutoApprovalStatus true "The auto-approval status, only an 'enabled' field"
-// @Param uuid path string true "Project UUID"
-// @Success 200 {object} GeneralResponse "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /project/{uuid}/autoapprovalstatus [put]
+// @Summary Change a project's auto-approval status
+// @Tags Project
+// @Produce json
+// @Param status body service.ProjectAutoApprovalStatus true "The auto-approval status, only an 'enabled' field"
+// @Param uuid path string true "Project UUID"
+// @Success 200 {object} GeneralResponse "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /project/{uuid}/autoapprovalstatus [put]
func (controller *ProjectController) toggleAutoApproval(c *gin.Context) {
if err := func() error {
projectUUID := c.Param("uuid")
@@ -315,14 +315,14 @@ func (controller *ProjectController) toggleAutoApproval(c *gin.Context) {
}
// get returns detailed information of a project
-// @Summary Get project's detailed info
-// @Tags Project
-// @Produce json
-// @Param uuid path string true "Project UUID"
-// @Success 200 {object} GeneralResponse{data=service.ProjectInfo} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /project/{uuid} [get]
+// @Summary Get project's detailed info
+// @Tags Project
+// @Produce json
+// @Param uuid path string true "Project UUID"
+// @Success 200 {object} GeneralResponse{data=service.ProjectInfo} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /project/{uuid} [get]
func (controller *ProjectController) get(c *gin.Context) {
if project, err := func() (*service.ProjectInfo, error) {
projectUUID := c.Param("uuid")
@@ -343,14 +343,14 @@ func (controller *ProjectController) get(c *gin.Context) {
}
// joinProject joins a project
-// @Summary Join a pending/invited project
-// @Tags Project
-// @Produce json
-// @Param uuid path string true "Project UUID"
-// @Success 200 {object} GeneralResponse{} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /project/{uuid}/join [post]
+// @Summary Join a pending/invited project
+// @Tags Project
+// @Produce json
+// @Param uuid path string true "Project UUID"
+// @Success 200 {object} GeneralResponse{} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /project/{uuid}/join [post]
func (controller *ProjectController) joinProject(c *gin.Context) {
if err := func() error {
projectUUID := c.Param("uuid")
@@ -370,14 +370,14 @@ func (controller *ProjectController) joinProject(c *gin.Context) {
}
// rejectProject joins a project
-// @Summary Reject to join a pending/invited project
-// @Tags Project
-// @Produce json
-// @Param uuid path string true "Project UUID"
-// @Success 200 {object} GeneralResponse{} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /project/{uuid}/reject [post]
+// @Summary Reject to join a pending/invited project
+// @Tags Project
+// @Produce json
+// @Param uuid path string true "Project UUID"
+// @Success 200 {object} GeneralResponse{} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /project/{uuid}/reject [post]
func (controller *ProjectController) rejectProject(c *gin.Context) {
if err := func() error {
projectUUID := c.Param("uuid")
@@ -397,14 +397,14 @@ func (controller *ProjectController) rejectProject(c *gin.Context) {
}
// leaveProject leave the specified project
-// @Summary Leave the joined project created by other site
-// @Tags Project
-// @Produce json
-// @Param uuid path string true "Project UUID"
-// @Success 200 {object} GeneralResponse{} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /project/{uuid}/leave [post]
+// @Summary Leave the joined project created by other site
+// @Tags Project
+// @Produce json
+// @Param uuid path string true "Project UUID"
+// @Success 200 {object} GeneralResponse{} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /project/{uuid}/leave [post]
func (controller *ProjectController) leaveProject(c *gin.Context) {
if err := func() error {
projectUUID := c.Param("uuid")
@@ -424,14 +424,14 @@ func (controller *ProjectController) leaveProject(c *gin.Context) {
}
// closeProject close the specified project
-// @Summary Close the managed project, only available to project managing site
-// @Tags Project
-// @Produce json
-// @Param uuid path string true "Project UUID"
-// @Success 200 {object} GeneralResponse{} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /project/{uuid}/close [post]
+// @Summary Close the managed project, only available to project managing site
+// @Tags Project
+// @Produce json
+// @Param uuid path string true "Project UUID"
+// @Success 200 {object} GeneralResponse{} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /project/{uuid}/close [post]
func (controller *ProjectController) closeProject(c *gin.Context) {
if err := func() error {
projectUUID := c.Param("uuid")
@@ -451,14 +451,14 @@ func (controller *ProjectController) closeProject(c *gin.Context) {
}
// handleInvitationAcceptance process a project invitation acceptance
-// @Summary Process invitation acceptance response, called by FML manager only
-// @Tags Project
-// @Produce json
-// @Param uuid path string true "Invitation UUID"
-// @Success 200 {object} GeneralResponse{} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /project/internal/invitation/{uuid}/accept [post]
+// @Summary Process invitation acceptance response, called by FML manager only
+// @Tags Project
+// @Produce json
+// @Param uuid path string true "Invitation UUID"
+// @Success 200 {object} GeneralResponse{} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /project/internal/invitation/{uuid}/accept [post]
func (controller *ProjectController) handleInvitationAcceptance(c *gin.Context) {
if err := func() error {
invitationUUID := c.Param("uuid")
@@ -478,14 +478,14 @@ func (controller *ProjectController) handleInvitationAcceptance(c *gin.Context)
}
// handleInvitationRejection process a project invitation rejection
-// @Summary Process invitation rejection response, called by FML manager only
-// @Tags Project
-// @Produce json
-// @Param uuid path string true "Invitation UUID"
-// @Success 200 {object} GeneralResponse{} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /project/internal/invitation/{uuid}/reject [post]
+// @Summary Process invitation rejection response, called by FML manager only
+// @Tags Project
+// @Produce json
+// @Param uuid path string true "Invitation UUID"
+// @Success 200 {object} GeneralResponse{} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /project/internal/invitation/{uuid}/reject [post]
func (controller *ProjectController) handleInvitationRejection(c *gin.Context) {
if err := func() error {
invitationUUID := c.Param("uuid")
@@ -505,14 +505,14 @@ func (controller *ProjectController) handleInvitationRejection(c *gin.Context) {
}
// handleInvitationRevocation process a project invitation revocation
-// @Summary Process invitation revocation request, called by FML manager only
-// @Tags Project
-// @Produce json
-// @Param uuid path string true "Invitation UUID"
-// @Success 200 {object} GeneralResponse{} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /project/internal/invitation/{uuid}/revoke [post]
+// @Summary Process invitation revocation request, called by FML manager only
+// @Tags Project
+// @Produce json
+// @Param uuid path string true "Invitation UUID"
+// @Success 200 {object} GeneralResponse{} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /project/internal/invitation/{uuid}/revoke [post]
func (controller *ProjectController) handleInvitationRevocation(c *gin.Context) {
if err := func() error {
invitationUUID := c.Param("uuid")
@@ -532,15 +532,15 @@ func (controller *ProjectController) handleInvitationRevocation(c *gin.Context)
}
// handleParticipantLeaving process a project participant removal event
-// @Summary Process participant leaving request, called by FML manager only
-// @Tags Project
-// @Produce json
-// @Param uuid path string true "Project UUID"
-// @Param siteUUID path string true "Site UUID"
-// @Success 200 {object} GeneralResponse{} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /project/internal/{uuid}/participant/{siteUUID}/leave [post]
+// @Summary Process participant leaving request, called by FML manager only
+// @Tags Project
+// @Produce json
+// @Param uuid path string true "Project UUID"
+// @Param siteUUID path string true "Site UUID"
+// @Success 200 {object} GeneralResponse{} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /project/internal/{uuid}/participant/{siteUUID}/leave [post]
func (controller *ProjectController) handleParticipantLeaving(c *gin.Context) {
if err := func() error {
projectUUID := c.Param("uuid")
@@ -561,15 +561,15 @@ func (controller *ProjectController) handleParticipantLeaving(c *gin.Context) {
}
// handleParticipantDismissal process a project participant dismissal event
-// @Summary Process participant dismissal event, called by FML manager only
-// @Tags Project
-// @Produce json
-// @Param uuid path string true "Project UUID"
-// @Param siteUUID path string true "Site UUID"
-// @Success 200 {object} GeneralResponse{} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /project/internal/{uuid}/participant/{siteUUID}/dismiss [post]
+// @Summary Process participant dismissal event, called by FML manager only
+// @Tags Project
+// @Produce json
+// @Param uuid path string true "Project UUID"
+// @Param siteUUID path string true "Site UUID"
+// @Success 200 {object} GeneralResponse{} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /project/internal/{uuid}/participant/{siteUUID}/dismiss [post]
func (controller *ProjectController) handleParticipantDismissal(c *gin.Context) {
if err := func() error {
projectUUID := c.Param("uuid")
@@ -590,15 +590,15 @@ func (controller *ProjectController) handleParticipantDismissal(c *gin.Context)
}
// createParticipants receive participants info from FML manager
-// @Summary Create joined participants for a project, called by FML manager only
-// @Tags Project
-// @Produce json
-// @Param uuid path string true "Project UUID"
-// @Param participantList body []entity.ProjectParticipant true "participants list"
-// @Success 200 {object} GeneralResponse{} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /project/internal/{uuid}/participants [post]
+// @Summary Create joined participants for a project, called by FML manager only
+// @Tags Project
+// @Produce json
+// @Param uuid path string true "Project UUID"
+// @Param participantList body []entity.ProjectParticipant true "participants list"
+// @Success 200 {object} GeneralResponse{} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /project/internal/{uuid}/participants [post]
func (controller *ProjectController) createParticipants(c *gin.Context) {
if err := func() error {
projectUUID := c.Param("uuid")
@@ -622,15 +622,15 @@ func (controller *ProjectController) createParticipants(c *gin.Context) {
}
// removeParticipant remove pending or joined participant
-// @Summary Remove pending participant (revoke invitation) or dismiss joined participant
-// @Tags Project
-// @Produce json
-// @Param uuid path string true "Project UUID"
-// @Param participantUUID path string true "Participant UUID"
-// @Success 200 {object} GeneralResponse{} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /project/{uuid}/participant/{participantUUID} [delete]
+// @Summary Remove pending participant (revoke invitation) or dismiss joined participant
+// @Tags Project
+// @Produce json
+// @Param uuid path string true "Project UUID"
+// @Param participantUUID path string true "Participant UUID"
+// @Success 200 {object} GeneralResponse{} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /project/{uuid}/participant/{participantUUID} [delete]
func (controller *ProjectController) removeParticipant(c *gin.Context) {
if err := func() error {
projectUUID := c.Param("uuid")
@@ -651,14 +651,14 @@ func (controller *ProjectController) removeParticipant(c *gin.Context) {
}
// handleParticipantInfoUpdate process a participant info update event
-// @Summary Process participant info update event, called by the FML manager only
-// @Tags Project
-// @Produce json
-// @Param participant body service.ProjectParticipantBase true "Updated participant info"
-// @Success 200 {object} GeneralResponse{} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /project/internal/event/participant/update [post]
+// @Summary Process participant info update event, called by the FML manager only
+// @Tags Project
+// @Produce json
+// @Param participant body service.ProjectParticipantBase true "Updated participant info"
+// @Success 200 {object} GeneralResponse{} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /project/internal/event/participant/update [post]
func (controller *ProjectController) handleParticipantInfoUpdate(c *gin.Context) {
if err := func() error {
var participant service.ProjectParticipantBase
@@ -681,14 +681,14 @@ func (controller *ProjectController) handleParticipantInfoUpdate(c *gin.Context)
}
// listLocalAvailableData returns a list of local data that can be associated to current project
-// @Summary Get available local data for this project
-// @Tags Project
-// @Produce json
-// @Param uuid path string true "Project UUID"
-// @Success 200 {object} GeneralResponse{data=[]service.ProjectData} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /project/{uuid}/data/local [get]
+// @Summary Get available local data for this project
+// @Tags Project
+// @Produce json
+// @Param uuid path string true "Project UUID"
+// @Success 200 {object} GeneralResponse{data=[]service.ProjectData} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /project/{uuid}/data/local [get]
func (controller *ProjectController) listLocalAvailableData(c *gin.Context) {
if data, err := func() ([]service.ProjectData, error) {
projectUUID := c.Param("uuid")
@@ -709,15 +709,15 @@ func (controller *ProjectController) listLocalAvailableData(c *gin.Context) {
}
// listData returns a list of associated data of current project
-// @Summary Get associated data list for this project
-// @Tags Project
-// @Produce json
-// @Param uuid path string true "Project UUID"
-// @Param participant query string false "participant uuid, i.e. the providing site uuid; if set, only returns the associated data of the specified participant"
-// @Success 200 {object} GeneralResponse{data=[]service.ProjectData} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /project/{uuid}/data [get]
+// @Summary Get associated data list for this project
+// @Tags Project
+// @Produce json
+// @Param uuid path string true "Project UUID"
+// @Param participant query string false "participant uuid, i.e. the providing site uuid; if set, only returns the associated data of the specified participant"
+// @Success 200 {object} GeneralResponse{data=[]service.ProjectData} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /project/{uuid}/data [get]
func (controller *ProjectController) listData(c *gin.Context) {
if data, err := func() ([]service.ProjectData, error) {
projectUUID := c.Param("uuid")
@@ -739,15 +739,15 @@ func (controller *ProjectController) listData(c *gin.Context) {
}
// addData associate local data
-// @Summary Associate local data to current project
-// @Tags Project
-// @Produce json
-// @Param uuid path string true "project UUID"
-// @Param request body service.ProjectDataAssociationRequest true "Local data info"
-// @Success 200 {object} GeneralResponse{} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /project/{uuid}/data [post]
+// @Summary Associate local data to current project
+// @Tags Project
+// @Produce json
+// @Param uuid path string true "project UUID"
+// @Param request body service.ProjectDataAssociationRequest true "Local data info"
+// @Success 200 {object} GeneralResponse{} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /project/{uuid}/data [post]
func (controller *ProjectController) addData(c *gin.Context) {
if err := func() error {
projectUUID := c.Param("uuid")
@@ -774,15 +774,15 @@ func (controller *ProjectController) addData(c *gin.Context) {
}
// removeData dismiss data association
-// @Summary Remove associated data from the current project
-// @Tags Project
-// @Produce json
-// @Param uuid path string true "Project UUID"
-// @Param dataUUID path string true "Data UUID"
-// @Success 200 {object} GeneralResponse{} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /project/{uuid}/data/{dataUUID} [delete]
+// @Summary Remove associated data from the current project
+// @Tags Project
+// @Produce json
+// @Param uuid path string true "Project UUID"
+// @Param dataUUID path string true "Data UUID"
+// @Success 200 {object} GeneralResponse{} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /project/{uuid}/data/{dataUUID} [delete]
func (controller *ProjectController) removeData(c *gin.Context) {
if err := func() error {
projectUUID := c.Param("uuid")
@@ -806,15 +806,15 @@ func (controller *ProjectController) removeData(c *gin.Context) {
}
// handleRemoteDataAssociation associate remote data
-// @Summary Add associated remote data to current project, called by FML manager only
-// @Tags Project
-// @Produce json
-// @Param uuid path string true "project UUID"
-// @Param data body []entity.ProjectData true "Remote data information"
-// @Success 200 {object} GeneralResponse{} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /project/internal/{uuid}/data/associate [post]
+// @Summary Add associated remote data to current project, called by FML manager only
+// @Tags Project
+// @Produce json
+// @Param uuid path string true "project UUID"
+// @Param data body []entity.ProjectData true "Remote data information"
+// @Success 200 {object} GeneralResponse{} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /project/internal/{uuid}/data/associate [post]
func (controller *ProjectController) handleRemoteDataAssociation(c *gin.Context) {
if err := func() error {
projectUUID := c.Param("uuid")
@@ -841,15 +841,15 @@ func (controller *ProjectController) handleRemoteDataAssociation(c *gin.Context)
}
// handleRemoteDataDismissal dismiss remote data
-// @Summary Dismiss associated remote data from current project, called by FML manager only
-// @Tags Project
-// @Produce json
-// @Param uuid path string true "project UUID"
-// @Param data body []string true "Data UUID list"
-// @Success 200 {object} GeneralResponse{} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /project/internal/{uuid}/data/dismiss [post]
+// @Summary Dismiss associated remote data from current project, called by FML manager only
+// @Tags Project
+// @Produce json
+// @Param uuid path string true "project UUID"
+// @Param data body []string true "Data UUID list"
+// @Success 200 {object} GeneralResponse{} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /project/internal/{uuid}/data/dismiss [post]
func (controller *ProjectController) handleRemoteDataDismissal(c *gin.Context) {
if err := func() error {
projectUUID := c.Param("uuid")
@@ -876,14 +876,14 @@ func (controller *ProjectController) handleRemoteDataDismissal(c *gin.Context) {
}
// listJob returns a list of jobs in the current project
-// @Summary Get job list for this project
-// @Tags Project
-// @Produce json
-// @Param uuid path string true "Project UUID"
-// @Success 200 {object} GeneralResponse{data=[]service.JobListItemBase} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /project/{uuid}/job [get]
+// @Summary Get job list for this project
+// @Tags Project
+// @Produce json
+// @Param uuid path string true "Project UUID"
+// @Success 200 {object} GeneralResponse{data=[]service.JobListItemBase} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /project/{uuid}/job [get]
func (controller *ProjectController) listJob(c *gin.Context) {
if data, err := func() ([]service.JobListItemBase, error) {
projectUUID := c.Param("uuid")
@@ -904,15 +904,15 @@ func (controller *ProjectController) listJob(c *gin.Context) {
}
// submitJob create new job
-// @Summary Create a new job
-// @Tags Project
-// @Produce json
-// @Param uuid path string true "project UUID"
-// @Param request body service.JobSubmissionRequest true "Job requests, only fill related field according to job type"
-// @Success 200 {object} GeneralResponse{data=service.JobListItemBase} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /project/{uuid}/job [post]
+// @Summary Create a new job
+// @Tags Project
+// @Produce json
+// @Param uuid path string true "project UUID"
+// @Param request body service.JobSubmissionRequest true "Job requests, only fill related field according to job type"
+// @Success 200 {object} GeneralResponse{data=service.JobListItemBase} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /project/{uuid}/job [post]
func (controller *ProjectController) submitJob(c *gin.Context) {
if job, err := func() (*service.JobListItemBase, error) {
projectUUID := c.Param("uuid")
@@ -952,14 +952,14 @@ func (controller *ProjectController) submitJob(c *gin.Context) {
}
// listModel returns a list of models in the current project
-// @Summary Get model list for this project
-// @Tags Project
-// @Produce json
-// @Param uuid path string true "Project UUID"
-// @Success 200 {object} GeneralResponse{data=[]service.ModelListItem} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /project/{uuid}/model [get]
+// @Summary Get model list for this project
+// @Tags Project
+// @Produce json
+// @Param uuid path string true "Project UUID"
+// @Success 200 {object} GeneralResponse{data=[]service.ModelListItem} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /project/{uuid}/model [get]
func (controller *ProjectController) listModel(c *gin.Context) {
if data, err := func() ([]service.ModelListItem, error) {
projectUUID := c.Param("uuid")
@@ -980,14 +980,14 @@ func (controller *ProjectController) listModel(c *gin.Context) {
}
// handleProjectClosing process a project closing event
-// @Summary Process project closing event, called by FML manager only
-// @Tags Project
-// @Produce json
-// @Param uuid path string true "Project UUID"
-// @Success 200 {object} GeneralResponse{} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /project/internal/{uuid}/close [post]
+// @Summary Process project closing event, called by FML manager only
+// @Tags Project
+// @Produce json
+// @Param uuid path string true "Project UUID"
+// @Success 200 {object} GeneralResponse{} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /project/internal/{uuid}/close [post]
func (controller *ProjectController) handleProjectClosing(c *gin.Context) {
if err := func() error {
projectUUID := c.Param("uuid")
@@ -1007,14 +1007,14 @@ func (controller *ProjectController) handleProjectClosing(c *gin.Context) {
}
// handleParticipantSync process a participant info sync event
-// @Summary Process participant info sync event, to sync the participant info from fml manager
-// @Tags Project
-// @Produce json
-// @Param request body service.ProjectResourceSyncRequest true "Info of the project to by synced"
-// @Success 200 {object} GeneralResponse{} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /project/internal/event/participant/sync [post]
+// @Summary Process participant info sync event, to sync the participant info from fml manager
+// @Tags Project
+// @Produce json
+// @Param request body service.ProjectResourceSyncRequest true "Info of the project to by synced"
+// @Success 200 {object} GeneralResponse{} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /project/internal/event/participant/sync [post]
func (controller *ProjectController) handleParticipantSync(c *gin.Context) {
if err := func() error {
var event service.ProjectResourceSyncRequest
@@ -1037,14 +1037,14 @@ func (controller *ProjectController) handleParticipantSync(c *gin.Context) {
}
// handleDataSync process a data association sync event
-// @Summary Process data sync event, to sync the data association info from fml manager
-// @Tags Project
-// @Produce json
-// @Param request body service.ProjectResourceSyncRequest true "Info of the project to by synced"
-// @Success 200 {object} GeneralResponse{} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /project/internal/event/data/sync [post]
+// @Summary Process data sync event, to sync the data association info from fml manager
+// @Tags Project
+// @Produce json
+// @Param request body service.ProjectResourceSyncRequest true "Info of the project to by synced"
+// @Success 200 {object} GeneralResponse{} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /project/internal/event/data/sync [post]
func (controller *ProjectController) handleDataSync(c *gin.Context) {
if err := func() error {
var event service.ProjectResourceSyncRequest
@@ -1067,13 +1067,13 @@ func (controller *ProjectController) handleDataSync(c *gin.Context) {
}
// handleProjectListSync process a project list sync event
-// @Summary Process list sync event, to sync the projects list status from fml manager
-// @Tags Project
-// @Produce json
-// @Success 200 {object} GeneralResponse{} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /project/internal/event/list/sync [post]
+// @Summary Process list sync event, to sync the projects list status from fml manager
+// @Tags Project
+// @Produce json
+// @Success 200 {object} GeneralResponse{} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /project/internal/event/list/sync [post]
func (controller *ProjectController) handleProjectListSync(c *gin.Context) {
if err := controller.projectApp.SyncProject(); err != nil {
resp := &GeneralResponse{
@@ -1090,14 +1090,14 @@ func (controller *ProjectController) handleProjectListSync(c *gin.Context) {
}
// handleParticipantUnregistration process a project participant unregistration event
-// @Summary Process participant unregistration event, called by FML manager only
-// @Tags Project
-// @Produce json
-// @Param siteUUID path string true "Participant Site UUID"
-// @Success 200 {object} GeneralResponse{} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /project/internal/all/participant/{siteUUID}/unregister [post]
+// @Summary Process participant unregistration event, called by FML manager only
+// @Tags Project
+// @Produce json
+// @Param siteUUID path string true "Participant Site UUID"
+// @Success 200 {object} GeneralResponse{} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /project/internal/all/participant/{siteUUID}/unregister [post]
func (controller *ProjectController) handleParticipantUnregistration(c *gin.Context) {
if err := func() error {
siteUUID := c.Param("siteUUID")
diff --git a/site-portal/server/api/site.go b/site-portal/server/api/site.go
index ef7dc565..91a1e9cc 100644
--- a/site-portal/server/api/site.go
+++ b/site-portal/server/api/site.go
@@ -54,13 +54,13 @@ func (controller *SiteController) Route(r *gin.RouterGroup) {
}
// getSite returns the site data
-// @Summary Return site data
-// @Tags Site
-// @Produce json
-// @Success 200 {object} GeneralResponse{data=entity.Site} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /site [get]
+// @Summary Return site data
+// @Tags Site
+// @Produce json
+// @Success 200 {object} GeneralResponse{data=entity.Site} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /site [get]
func (controller *SiteController) getSite(c *gin.Context) {
site, err := controller.siteAppService.GetSite()
if err != nil {
@@ -81,14 +81,14 @@ func (controller *SiteController) getSite(c *gin.Context) {
}
// putSite update site related information
-// @Summary Update site information
-// @Tags Site
-// @Produce json
-// @Param site body entity.Site true "The site information, some info like id, UUID, connected status cannot be changed"
-// @Success 200 {object} GeneralResponse "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /site [put]
+// @Summary Update site information
+// @Tags Site
+// @Produce json
+// @Param site body entity.Site true "The site information, some info like id, UUID, connected status cannot be changed"
+// @Success 200 {object} GeneralResponse "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /site [put]
func (controller *SiteController) putSite(c *gin.Context) {
if err := func() error {
updatedSiteInfo := &entity.Site{}
@@ -111,14 +111,14 @@ func (controller *SiteController) putSite(c *gin.Context) {
}
// connectFATEFlow test connection to fate flow
-// @Summary Test site connection to fate flow service
-// @Tags Site
-// @Produce json
-// @Param connectInfo body service.FATEFlowConnectionInfo true "The fate flow connection info"
-// @Success 200 {object} GeneralResponse "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /site/fateflow/connect [post]
+// @Summary Test site connection to fate flow service
+// @Tags Site
+// @Produce json
+// @Param connectInfo body service.FATEFlowConnectionInfo true "The fate flow connection info"
+// @Success 200 {object} GeneralResponse "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /site/fateflow/connect [post]
func (controller *SiteController) connectFATEFlow(c *gin.Context) {
if err := func() error {
connectionInfo := &service.FATEFlowConnectionInfo{}
@@ -141,14 +141,14 @@ func (controller *SiteController) connectFATEFlow(c *gin.Context) {
}
// connectKubeflow test connection to Kubeflow
-// @Summary Test site connection to Kubeflow, including MinIO and KFServing
-// @Tags Site
-// @Produce json
-// @Param config body valueobject.KubeflowConfig true "The Kubeflow config info"
-// @Success 200 {object} GeneralResponse "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /site/kubeflow/connect [post]
+// @Summary Test site connection to Kubeflow, including MinIO and KFServing
+// @Tags Site
+// @Produce json
+// @Param config body valueobject.KubeflowConfig true "The Kubeflow config info"
+// @Success 200 {object} GeneralResponse "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /site/kubeflow/connect [post]
func (controller *SiteController) connectKubeflow(c *gin.Context) {
if err := func() error {
connectionInfo := &valueobject.KubeflowConfig{}
@@ -171,14 +171,14 @@ func (controller *SiteController) connectKubeflow(c *gin.Context) {
}
// connectFMLManager registers the current site to fml manager
-// @Summary Connect to fml manager and register itself
-// @Tags Site
-// @Produce json
-// @Param connectInfo body service.FMLManagerConnectionInfo true "The FML Manager endpoint"
-// @Success 200 {object} GeneralResponse "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /site/fmlmanager/connect [post]
+// @Summary Connect to fml manager and register itself
+// @Tags Site
+// @Produce json
+// @Param connectInfo body service.FMLManagerConnectionInfo true "The FML Manager endpoint"
+// @Success 200 {object} GeneralResponse "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /site/fmlmanager/connect [post]
func (controller *SiteController) connectFMLManager(c *gin.Context) {
if err := func() error {
connectionInfo := &service.FMLManagerConnectionInfo{}
@@ -201,13 +201,13 @@ func (controller *SiteController) connectFMLManager(c *gin.Context) {
}
// unregisterSite unregisters the current site from fml manager
-// @Summary Unregister from the fml manager
-// @Tags Site
-// @Produce json
-// @Success 200 {object} GeneralResponse "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /site/fmlmanager/unregister [post]
+// @Summary Unregister from the fml manager
+// @Tags Site
+// @Produce json
+// @Success 200 {object} GeneralResponse "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /site/fmlmanager/unregister [post]
func (controller *SiteController) unregisterSite(c *gin.Context) {
if err := func() error {
return controller.siteAppService.UnregisterFromFMLManager()
diff --git a/site-portal/server/api/user.go b/site-portal/server/api/user.go
index 9111bb09..500d02f8 100644
--- a/site-portal/server/api/user.go
+++ b/site-portal/server/api/user.go
@@ -57,13 +57,13 @@ func (controller *UserController) Route(r *gin.RouterGroup) {
}
// listUsers list all users
-// @Summary List all saved users
-// @Tags User
-// @Produce json
-// @Success 200 {object} GeneralResponse{data=[]service.PublicUser} "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /user [get]
+// @Summary List all saved users
+// @Tags User
+// @Produce json
+// @Success 200 {object} GeneralResponse{data=[]service.PublicUser} "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /user [get]
func (controller *UserController) listUsers(c *gin.Context) {
users, err := controller.userAppService.GetUsers()
if err != nil {
@@ -83,15 +83,15 @@ func (controller *UserController) listUsers(c *gin.Context) {
}
// updatePermission update user permission
-// @Summary Update user permission
-// @Tags User
-// @Produce json
-// @Param permission body valueobject.UserPermissionInfo true "Permission, must contain all permissions, otherwise the missing once will be considered as false"
-// @Param id path string true "User ID"
-// @Success 200 {object} GeneralResponse "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /user/{id}/permission [put]
+// @Summary Update user permission
+// @Tags User
+// @Produce json
+// @Param permission body valueobject.UserPermissionInfo true "Permission, must contain all permissions, otherwise the missing once will be considered as false"
+// @Param id path string true "User ID"
+// @Success 200 {object} GeneralResponse "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /user/{id}/permission [put]
func (controller *UserController) updatePermission(c *gin.Context) {
if data, err := func() (interface{}, error) {
userId, err := strconv.Atoi(c.Param("id"))
@@ -128,36 +128,36 @@ func (controller *UserController) updatePermission(c *gin.Context) {
}
// login login to site portal using the provided credentials
-// @Summary login to site portal
-// @Tags User
-// @Produce json
-// @Param credentials body service.LoginInfo true "credentials for login"
-// @Success 200 {object} GeneralResponse "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Router /user/login [post]
+// @Summary login to site portal
+// @Tags User
+// @Produce json
+// @Param credentials body service.LoginInfo true "credentials for login"
+// @Success 200 {object} GeneralResponse "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Router /user/login [post]
func (controller *UserController) login(c *gin.Context) {
authMiddleware.LoginHandler(c)
}
// logout logout from the site portal
-// @Summary logout from the site portal
-// @Tags User
-// @Produce json
-// @Success 200 {object} GeneralResponse "Success"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /user/logout [post]
+// @Summary logout from the site portal
+// @Tags User
+// @Produce json
+// @Success 200 {object} GeneralResponse "Success"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /user/logout [post]
func (controller *UserController) logout(c *gin.Context) {
authMiddleware.LogoutHandler(c)
}
// getCurrentUser return current user
-// @Summary Return current user in the jwt token
-// @Tags User
-// @Produce json
-// @Success 200 {object} GeneralResponse{data=string} "Success, the name of current user"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /user/current [get]
+// @Summary Return current user in the jwt token
+// @Tags User
+// @Produce json
+// @Success 200 {object} GeneralResponse{data=string} "Success, the name of current user"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /user/current [get]
func (controller *UserController) getCurrentUsername(c *gin.Context) {
if username, err := func() (string, error) {
claims := jwt.ExtractClaims(c)
@@ -180,14 +180,14 @@ func (controller *UserController) getCurrentUsername(c *gin.Context) {
}
// updatePassword update user password
-// @Summary Update user Password
-// @Tags User
-// @Produce json
-// @Param passwordChangeInfo body service.PwdChangeInfo string "current and new password"
-// @Success 200 {object} GeneralResponse "Success"
-// @Failure 401 {object} GeneralResponse "Unauthorized operation"
-// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
-// @Router /user/{id}/password [put]
+// @Summary Update user Password
+// @Tags User
+// @Produce json
+// @Param passwordChangeInfo body service.PwdChangeInfo string "current and new password"
+// @Success 200 {object} GeneralResponse "Success"
+// @Failure 401 {object} GeneralResponse "Unauthorized operation"
+// @Failure 500 {object} GeneralResponse{code=int} "Internal server error"
+// @Router /user/{id}/password [put]
func (controller *UserController) updatePassword(c *gin.Context) {
if err := func() error {
userId, err := strconv.Atoi(c.Param("id"))
diff --git a/site-portal/server/main.go b/site-portal/server/main.go
index 99fde94e..fdc42d50 100644
--- a/site-portal/server/main.go
+++ b/site-portal/server/main.go
@@ -48,13 +48,13 @@ import (
var FRONTEND_DIR = getFrontendDir()
// main starts the API server
-// @title site portal API service
-// @version v1
-// @description backend APIs of site portal service
-// @termsOfService http://swagger.io/terms/
-// @contact.name FedLCM team
-// @BasePath /api/v1
-// @in header
+// @title site portal API service
+// @version v1
+// @description backend APIs of site portal service
+// @termsOfService http://swagger.io/terms/
+// @contact.name FedLCM team
+// @BasePath /api/v1
+// @in header
func main() {
viper.AutomaticEnv()
replacer := strings.NewReplacer(".", "_")