diff --git a/.gitignore b/.gitignore index 48143ca5a..e64f8bf44 100644 --- a/.gitignore +++ b/.gitignore @@ -93,3 +93,4 @@ config.mk server.config.js es.match_phrase.js es.match_phrase.json +web_console/config diff --git a/deploy/charts/fedlearner/charts/fedlearner-web-console/templates/deployment.yaml b/deploy/charts/fedlearner/charts/fedlearner-web-console/templates/deployment.yaml index acd9cb837..13c628f54 100644 --- a/deploy/charts/fedlearner/charts/fedlearner-web-console/templates/deployment.yaml +++ b/deploy/charts/fedlearner/charts/fedlearner-web-console/templates/deployment.yaml @@ -34,9 +34,9 @@ spec: image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} command: - - node + - sh args: - - bootstrap.js + - startup.sh env: - name: NODE_ENV value: production diff --git a/deploy/charts/fedlearner/charts/fedlearner-web-console/values.yaml b/deploy/charts/fedlearner/charts/fedlearner-web-console/values.yaml index a9512c4ec..6fa6ce554 100644 --- a/deploy/charts/fedlearner/charts/fedlearner-web-console/values.yaml +++ b/deploy/charts/fedlearner/charts/fedlearner-web-console/values.yaml @@ -80,9 +80,11 @@ cluster: DB_PASSWORD: fedlearner DB_HOST: fedlearner-stack-mariadb DB_PORT: 3306 - DB_SYNC: true + DB_SYNC: false GRPC_AUTHORITY: "" KIBANA_HOST: fedlearner-stack-kibana KIBANA_PORT: 443 ES_HOST: fedlearner-stack-elasticsearch-client ES_PORT: 9200 + ETCD_ADDR: "fedlearner-stack-etcd.default.svc.cluster.local:2379" + KVSTORE_TYPE: "mysql" diff --git a/deploy/integrated_test/client_integrated_test.py b/deploy/integrated_test/client_integrated_test.py index 7c4986388..ceb85dbe0 100644 --- a/deploy/integrated_test/client_integrated_test.py +++ b/deploy/integrated_test/client_integrated_test.py @@ -2,7 +2,8 @@ import argparse import json import requests -from tools import login, request_and_response, build_raw_data, build_data_join_ticket, build_train_ticket +from tools import login, request_and_response, build_raw_data, \ + build_data_join_ticket, build_nn_ticket, build_tree_ticket def build_federation_json(args): @@ -18,17 +19,61 @@ def build_federation_json(args): if __name__ == '__main__': parser = argparse.ArgumentParser() - parser.add_argument('--name', type=str) - parser.add_argument('--x-federation', type=str) - parser.add_argument('--image', type=str) - parser.add_argument('--url', type=str) - parser.add_argument('--username', type=str) - parser.add_argument('--password', type=str) - parser.add_argument('--api-version') + parser.add_argument('--name', + type=str, + help='Name for peer federation.') + parser.add_argument('--x-federation', + type=str, + help='Name for local federation.') + parser.add_argument('--image', + type=str, + help='Image address.') + parser.add_argument('--data-portal-type', + type=str, + help='Type of raw data, Streaming(default) or PSI.', + choices=['Streaming', 'PSI'], + default='Streaming') + parser.add_argument('--model-type', + type=str, + help='Type of train model, (nn model) or tree model.', + choices=['nn_model', 'tree_model'], + default='nn_model') + parser.add_argument('--rsa-key-path', + type=str, + help='Path to RSA public key.') + parser.add_argument('--rsa-key-pem', + type=str, + help='Either rsa key path or rsa key pem must be given.') + parser.add_argument('--url', + type=str, + help='URL to webconsole.', + default='127.0.0.1:1989') + parser.add_argument('--username', + type=str, + help='Username of webconsole.', + default='ada') + parser.add_argument('--password', + type=str, + help='Password of webconsole.', + default='ada') + parser.add_argument('--api-version', + help='API version of webconsole.', + default=1) args = parser.parse_args() + args.streaming = args.data_portal_type == 'Streaming' + if not args.streaming: + args.cmd_args = {'Master': ["/app/deploy/scripts/rsa_psi/run_psi_data_join_master.sh"], + 'Worker': ["/app/deploy/scripts/rsa_psi/run_psi_data_join_worker.sh"]} + if args.rsa_key_pem is not None: + args.psi_extras = [{"name": "RSA_KEY_PEM", "value": args.rsa_key_pem}] + elif args.rsa_key_path is not None: + args.psi_extras = [{"name": "RSA_KEY_PATH", "value": args.rsa_key_path}] + else: + raise Exception('Either RSA_KEY_PEN or RSA_KEY_PATH must be provided when using PSI.') + args.psi_extras.append({"name": "SIGN_RPC_TIMEOUT_MS", "value": "128000"}) + args.url = args.url.strip().rstrip('/') + '/api/v' + str(args.api_version) cookie = login(args) - federation_json, suffix = build_federation_json(args) federation_id, federation_name = request_and_response(args=args, url=args.url + '/federations', @@ -45,15 +90,22 @@ def build_federation_json(args): requests.post(url=args.url + '/raw_data/' + str(raw_data_id) + '/submit', cookies=cookie) join_ticket_json, suffix = build_data_join_ticket(args, federation_id, raw_data_name, - 'template_json/template_join_ticket.json', 'Leader') + 'template_json/template_streaming_join_ticket.json' + if args.streaming + else 'template_json/template_psi_join_ticket.json', + 'Leader' if args.streaming else 'Follower') join_ticket_id, join_ticket_name = request_and_response(args=args, url=args.url + '/tickets', json_data=join_ticket_json, cookies=cookie, name_suffix=suffix) - train_ticket_json, suffix = build_train_ticket(args, federation_id, - 'template_json/template_train_ticket.json', 'Follower') + if args.model_type == 'nn_model': + train_ticket_json, suffix = build_nn_ticket(args, federation_id, + 'template_json/template_nn_ticket.json', 'Follower') + else: + train_ticket_json, suffix = build_tree_ticket(args, federation_id, + 'template_json/template_tree_ticket.json', 'Follower') train_ticket_id, train_ticket_name = request_and_response(args=args, url=args.url + '/tickets', json_data=train_ticket_json, diff --git a/deploy/integrated_test/template_json/template_train_ticket.json b/deploy/integrated_test/template_json/template_nn_ticket.json similarity index 100% rename from deploy/integrated_test/template_json/template_train_ticket.json rename to deploy/integrated_test/template_json/template_nn_ticket.json diff --git a/deploy/integrated_test/template_json/template_psi_join_ticket.json b/deploy/integrated_test/template_json/template_psi_join_ticket.json new file mode 100644 index 000000000..503ca701e --- /dev/null +++ b/deploy/integrated_test/template_json/template_psi_join_ticket.json @@ -0,0 +1,178 @@ +{ + "public_params": { + "spec": { + "flReplicaSpecs": { + "Master": { + "pair": true, + "replicas": 1, + "template": { + "spec": { + "containers": [ + { + "env": [ + { + "name": "PARTITION_NUM", + "value": "4" + }, + { + "name": "START_TIME", + "value": "0" + }, + { + "name": "END_TIME", + "value": "999999999999" + }, + { + "name": "NEGATIVE_SAMPLING_RATE", + "value": "1.0" + }, + { + "name": "RAW_DATA_SUB_DIR", + "value": "portal_publish_dir/" + } + ], + "image": "!image", + "ports": [ + { + "containerPort": 50051, + "name": "flapp-port" + } + ], + "command": [ + "/app/deploy/scripts/wait4pair_wrapper.sh" + ], + "args": [] + } + ] + } + } + }, + "Worker": { + "pair": true, + "replicas": 4, + "template": { + "spec": { + "containers": [ + { + "env": [ + { + "name": "PARTITION_NUM", + "value": "4" + }, + { + "name": "RAW_DATA_SUB_DIR", + "value": "portal_publish_dir/" + }, + { + "name": "DATA_BLOCK_DUMP_INTERVAL", + "value": "600" + }, + { + "name": "DATA_BLOCK_DUMP_THRESHOLD", + "value": "65536" + }, + { + "name": "EXAMPLE_ID_DUMP_INTERVAL", + "value": "600" + }, + { + "name": "EXAMPLE_ID_DUMP_THRESHOLD", + "value": "65536" + }, + { + "name": "PSI_RAW_DATA_ITER", + "value": "TF_RECORD" + }, + { + "name": "PSI_OUTPUT_BUILDER", + "value": "TF_RECORD" + }, + { + "name": "DATA_BLOCK_BUILDER", + "value": "TF_RECORD" + }, + { + "name": "EXAMPLE_JOINER", + "value": "SORT_RUN_JOINER" + }, + { + "name": "SIGN_RPC_TIMEOUT_MS", + "value": "128000" + } + ], + "image": "!image", + "ports": [ + { + "containerPort": 50051, + "name": "flapp-port" + } + ], + "command": [ + "/app/deploy/scripts/wait4pair_wrapper.sh" + ], + "args": [] + } + ] + } + } + } + } + } + }, + "private_params": { + "spec": { + "flReplicaSpecs": { + "Master": { + "template": { + "spec": { + "containers": [ + { + "image": "!image", + "env": [ + { + "name": "RAW_DATA_SUB_DIR", + "value": "portal_publish_dir/" + }, + { + "name": "PARTITION_NUM", + "value": "4" + } + ] + } + ] + } + }, + "replicas": 1 + }, + "Worker": { + "template": { + "spec": { + "containers": [ + { + "image": "!image", + "env": [ + { + "name": "RAW_DATA_SUB_DIR", + "value": "portal_publish_dir/" + }, + { + "name": "PARTITION_NUM", + "value": "4" + } + ] + } + ] + } + }, + "replicas": 4 + } + } + } + }, + "name": "!name", + "federation_id": "!federation_id", + "job_type": "psi_data_join", + "role": "!role", + "expire_time": "!expire_time", + "remark": "Built by integrated test." +} diff --git a/deploy/integrated_test/template_json/template_raw_data.json b/deploy/integrated_test/template_json/template_raw_data.json index 5f261e7cc..f7b9eab92 100644 --- a/deploy/integrated_test/template_json/template_raw_data.json +++ b/deploy/integrated_test/template_json/template_raw_data.json @@ -2,7 +2,7 @@ "name": "!name", "federation_id": "!federation_id", "output_partition_num": 4, - "data_portal_type": "Streaming", + "data_portal_type": "!data_portal_type", "input": "/app/deploy/integrated_test/tfrecord_raw_data", "image": "!image", "context": { diff --git a/deploy/integrated_test/template_json/template_join_ticket.json b/deploy/integrated_test/template_json/template_streaming_join_ticket.json similarity index 97% rename from deploy/integrated_test/template_json/template_join_ticket.json rename to deploy/integrated_test/template_json/template_streaming_join_ticket.json index c08be5f92..fa52e48bd 100644 --- a/deploy/integrated_test/template_json/template_join_ticket.json +++ b/deploy/integrated_test/template_json/template_streaming_join_ticket.json @@ -61,13 +61,21 @@ "containers": [ { "env": [ + { + "name": "PARTITION_NUM", + "value": "4" + }, + { + "name": "RAW_DATA_SUB_DIR", + "value": "portal_publish_dir/" + }, { "name": "DATA_BLOCK_DUMP_INTERVAL", "value": "600" }, { "name": "DATA_BLOCK_DUMP_THRESHOLD", - "value": "262144" + "value": "65536" }, { "name": "EXAMPLE_ID_DUMP_INTERVAL", @@ -75,7 +83,7 @@ }, { "name": "EXAMPLE_ID_DUMP_THRESHOLD", - "value": "262144" + "value": "65536" }, { "name": "EXAMPLE_ID_BATCH_SIZE", @@ -96,14 +104,6 @@ { "name": "RAW_DATA_ITER", "value": "TF_RECORD" - }, - { - "name": "RAW_DATA_SUB_DIR", - "value": "portal_publish_dir/" - }, - { - "name": "PARTITION_NUM", - "value": "4" } ], "image": "!image", @@ -182,5 +182,5 @@ "job_type": "data_join", "role": "!role", "expire_time": "!expire_time", - "remark": "Build by integrated test." + "remark": "Built by integrated test." } diff --git a/deploy/integrated_test/template_json/template_tree_ticket.json b/deploy/integrated_test/template_json/template_tree_ticket.json new file mode 100644 index 000000000..07765938e --- /dev/null +++ b/deploy/integrated_test/template_json/template_tree_ticket.json @@ -0,0 +1,85 @@ +{ + "public_params": { + "spec": { + "flReplicaSpecs": { + "Worker": { + "pair": true, + "replicas": 1, + "template": { + "spec": { + "containers": [ + { + "env": [ + { + "name": "FILE_EXT", + "value": ".data" + }, + { + "name": "FILE_TYPE", + "value": "tfrecord" + }, + { + "name": "SEND_SCORES_TO_FOLLOWER", + "value": "" + }, + { + "name": "MODE", + "value": "train" + }, + { + "name": "DATA_SOURCE", + "value": "!DATA_SOURCE" + } + ], + "image": "!image", + "ports": [ + { + "containerPort": 50051, + "name": "flapp-port" + } + ], + "command": [ + "/app/deploy/scripts/wait4pair_wrapper.sh" + ], + "args": [ + "/app/deploy/scripts/trainer/run_tree_worker.sh" + ] + } + ] + } + } + } + } + } + }, + "private_params": { + "spec": { + "flReplicaSpecs": { + "Worker": { + "template": { + "spec": { + "containers": [ + { + "image": "!image", + "env": [ + { + "name": "DATA_SOURCE", + "value": "!DATA_SOURCE" + } + ] + } + ] + } + } + } + } + } + }, + "name": "!name", + "federation_id": -1, + "job_type": "tree_model", + "role": "!role", + "expire_time": "!expire_time", + "remark": "Built by integrated test.", + "undefined": "" +} diff --git a/deploy/integrated_test/tfrecord_raw_data/raw_data_partition_0000.rd b/deploy/integrated_test/tfrecord_raw_data/raw_data_partition_0000.rd index f10e60143..662ee7561 100644 Binary files a/deploy/integrated_test/tfrecord_raw_data/raw_data_partition_0000.rd and b/deploy/integrated_test/tfrecord_raw_data/raw_data_partition_0000.rd differ diff --git a/deploy/integrated_test/tfrecord_raw_data/raw_data_partition_0001.rd b/deploy/integrated_test/tfrecord_raw_data/raw_data_partition_0001.rd index c86af6015..5428c488e 100644 Binary files a/deploy/integrated_test/tfrecord_raw_data/raw_data_partition_0001.rd and b/deploy/integrated_test/tfrecord_raw_data/raw_data_partition_0001.rd differ diff --git a/deploy/integrated_test/tfrecord_raw_data/raw_data_partition_0002.rd b/deploy/integrated_test/tfrecord_raw_data/raw_data_partition_0002.rd index 8bef6f388..d92eb1819 100644 Binary files a/deploy/integrated_test/tfrecord_raw_data/raw_data_partition_0002.rd and b/deploy/integrated_test/tfrecord_raw_data/raw_data_partition_0002.rd differ diff --git a/deploy/integrated_test/tfrecord_raw_data/raw_data_partition_0003.rd b/deploy/integrated_test/tfrecord_raw_data/raw_data_partition_0003.rd index 99d155524..35e28e1fb 100644 Binary files a/deploy/integrated_test/tfrecord_raw_data/raw_data_partition_0003.rd and b/deploy/integrated_test/tfrecord_raw_data/raw_data_partition_0003.rd differ diff --git a/deploy/integrated_test/tools.py b/deploy/integrated_test/tools.py index ac6c30aee..471abd65b 100644 --- a/deploy/integrated_test/tools.py +++ b/deploy/integrated_test/tools.py @@ -45,6 +45,8 @@ def request_and_response(args, url, json_data, cookies, name_suffix=''): try: response = json.loads(response.text) except json.decoder.JSONDecodeError: + print('Json data to be sent:') + print(json_data) raise Exception('404 error encountered when building/modifying {}. ' 'Please check whether webconsole api changed.'.format(url.split('/')[-1])) if 'error' not in response.keys(): @@ -61,6 +63,7 @@ def build_raw_data(args, fed_id, filepath): name_suffix = '-raw-data' raw_json['name'] = args.name + name_suffix raw_json['federation_id'] = fed_id + raw_json['data_portal_type'] = args.data_portal_type raw_json['image'] = args.image fl_rep_spec = raw_json['context']['yaml_spec']['spec']['flReplicaSpecs'] fl_rep_spec['Master']['template']['spec']['containers'][0]['image'] = args.image @@ -79,9 +82,14 @@ def build_data_join_ticket(args, fed_id, raw_name, filepath, role): ticket_json['sdk_version'] = args.image.split(':')[-1] ticket_json['expire_time'] = str(datetime.datetime.now().year + 1) + '-12-31' for param in ['public_params', 'private_params']: - for pod in ticket_json[param]['spec']['flReplicaSpecs'].values(): + for pod_name, pod in ticket_json[param]['spec']['flReplicaSpecs'].items(): container = pod['template']['spec']['containers'][0] container['image'] = args.image + if not args.streaming: + if param == 'public_params': + container['args'] = args.cmd_args[pod_name] + if pod_name == 'Worker': + container['env'].extend(args.psi_extras) for d in container['env']: if d['name'] == 'RAW_DATA_SUB_DIR': d['value'] += raw_name diff --git a/deploy/scripts/rsa_psi/run_psi_data_join_master.sh b/deploy/scripts/rsa_psi/run_psi_data_join_master.sh new file mode 100755 index 000000000..1ddf7969a --- /dev/null +++ b/deploy/scripts/rsa_psi/run_psi_data_join_master.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +# Copyright 2020 The FedLearner Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -ex + +data_join_master_cmd=/app/deploy/scripts/data_join/run_data_join_master.sh + +export RAW_DATA_SUB_DIR="portal_publish_dir/${APPLICATION_ID}_psi_preprocess" + +# Reverse the role assignment for data join so that leader for PSI preprocessor +# becomes follower for data join. Data join's workers get their role from +# master so we don't need to do this for worker. +if [ $ROLE == "leader" ]; then + export ROLE="follower" +else + export ROLE="leader" +fi + +${data_join_master_cmd} diff --git a/deploy/scripts/rsa_psi/run_psi_data_join_worker.sh b/deploy/scripts/rsa_psi/run_psi_data_join_worker.sh new file mode 100755 index 000000000..fd0bdb449 --- /dev/null +++ b/deploy/scripts/rsa_psi/run_psi_data_join_worker.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +# Copyright 2020 The FedLearner Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -ex + +psi_data_join_leader_worker_cmd=/app/deploy/scripts/data_join/run_psi_data_join_leader_worker_v2.sh +psi_data_join_follower_worker_cmd=/app/deploy/scripts/data_join/run_psi_data_join_follower_worker_v2.sh + +export INPUT_FILE_SUBSCRIBE_DIR=$RAW_DATA_SUB_DIR +export RAW_DATA_PUBLISH_DIR="portal_publish_dir/${APPLICATION_ID}_psi_preprocess" +if [ $ROLE == "leader" ]; then + ${psi_data_join_leader_worker_cmd} +else + ${psi_data_join_follower_worker_cmd} +fi diff --git a/deploy/scripts/rsa_psi/run_psi_preprocessor.sh b/deploy/scripts/rsa_psi/run_psi_preprocessor.sh index 2a77b54d9..2ec30a0cc 100755 --- a/deploy/scripts/rsa_psi/run_psi_preprocessor.sh +++ b/deploy/scripts/rsa_psi/run_psi_preprocessor.sh @@ -60,9 +60,13 @@ builder_compressed_type=$(normalize_env_to_args "--builder_compressed_type" $PSI preprocessor_offload_processor_number=$(normalize_env_to_args "--preprocessor_offload_processor_number" $PREPROCESSOR_OFFLOAD_PROCESSOR_NUMBER) kvstore_type=$(normalize_env_to_args '--kvstore_type' $KVSTORE_TYPE) + +# Turn off display to avoid RSA_KEY_PEM showing in log +set +x + python -m fedlearner.data_join.cmd.rsa_psi_preprocessor_cli \ --psi_role=$ROLE \ - --rsa_key_path=$RSA_KEY_PATH \ + --rsa_key_path="$RSA_KEY_PATH" \ --rsa_key_pem="$RSA_KEY_PEM" \ --output_file_dir="$OUTPUT_BASE_DIR/psi_output" \ --raw_data_publish_dir=$RAW_DATA_PUBLISH_DIR \ diff --git a/deploy/scripts/rsa_psi/run_rsa_psi_signer.sh b/deploy/scripts/rsa_psi/run_rsa_psi_signer.sh index 2f88342b2..78cd484d2 100755 --- a/deploy/scripts/rsa_psi/run_rsa_psi_signer.sh +++ b/deploy/scripts/rsa_psi/run_rsa_psi_signer.sh @@ -24,8 +24,11 @@ slow_sign_threshold=$(normalize_env_to_args "--slow_sign_threshold" $SLOW_SIGN_T worker_num=$(normalize_env_to_args "--worker_num" $WORKER_NUM) signer_offload_processor_number=$(normalize_env_to_args "--signer_offload_processor_number" $SIGNER_OFFLOAD_PROCESSOR_NUMBER) +# Turn off display to avoid RSA_KEY_PEM showing in log +set +x + python -m fedlearner.data_join.cmd.rsa_psi_signer_service \ --listen_port=50051 \ - --rsa_private_key_path=$RSA_PRIVATE_KEY_PATH \ + --rsa_private_key_path="$RSA_PRIVATE_KEY_PATH" \ --rsa_privet_key_pem="$RSA_KEY_PEM" \ $slow_sign_threshold $worker_num $signer_offload_processor_number diff --git a/deploy/scripts/trainer/run_tree_worker.sh b/deploy/scripts/trainer/run_tree_worker.sh index 23e79cab3..6f1edd3a0 100755 --- a/deploy/scripts/trainer/run_tree_worker.sh +++ b/deploy/scripts/trainer/run_tree_worker.sh @@ -22,24 +22,31 @@ source /app/deploy/scripts/env_to_args.sh NUM_WORKERS=`python -c 'import json, os; print(len(json.loads(os.environ["CLUSTER_SPEC"])["clusterSpec"]["Worker"]))'` +if [[ -z "${DATA_PATH}" && -n "${DATA_SOURCE}" ]]; then + export DATA_PATH="${STORAGE_ROOT_PATH}/data_source/${DATA_SOURCE}/data_block" +fi + mode=$(normalize_env_to_args "--mode" "$MODE") data_path=$(normalize_env_to_args "--data-path" "$DATA_PATH") validation_data_path=$(normalize_env_to_args "--validation-data-path" "$VALIDATION_DATA_PATH") no_data=$(normalize_env_to_args "--no-data" "$NO_DATA") file_ext=$(normalize_env_to_args "--file-ext" "$FILE_EXT") +file_type=$(normalize_env_to_args "--file-type" "$FILE_TYPE") load_model_path=$(normalize_env_to_args "--load-model-path" "$LOAD_MODEL_PATH") verbosity=$(normalize_env_to_args "--verbosity" "$VERBOSITY") +loss_type=$(normalize_env_to_args "--loss-type" "$LOSS_TYPE") learning_rate=$(normalize_env_to_args "--learning-rate" "$LEARNING_RATE") max_iters=$(normalize_env_to_args "--max-iters" "$MAX_ITERS") max_depth=$(normalize_env_to_args "--max-depth" "$MAX_DEPTH") l2_regularization=$(normalize_env_to_args "--l2-regularization" "$L2_REGULARIZATION") max_bins=$(normalize_env_to_args "--max-bins" "$MAX_BINS") -num_parallel=$(normalize_env_to_args "--num-parallel" "$NUM_PARALELL") +num_parallel=$(normalize_env_to_args "--num-parallel" "$NUM_PARALLEL") verify_example_ids=$(normalize_env_to_args "--verify-example-ids" "$VERIFY_EXAMPLE_IDS") ignore_fields=$(normalize_env_to_args "--ignore-fields" "$IGNORE_FIELDS") cat_fields=$(normalize_env_to_args "--cat-fields" "$CAT_FIELDS") use_streaming=$(normalize_env_to_args "--use-streaming" "$USE_STREAMING") send_scores_to_follower=$(normalize_env_to_args "--send-scores-to-follower" "$SEND_SCORES_TO_FOLLOWER") +send_metrics_to_follower=$(normalize_env_to_args "--send-metrics-to-follower" "$SEND_METRICS_TO_FOLLOWER") python -m fedlearner.model.tree.trainer \ @@ -53,8 +60,9 @@ python -m fedlearner.model.tree.trainer \ --checkpoint-path="$OUTPUT_BASE_DIR/checkpoints" \ --output-path="$OUTPUT_BASE_DIR/outputs" \ $mode $data_path $validation_data_path \ - $no_data $file_ext $load_model_path \ - $verbosity $learning_rate $max_iters \ + $no_data $file_ext $file_type $load_model_path \ + $verbosity $loss_type $learning_rate $max_iters \ $max_depth $l2_regularization $max_bins \ $num_parallel $verify_example_ids $ignore_fields \ - $cat_fields $use_streaming $send_scores_to_follower + $cat_fields $use_streaming $send_scores_to_follower \ + $send_metrics_to_follower diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index b8ceed905..000000000 --- a/package-lock.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "requires": true, - "lockfileVersion": 1, - "dependencies": { - "esm": { - "version": "3.2.25", - "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", - "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==" - } - } -} diff --git a/web_console/Dockerfile b/web_console/Dockerfile index b7ec22d01..8ace1ca81 100644 --- a/web_console/Dockerfile +++ b/web_console/Dockerfile @@ -13,4 +13,4 @@ RUN npm run build EXPOSE 1989 -CMD ["node", "bootstrap.js"] +CMD ["startup.sh"] diff --git a/web_console/api/federation.js b/web_console/api/federation.js index 4f7eac4ec..4fc1545a6 100644 --- a/web_console/api/federation.js +++ b/web_console/api/federation.js @@ -111,4 +111,18 @@ router.get('/api/v1/federations/:id/tickets', SessionMiddleware, async (ctx) => ctx.body = { data }; }); +router.get('/api/v1/federations/:id/heartbeat', SessionMiddleware, async (ctx) => { + const federation = await Federation.findByPk(ctx.params.id); + if (!federation) { + ctx.status = 404; + ctx.body = { + error: 'Federation not found', + }; + return; + } + const client = new FederationClient(federation); + const res = await client.heartBeat(); + ctx.body = res; +}); + module.exports = router; diff --git a/web_console/api/job.js b/web_console/api/job.js index 2a0a63069..34ac7f124 100644 --- a/web_console/api/job.js +++ b/web_console/api/job.js @@ -1,6 +1,7 @@ const router = require('@koa/router')(); const { Op } = require('sequelize'); const SessionMiddleware = require('../middlewares/session'); +const FindOptionsMiddleware = require('../middlewares/find_options'); const k8s = require('../libs/k8s'); const es = require('../libs/es'); const { Job, Ticket, Federation } = require('../models'); @@ -8,6 +9,7 @@ const FederationClient = require('../rpc/client'); const getConfig = require('../utils/get_confg'); const checkParseJson = require('../utils/check_parse_json'); const { clientValidateJob, clientGenerateYaml } = require('../utils/job_builder'); +const { client } = require('../libs/k8s'); const config = getConfig({ NAMESPACE: process.env.NAMESPACE, @@ -19,15 +21,35 @@ try { es_oparator_match_phrase = require('../es.match_phrase'); } catch (err) { /* */ } -router.get('/api/v1/jobs', SessionMiddleware, async (ctx) => { +router.get('/api/v1/jobs', SessionMiddleware, FindOptionsMiddleware, async (ctx) => { const jobs = await Job.findAll({ + ...ctx.findOptions, order: [['created_at', 'DESC']], }); const { flapps } = await k8s.getFLAppsByNamespace(namespace); - const data = jobs.map((job) => ({ - ...(flapps.items.find((item) => item.metadata.name === job.name)), - localdata: job, - })); + let data = []; + for (job of jobs) { + if (job.status == null) job.status = 'started'; + if (job.federation_id == null) { + const clientTicket = await Ticket.findOne({ + where: { + name: { [Op.eq]: job.client_ticket_name }, + }, + }); + job.federation_id = clientTicket.federation_id; + } + if (job.status === 'stopped') { + data.push({ + ...JSON.parse(job.k8s_meta_snapshot).flapp, + localdata: job, + }); + } else { + data.push({ + ...(flapps.items.find((item) => item.metadata.name === job.name)), + localdata: job, + }); + } + } ctx.body = { data }; }); @@ -41,7 +63,24 @@ router.get('/api/v1/job/:id', SessionMiddleware, async (ctx) => { }; return; } - const { flapp } = await k8s.getFLApp(namespace, job.name); + + if (job.status == null) job.status = 'started'; + if (job.federation_id == null) { + const clientTicket = await Ticket.findOne({ + where: { + name: { [Op.eq]: job.client_ticket_name }, + }, + }); + job.federation_id = clientTicket.federation_id; + } + + var flapp; + if (job.status === 'stopped') { + flapp = JSON.parse(job.k8s_meta_snapshot).flapp; + } else { + flapp = (await k8s.getFLApp(namespace, job.name)).flapp; + } + ctx.body = { data: { ...flapp, @@ -52,7 +91,28 @@ router.get('/api/v1/job/:id', SessionMiddleware, async (ctx) => { router.get('/api/v1/job/:k8s_name/pods', SessionMiddleware, async (ctx) => { const { k8s_name } = ctx.params; - const { pods } = await k8s.getFLAppPods(namespace, k8s_name); + + const job = await Job.findOne({ + where: { + name: { [Op.eq]: k8s_name }, + }, + }); + + if (!job) { + ctx.status = 404; + ctx.body = { + error: 'Job not found', + }; + return; + } + + var pods; + if (job.status === 'stopped') { + pods = JSON.parse(job.k8s_meta_snapshot).pods; + } else { + pods = (await k8s.getFLAppPods(namespace, k8s_name)).pods; + } + ctx.body = { data: pods.items }; }); @@ -126,11 +186,6 @@ router.post('/api/v1/job', SessionMiddleware, async (ctx) => { return; } - const job = { - name, job_type, client_ticket_name, server_ticket_name, - client_params, server_params, - }; - const exists = await Job.findOne({ where: { name: { [Op.eq]: name }, @@ -157,25 +212,47 @@ router.post('/api/v1/job', SessionMiddleware, async (ctx) => { return; } + const clientFed = await Federation.findByPk(clientTicket.federation_id); + if (!clientFed) { + ctx.status = 422; + ctx.body = { + error: 'Federation does not exist', + }; + return; + } + const rpcClient = new FederationClient(clientFed); + + let serverTicket; try { - clientValidateJob(job, clientTicket); - } catch (e) { - ctx.status = 400; + const { data } = await rpcClient.getTickets({ job_type: '', role: '' }); + serverTicket = data.find(x => x.name === server_ticket_name); + if (!serverTicket) { + throw new Error(`Cannot find server ticket ${server_ticket_name}`); + } + } catch (err) { + ctx.status = 500; ctx.body = { - error: `client_params validation failed: ${e.message}`, + error: `Cannot get server ticket: ${err.message}`, }; return; } - const clientFed = await Federation.findByPk(clientTicket.federation_id); - if (!clientFed) { - ctx.status = 422; + const job = { + name, job_type, client_ticket_name, server_ticket_name, + client_params, server_params, status: 'started', + federation_id: clientFed.id, + }; + + try { + clientValidateJob(job, clientTicket, serverTicket); + } catch (e) { + ctx.status = 400; ctx.body = { - error: 'Federation does not exist', + error: `client_params validation failed: ${e.message}`, }; return; } - const rpcClient = new FederationClient(clientFed); + try { await rpcClient.createJob({ ...job, @@ -184,7 +261,7 @@ router.post('/api/v1/job', SessionMiddleware, async (ctx) => { } catch (err) { ctx.status = 500; ctx.body = { - error: err.details, + error: `RPC Error: ${err.message}`, }; return; } @@ -215,6 +292,166 @@ router.post('/api/v1/job', SessionMiddleware, async (ctx) => { ctx.body = { data }; }); +router.post('/api/v1/job/:id/update', SessionMiddleware, async (ctx) => { + // get old job info + const { id } = ctx.params; + const old_job = await Job.findByPk(id); + if (!old_job) { + ctx.status = 404; + ctx.body = { + error: 'Job not found', + }; + return; + } + + if (old_job.status === 'error') { + ctx.status = 422; + ctx.body = { + error: 'Cannot update errored job', + }; + return; + } + + const { + name, job_type, client_ticket_name, server_ticket_name, + client_params, server_params, status, + } = ctx.request.body; + + if (old_job.status === 'started' && status != 'stopped') { + ctx.status = 422; + ctx.body = { + error: 'Cannot change running job', + }; + return; + } + + if (name != old_job.name) { + ctx.status = 422; + ctx.body = { + error: 'cannot change job name', + }; + return; + } + + if (job_type != old_job.job_type) { + ctx.status = 422; + ctx.body = { + error: 'cannot change job type', + }; + return; + } + + const clientTicket = await Ticket.findOne({ + where: { + name: { [Op.eq]: client_ticket_name }, + }, + }); + if (!clientTicket) { + ctx.status = 422; + ctx.body = { + error: `client_ticket ${client_ticket_name} does not exist`, + }; + return; + } + + const OldClientTicket = await Ticket.findOne({ + where: { + name: { [Op.eq]: old_job.client_ticket_name }, + }, + }); + if (!OldClientTicket) { + ctx.status = 422; + ctx.body = { + error: `client_ticket ${old_job.client_ticket_name} does not exist`, + }; + return; + } + + if (clientTicket.federation_id != OldClientTicket.federation_id) { + ctx.status = 422; + ctx.body = { + error: 'cannot change job federation', + }; + return; + } + + const clientFed = await Federation.findByPk(clientTicket.federation_id); + if (!clientFed) { + ctx.status = 422; + ctx.body = { + error: 'Federation does not exist', + }; + return; + } + const rpcClient = new FederationClient(clientFed); + + let serverTicket; + try { + const { data } = await rpcClient.getTickets({ job_type: '', role: '' }); + serverTicket = data.find(x => x.name === server_ticket_name); + if (!serverTicket) { + throw new Error(`Cannot find server ticket ${server_ticket_name}`); + } + } catch (err) { + ctx.status = 500; + ctx.body = { + error: `RPC Error: ${err.message}`, + }; + return; + } + + const new_job = { + name, job_type, client_ticket_name, server_ticket_name, + client_params, server_params, status, + federation_id: clientTicket.federation_id, + }; + + try { + clientValidateJob(new_job, clientTicket, serverTicket); + } catch (e) { + ctx.status = 400; + ctx.body = { + error: `client_params validation failed: ${e.message}`, + }; + return; + } + + // update job + try { + await rpcClient.updateJob({ + ...new_job, + server_params: JSON.stringify(server_params), + }); + } catch (err) { + ctx.status = 500; + ctx.body = { + error: `RPC Error: ${err.message}`, + }; + return; + } + + if (old_job.status === 'started' && new_job.status === 'stopped') { + flapp = (await k8s.getFLApp(namespace, new_job.name)).flapp; + pods = (await k8s.getFLAppPods(namespace, new_job.name)).pods; + old_job.k8s_meta_snapshot = JSON.stringify({flapp, pods}); + await k8s.deleteFLApp(namespace, new_job.name); + } else if (old_job.status === 'stopped' && new_job.status === 'started') { + const clientYaml = clientGenerateYaml(clientFed, new_job, clientTicket); + await k8s.createFLApp(namespace, clientYaml); + } + + old_job.client_ticket_name = new_job.client_ticket_name; + old_job.server_ticket_name = new_job.server_ticket_name; + old_job.client_params = new_job.client_params; + old_job.server_params = new_job.server_params; + old_job.status = new_job.status; + old_job.federation_id = new_job.federation_id; + + const data = await old_job.save(); + + ctx.body = { data }; +}); + router.delete('/api/v1/job/:id', SessionMiddleware, async (ctx) => { // TODO: just owner can delete const { id } = ctx.params; @@ -228,6 +465,11 @@ router.delete('/api/v1/job/:id', SessionMiddleware, async (ctx) => { return; } + if (!data.status || data.status == 'started') { + await k8s.deleteFLApp(namespace, data.name); + } + await data.destroy({ force: true }); + const ticket = await Ticket.findOne({ where: { name: { [Op.eq]: data.client_ticket_name }, @@ -240,12 +482,10 @@ router.delete('/api/v1/job/:id', SessionMiddleware, async (ctx) => { } catch (err) { ctx.status = 500; ctx.body = { - error: err.details, + error: `RPC Error: ${err.message}`, }; return; } - await k8s.deleteFLApp(namespace, data.name); - await data.destroy({ force: true }); ctx.body = { data }; }); diff --git a/web_console/components/BooleanSelect.jsx b/web_console/components/BooleanSelect.jsx new file mode 100644 index 000000000..2ecd86bd4 --- /dev/null +++ b/web_console/components/BooleanSelect.jsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { Select } from '@zeit-ui/react'; + +const options = [ + { label: 'True', value: 'true' }, + { label: 'False', value: 'false' }, +] + +export default function ClientTicketSelect(props) { + const actualValue = props.value?.toString() || 'true' + const actualOnChange = (value) => { + props.onChange(value === 'true'); + }; + return ( + + ); +} diff --git a/web_console/components/ClientTicketSelect.jsx b/web_console/components/ClientTicketSelect.jsx index 13ec80a9d..3f2d92656 100644 --- a/web_console/components/ClientTicketSelect.jsx +++ b/web_console/components/ClientTicketSelect.jsx @@ -1,13 +1,23 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import { Select } from '@zeit-ui/react'; import useSWR from 'swr'; import { fetcher } from '../libs/http'; +import { JOB_TYPE_CLASS } from '../constants/job' -export default function ClientTicketSelect(props) { +let filter = () => true +export default function ClientTicketSelect({type, ...props}) { const { data } = useSWR('tickets', fetcher); - const tickets = (data && data.data) || []; - const actualValue = tickets.find((x) => x.name === props.value)?.value; + if (type) { + filter = el => JOB_TYPE_CLASS[type].some(t => el.job_type === t) + } + + const tickets = data + ? data.data.filter(filter) + : []; + + // const actualValue = tickets.find((x) => x.name === props.value)?.value; + const actualValue = props.value || '' const actualOnChange = (value) => { const ticket = tickets.find((x) => x.name === value); props.onChange(ticket.name); diff --git a/web_console/components/CommonJobList.jsx b/web_console/components/CommonJobList.jsx new file mode 100644 index 000000000..a9b0672da --- /dev/null +++ b/web_console/components/CommonJobList.jsx @@ -0,0 +1,747 @@ +import React, { useMemo, useState, useCallback } from 'react'; +import css from 'styled-jsx/css'; +import { Link, Text, Input, Fieldset, Button, Card, Description, useTheme, useInput, Tooltip } from '@zeit-ui/react'; +import AlertCircle from '@geist-ui/react-icons/alertCircle' +import Search from '@zeit-ui/react-icons/search'; +import NextLink from 'next/link'; +import useSWR from 'swr'; +import produce from 'immer' + +import { fetcher } from '../libs/http'; +import { FLAppStatus, handleStatus, getStatusColor, JobStatus } from '../utils/job'; +import Layout from '../components/Layout'; +import PopConfirm from '../components/PopConfirm'; +import Dot from '../components/Dot'; +import Empty from '../components/Empty'; +import { deleteJob, createJob } from '../services/job'; +import Form from '../components/Form'; +import { + JOB_DATA_JOIN_PARAMS, + JOB_NN_PARAMS, + JOB_PSI_DATA_JOIN_PARAMS, + JOB_TREE_PARAMS, + JOB_DATA_JOIN_REPLICA_TYPE, + JOB_NN_REPLICA_TYPE, + JOB_PSI_DATA_JOIN_REPLICA_TYPE, + JOB_TREE_REPLICA_TYPE, +} from '../constants/form-default' +import { getParsedValueFromData, fillJSON, getValueFromJson, getValueFromEnv, filterArrayValue } from '../utils/form_utils'; +import { getJobStatus } from '../utils/job' +import { JOB_TYPE_CLASS, JOB_TYPE } from '../constants/job' + +// import {mockJobList} from '../constants/mock_data' + +function useStyles(theme) { + return css` + .counts-wrap { + padding: 0 5%; + display: flex; + align-items: center; + justify-content: space-between; + } + + .num { + text-align: center; + color: ${theme.palette.accents_5}; + margin-bottom: 1em; + } + .h { + font-weight: normal; + margin: 1em 0; + } + .b { + color: ${theme.palette.accents_6}; + font-size: 1.4em; + } + + .list-wrap { + position: relative; + } + .filter-bar { + position: absolute; + right: 0; + top: 0; + display: flex; + align-items: center; + justify-content: flex-end; + } + .filter-form { + display: flex; + align-items: center; + margin-right: 10px; + } + .filter-input { + width: 200px; + margin-right: 10px; + } + + .content-list-wrap { + list-style: none; + padding: 0; + margin: 0; + } + .content-list { + padding: 20px 10px; + border-bottom: 1px solid ${theme.palette.border}; + } + .content-list:last-of-type { + border-bottom: none; + } + .desc-wrap { + display: flex; + } + `; +} + +const RESOURCE_PATH_PREFIX = 'spec.flReplicaSpecs.[replicaType].template.spec.containers[].resources' +const ENV_PATH = 'spec.flReplicaSpecs.[replicaType].template.spec.containers[].env' +const PARAMS_GROUP = ['client_params', 'server_params'] + +function handleParamData(container, data, field) { + if (field.type === 'label') { return } + + let path = field.path || field.key + let value = data + + if (/[/s/S]* num$/.test(field.key)) { + value = parseInt(value) + } + + fillJSON(container, path, value) +} + +function fillField(data, field) { + if (data === undefined) return field + + let isSetValueWithEmpty = false + let disabled = false + + let v = getValueFromJson(data, field.path || field.key) || field.emptyDefault || '' + + if (field.key === 'federation_id') { + const federationID = parseInt(localStorage.getItem('federationID')) + if (federationID > 0) { + v = federationID + disabled = true + } + } + else if (/[/s/S]* num$/.test(field.key)) { + let replicaType = field.key.split(' ')[0] + let path = `spec.flReplicaSpecs.${replicaType}.replicas` + v = getValueFromJson(data['client_params'], path) + || getValueFromJson(data['server_params'], path) + } + + if (typeof v === 'object' && v !== null) { + v = JSON.stringify(v, null, 2) + } + + if (v !== undefined || (v === undefined && isSetValueWithEmpty)) { + field.value = v + } + field.editing = true + + if (!field.props) field.props = {} + field.props.disabled = disabled + + return field +} + +let federationId = null, jobType = null + +const passFieldInfo = fields => produce(fields, draft => { + draft.map(field => { + if (field.key === 'client_ticket_name') { + field.props.job_type = jobType + } + if (field.key === 'server_ticket_name') { + field.props.federation_id = federationId + } + }) +}) + +function mapValueToFields({data, fields, targetGroup, type = 'form', init = false}) { + return produce(fields, draft => { + draft.map((x) => { + + if (x.groupName) { + if (!data[x.groupName]) return + if (!init && x.groupName !== targetGroup) return + + if (x.formTypes) { + let types = init ? x.formTypes : [type] + types.forEach(el => { + x.fields[el].forEach(field => fillField(data[x.groupName], field)) + }) + } else { + x.fields.forEach(field => fillField(data[x.groupName], field)) + } + + } else { + fillField(data, x) + } + + }); + }) +} + +let formMeta = {} +const setFormMeta = value => { formMeta = value } + +export default function JobList({ + datasoure, + training, + filter, + ...props +}) { + const theme = useTheme(); + const styles = useStyles(theme); + + let JOB_REPLICA_TYPE, NAME_KEY, FILTER_TYPES, PAGE_NAME, INIT_PARAMS, DEFAULT_JOB_TYPE + if (datasoure) { + + PAGE_NAME = 'datasource' + + JOB_REPLICA_TYPE = JOB_DATA_JOIN_REPLICA_TYPE + + NAME_KEY = 'DATA_SOURCE_NAME' + + FILTER_TYPES = JOB_TYPE_CLASS.datasource + + INIT_PARAMS = JOB_DATA_JOIN_PARAMS + + DEFAULT_JOB_TYPE = JOB_TYPE.data_join + + } else { + + PAGE_NAME = 'training' + + JOB_REPLICA_TYPE = JOB_NN_REPLICA_TYPE + + NAME_KEY = 'TRAINING_NAME' + + FILTER_TYPES = JOB_TYPE_CLASS.training + + INIT_PARAMS = JOB_NN_PARAMS + + DEFAULT_JOB_TYPE = JOB_TYPE.nn_model + + } + + filter = filter + || useCallback(job => FILTER_TYPES.some(type => type === job.localdata.job_type), []) + + const getParamsFormFields = useCallback(() => JOB_REPLICA_TYPE.reduce((total, currType) => { + total.push(...[ + { key: currType, type: 'label' }, + { + key: currType + '.env', + label: 'env', + type: 'name-value', + path: `spec.flReplicaSpecs.${currType}.template.spec.containers[].env`, + span: 24, + emptyDefault: [], + props: { + ignoreKeys: filterArrayValue([ + datasoure && 'DATA_SOURCE_NAME', + training && 'TRAINING_NAME', + ]) + } + }, + { + key: 'resoure.' + currType + '.cup_request', + label: 'cpu request', + path: RESOURCE_PATH_PREFIX.replace('[replicaType]', currType) + '.requests.cpu', + span: 12, + }, + { + key: 'resoure.' + currType + '.cup_limit', + label: 'cpu limit', + path: RESOURCE_PATH_PREFIX.replace('[replicaType]', currType) + '.limits.cpu', + span: 12, + }, + { + key: 'resoure.' + currType + '.memory_request', + label: 'memory request', + path: RESOURCE_PATH_PREFIX.replace('[replicaType]', currType) + '.requests.memory', + span: 12, + }, + { + key: 'resoure.' + currType + '.memory_limit', + label: 'memory limit', + path: RESOURCE_PATH_PREFIX.replace('[replicaType]', currType) + '.limits.memory', + span: 12, + }, + ]) + return total + }, []), [RESOURCE_PATH_PREFIX, JOB_REPLICA_TYPE]) + + const { data, mutate } = useSWR('jobs', fetcher); + const jobs = data && data.data ? data.data.filter(el => el.metadata).filter(filter) : null + // const jobs = mockJobList.data + + // form meta convert functions + const rewriteFields = useCallback((draft, data) => { + // this function will be call inner immer + // env + const insert2Env = filterArrayValue([ + { name: NAME_KEY, getValue: data => data.name }, + ]) + + PARAMS_GROUP.forEach(paramType => { + JOB_REPLICA_TYPE.forEach(replicaType => { + if (!draft[paramType]) { + draft[paramType] = {} + } + let envs = getValueFromJson(draft[paramType], ENV_PATH.replace('[replicaType]', replicaType)) + if (!envs) return + + let envNames = envs.map(env => env.name) + + insert2Env.forEach(el => { + let idx = envNames.indexOf(el.name) + let value = el.getValue(data) || '' + if (idx >= 0) { + envs[idx].value = value.toString() + } else { + // here envs is not extensible, push will throw error + envs = envs.concat({name: el.name, value: value.toString()}) + } + }) + + // trigger immer‘s intercepter + fillJSON(draft[paramType], ENV_PATH.replace('[replicaType]', replicaType), envs) + + // replicas + let path = `spec.flReplicaSpecs.${replicaType}.replicas` + if (replicaType !== 'Master') { + let num = parseInt(data[`${replicaType} num`]) + !isNaN(num) && fillJSON(draft[paramType], path, parseInt(data[`${replicaType} num`])) + } + + }) + }) + + // delete useless fields + + JOB_REPLICA_TYPE + .forEach(replicaType => + draft[`${replicaType} num`] && delete draft[`${replicaType} num`] + ) + + }, [JOB_REPLICA_TYPE]) + const mapFormMeta2FullData = useCallback((fields = fields) => { + let data = {} + fields.map((x) => { + if (x.groupName) { + data[x.groupName] = { ...formMeta[x.groupName] } + data[x.groupName][x.groupName] = formMeta[x.groupName] + } else { + data[x.key] = formMeta[x.key] + } + }) + return data + }, []) + const writeJson2FormMeta = useCallback((groupName, data) => { + setFormMeta(produce(formMeta, draft => { + fields.map((x) => { + if (x.groupName) { + if (x.groupName !== groupName) return + draft[groupName] = JSON.parse(data[groupName][groupName]) + } else { + draft[x.key] = getParsedValueFromData(data, x) || draft[x.key] + } + }) + + rewriteFields(draft, data) + })) + }, []) + const writeForm2FormMeta = useCallback((groupName, data) => { + setFormMeta(produce(formMeta, draft => { + let value + + fields.map(x => { + if (x.groupName) { + if (x.groupName !== groupName) return + if (!draft[groupName]) { draft[groupName] = {} } + + for (let field of getParamsFormFields()) { + value = getParsedValueFromData(data[groupName], field) + handleParamData(draft[groupName], value, field) + } + + } else { + value = getParsedValueFromData(data, x) || draft[x.key] + handleParamData(draft, value, x) + } + }) + rewriteFields(draft, data) + })) + }, []) + // ---end--- + const onJobTypeChange = useCallback((value, totalData, groupFormType) => { + writeFormMeta(totalData, groupFormType) + + switch (value) { + case JOB_TYPE.data_join: + JOB_REPLICA_TYPE = JOB_DATA_JOIN_REPLICA_TYPE + setFormMeta({...formMeta, ...JOB_DATA_JOIN_PARAMS}); break + case JOB_TYPE.psi_data_join: + JOB_REPLICA_TYPE = JOB_PSI_DATA_JOIN_REPLICA_TYPE + setFormMeta({...formMeta, ...JOB_PSI_DATA_JOIN_PARAMS}); break + case JOB_TYPE.nn_model: + JOB_REPLICA_TYPE = JOB_NN_REPLICA_TYPE + setFormMeta({...formMeta, ...JOB_NN_PARAMS}); break + case JOB_TYPE.tree_model: + JOB_REPLICA_TYPE = JOB_TREE_REPLICA_TYPE + setFormMeta({...formMeta, ...JOB_TREE_PARAMS}); break + } + + jobType = value + + setFields( + passFieldInfo(mapValueToFields({ + data: mapFormMeta2FullData(fields), + fields: getDefaultFields(), + init: true, + })) + ) + }, []) + const getDefaultFields = useCallback(() => filterArrayValue([ + { + key: 'name', + required: true, + }, + { + key: 'job_type', + type: 'jobType', + props: {type: PAGE_NAME}, + required: true, + label: ( + <> + job_type + change job type will reset all params}> + + + + ), + default: DEFAULT_JOB_TYPE, + onChange: onJobTypeChange, + }, + { + key: 'federation_id', + type: 'federation', + label: 'federation', + required: true, + onChange: (value, formData) => { + federationId = value + setFields(fields => passFieldInfo(mapValueToFields({data: formData, fields}))) + }, + props: { + initTrigerChange: true + } + }, + { + key: 'client_ticket_name', + type: 'clientTicket', + label: 'client_ticket', + props: { + type: PAGE_NAME, + }, + required: true + }, + { + key: 'server_ticket_name', + type: 'serverTicket', + label: 'server_ticket', + required: true, + props: { + federation_id: null, + type: PAGE_NAME, + }, + }, + ...JOB_REPLICA_TYPE + .filter(el => training && el !== 'Master') + .map(replicaType => ({ + key: `${replicaType} num`, + })), + ...PARAMS_GROUP.map(paramsType => ({ + groupName: paramsType, + initialVisible: false, + formTypes: ['form', 'json'], + onFormTypeChange: (data, currType, targetType) => { + let newFields + try { + if (targetType === 'json') { + writeForm2FormMeta(paramsType, data) + newFields = mapValueToFields({ + data: mapFormMeta2FullData(fields), + fields: fields.filter(el => el.groupName === paramsType), + targetGroup: paramsType, + type: 'json' + }) + } + if (targetType === 'form') { + writeJson2FormMeta(paramsType, data) + newFields = mapValueToFields({ + data: mapFormMeta2FullData(fields), + fields: fields.filter(el => el.groupName === paramsType), + targetGroup: paramsType, + type: 'form' + }) + } + } catch (error) { + return { error } + } + return { newFields } + }, + fields: { + form: getParamsFormFields(), + json: [ + { + key: paramsType, + type: 'json', + span: 24, + hideLabel: true, + props: { + minHeight: '500px' + }, + }, + ] + } + })) + ]), [JOB_REPLICA_TYPE]) + + let [fields, setFields] = useState(getDefaultFields()) + + const labeledList = useMemo(() => { + const allList = { name: 'All', list: jobs || [] }; + return Object.entries(FLAppStatus).reduce((prev, [key, status]) => { + return prev.concat({ + name: key, + list: allList.list.filter((item) => item.status?.appState === status), + }); + }, [allList]); + }, [jobs]); + + const [label, setLabel] = useState('All'); + const switchLabel = useCallback((l) => setLabel(l), []); + + const searchIcon = useMemo(() => , []); + const [filterText, setFilterText] = useState(''); + const { state: inputText, reset, bindings } = useInput(''); + const search = useCallback((e) => { + e.preventDefault(); + setFilterText(inputText); + }, [inputText]); + const resetSearch = useCallback(() => { + reset(); + setFilterText(''); + }, [reset, setFilterText]); + + const showingList = useMemo(() => { + const target = labeledList.find(({ name }) => name === label); + return ((target && target.list) || []).filter((item) => { + return !filterText || item.localdata.name.indexOf(filterText) > -1; + }); + }, [label, labeledList, filterText]); + + const [formVisible, setFormVisible] = useState(false); + + const onClickCreate = () => { + setFormMeta({...INIT_PARAMS}) + setFields(mapValueToFields({data: mapFormMeta2FullData(fields), fields, init: true})) + toggleForm() + } + const toggleForm = useCallback(() => { + if (formVisible) { + setFields(getDefaultFields()) + setFormMeta({}) + } + setFormVisible(visible => !visible) + }, [formVisible]); + const onOk = () => { + mutate(); + toggleForm(); + }; + + const writeFormMeta = (data, groupFormType) => { + PARAMS_GROUP.forEach(paramType => { + switch (groupFormType[paramType]) { + case 'json': + writeJson2FormMeta(paramType, data) + break + case 'form': + writeForm2FormMeta(paramType, data) + } + }) + } + const onCreateJob = (data, groupFormType) => { + writeFormMeta(data, groupFormType) + return createJob(formMeta); + }; + + const handleClone = (item) => { + setFormMeta(item.localdata) + + setFields(fields => mapValueToFields({ + data: mapFormMeta2FullData(fields), + fields, + type: 'form', + init: true + })) + + toggleForm() + } + + const renderOperation = item => ( + <> + + View Detail + + + View Charts + + handleClone(item)} + type="success" + style={{marginRight: `${theme.layout.gap}`}} + > + Clone + + deleteJob(item.localdata.id)} + onOk={() => mutate({ data: jobs.filter((i) => i !== item) })} + > + Delete + + + ) + + return ( +
+ + {formVisible + ? ( +
+ ) + : ( + <> + +
+ { + labeledList.map(({ name, list }) => ( +
+

{name}

+ {data ? list.length : '-'} +
+ )) + } +
+
+ +
+
+ + + + +
+ + { + labeledList.map(({ name }) => ( +
+ { + label === name + ? showingList.length + ? ( +
    + { + showingList.map((item) => ( +
  • + + {item.localdata.name} + +
    + + + {getJobStatus(item)} + + )} + /> + + + +
    +
  • + )) + } +
+ ) + : + : null + } +
+ )) + } +
+
+
+ + ) + } + + +
+
+ ); +} diff --git a/web_console/components/CommonTicket.jsx b/web_console/components/CommonTicket.jsx new file mode 100644 index 000000000..e46aeb819 --- /dev/null +++ b/web_console/components/CommonTicket.jsx @@ -0,0 +1,646 @@ +import React, { useState, useCallback, useMemo, useEffect } from 'react'; +import { Table, Button, Card, Text, Link, Tooltip, useTheme } from '@zeit-ui/react'; +import AlertCircle from '@geist-ui/react-icons/alertCircle' +import useSWR from 'swr'; +import produce from 'immer' +import Layout from './Layout'; +import Form from './Form'; +import { fetcher } from '../libs/http'; +import { createTicket, updateTicket } from '../services/ticket'; +import { + TICKET_DATA_JOIN_PARAMS, + TICKET_NN_PARAMS, + TICKET_PSI_DATA_JOIN_PARAMS, + TICKET_TREE_PARAMS, + TICKET_DATA_JOIN_REPLICA_TYPE, + TICKET_NN_REPLICA_TYPE, + TICKET_PSI_DATA_JOIN_REPLICA_TYPE, + TICKET_TREE_REPLICA_TYPE +} from '../constants/form-default' +import { getParsedValueFromData, fillJSON, getValueFromJson, filterArrayValue, getValueFromEnv } from '../utils/form_utils'; +import { JOB_TYPE_CLASS, JOB_TYPE } from '../constants/job' + +const ENV_PATH = 'spec.flReplicaSpecs.[replicaType].template.spec.containers[].env' +const PARAMS_GROUP = ['public_params', 'private_params'] + +const All_REPLICAS = Array.from(new Set([ + ...TICKET_DATA_JOIN_REPLICA_TYPE, + ...TICKET_NN_REPLICA_TYPE, + ...TICKET_PSI_DATA_JOIN_REPLICA_TYPE, + ...TICKET_TREE_REPLICA_TYPE +])) + +/** + * search value from params + * @param {*} path template string with `[replicaType]` and `[paramType]` + */ +function searchValue(data, path) { + let value + + for (let paramType of PARAMS_GROUP) { + for (let replicaType of All_REPLICAS) { + let targetPath = path + .replace(/\[paramType\]/g, paramType) + .replace(/\[replicaType\]/g, replicaType) + + value = getValueFromJson(data[paramType], targetPath) + + if (value) { + return value + } + } + } + return value +} +function searchEnvValue(data, key) { + let value + + for (let paramType of PARAMS_GROUP) { + for (let replicaType of All_REPLICAS) { + let path = ENV_PATH.replace('[replicaType]', replicaType) + value = getValueFromEnv(data[paramType] || {}, path, key) + if (value) { + return value + } + } + } + return value +} + +function fillField(data, field, editing) { + if (data === undefined && !editing) return field + + let isSetValueWithEmpty = false + let disabled = false + + let v = getValueFromJson(data, field.path || field.key) + + if (field.key === 'raw_data') { + v = searchEnvValue(data, 'RAW_DATA_SUB_DIR') + v = v.replace('portal_publish_dir/', '') + } + else if (field.key === 'name' && editing) { + disabled = true + } + else if (field.key === 'federation_id') { + const federationID = parseInt(localStorage.getItem('federationID')) + if (federationID > 0) { + v = federationID + disabled = true + } + } + else if (field.key === 'num_partitions') { + v = searchEnvValue(data, 'PARTITION_NUM') + } + else if (field.key === 'image') { + v = searchValue(data, field.path) + } + else if (field.type === 'bool-select') { + v = typeof v === 'boolean' ? v : true + } + else if (field.key === 'datasource') { + v = searchEnvValue(data, 'DATA_SOURCE') + } + else if (field.key === 'code_key') { + v = searchEnvValue(data, 'CODE_KEY') + } + else { + v = v || field.emptyDefault || '' + } + + if (typeof v === 'object' && v !== null) { + v = JSON.stringify(v, null, 2) + } + + if (v !== undefined || (v === undefined && isSetValueWithEmpty)) { + field.value = v + } + field.editing = true + + if (!field.props) field.props = {} + field.props.disabled = disabled + + return field +} + +/** + * editing: always write value to field + * init: this call is for init form value and will not pass any group + */ +function mapValueToFields({data, fields, targetGroup, type = 'form', editing = false, init = false}) { + return produce(fields, draft => { + draft.map((x) => { + if (x.groupName) { + editing && (init = true) + if (!data[x.groupName]) return + if (!init && x.groupName !== targetGroup) return + if (x.formTypes) { + let types = init ? x.formTypes : [type] + types.forEach(el => { + x.fields[el].forEach(field => fillField(data[x.groupName], field, editing)) + }) + } else { + x.fields.forEach(field => { + fillField(data[x.groupName], field, editing) + }) + } + } else { + fillField(data, x, editing) + } + }); + }) +} + +function handleParamData(container, data, field) { + if (field.type === 'label') { return } + + let path = field.path || field.key + let value = data + + fillJSON(container, path, value) +} + +let formMeta = {} +const setFormMeta = data => formMeta = data + +let jobType = '' + +export default function TicketList({ + datasoure, + training, + filter, + ...props +}) { + + const theme = useTheme(); + + let TICKET_REPLICA_TYPE, INIT_PARAMS, FILTER_TYPE, PAGE_NAME, DEFAULT_JOB_TYPE + if (datasoure) { + + PAGE_NAME = 'datasource' + + TICKET_REPLICA_TYPE = TICKET_DATA_JOIN_REPLICA_TYPE + + INIT_PARAMS = TICKET_DATA_JOIN_PARAMS + + FILTER_TYPE = JOB_TYPE_CLASS.datasource + + DEFAULT_JOB_TYPE = JOB_TYPE.data_join + + } else { + + PAGE_NAME = 'training' + + TICKET_REPLICA_TYPE = TICKET_NN_REPLICA_TYPE + + INIT_PARAMS = TICKET_NN_PARAMS + + FILTER_TYPE = JOB_TYPE_CLASS.training + + DEFAULT_JOB_TYPE = JOB_TYPE.nn_model + + } + + filter = filter + || useCallback(job => FILTER_TYPE.some(type => type === job.job_type), []) + + const { data, mutate } = useSWR('tickets', fetcher); + const tickets = data ? data.data.filter(filter) : null; + const columns = tickets && tickets.length > 0 + ? [ + ...Object.keys(tickets[0]).filter((x) => !['public_params', 'private_params', 'expire_time', 'created_at', 'updated_at', 'deleted_at'].includes(x)), + 'operation', + ] + : []; + + // rewrite functions + const rewriteEnvs = useCallback((draft, data, rules) => { + PARAMS_GROUP.forEach(paramType => { + TICKET_REPLICA_TYPE.forEach(replicaType => { + let envPath = ENV_PATH.replace('[replicaType]', replicaType) + + if (!draft[paramType]) { + draft[paramType] = {} + } + + let envs = getValueFromJson(draft[paramType], envPath) + if (!envs) { envs = [] } + + let envNames = envs.map(env => env.name) + rules.forEach(el => { + if (el.writeTo && !el.writeTo.some(x => x === replicaType)) return + + let idx = envNames.indexOf(el.name) + let value = el.getValue(data) || '' + if (idx >= 0) { + envs[idx].value = value.toString() + } else { + // here envs is not extensible, push will throw error + envs = envs.concat({name: el.name, value: value.toString()}) + } + }) + // trigger immer‘s intercepter + fillJSON(draft[paramType], envPath, envs) + }) + }) + }, []) + const commonRewrite = useCallback((draft, data) => { + TICKET_REPLICA_TYPE.forEach(replicaType => { + let path = `spec.flReplicaSpecs.${replicaType}.template.spec.containers[].image` + + PARAMS_GROUP.forEach(paramType => { + if (!draft[paramType]) { + draft[paramType] = {} + } + fillJSON(draft[paramType], path, data.image) + }) + }) + + // image + draft.image && delete draft.image + }, []) + const dataSourceRewrite = useCallback((draft, data) => { + // envs + const insert2Env = [ + { name: 'RAW_DATA_SUB_DIR', getValue: data => 'portal_publish_dir/' + (data.raw_data.name || data.raw_data) }, + { name: 'PARTITION_NUM', getValue: data => data.num_partitions }, + ] + rewriteEnvs(draft, data, insert2Env) + + // replicas + TICKET_REPLICA_TYPE.forEach(replicaType => { + let num = replicaType === 'Master' ? 1 : draft.num_partitions + + PARAMS_GROUP.forEach(paramType => { + fillJSON(draft[paramType], `spec.flReplicaSpecs.${replicaType}.replicas`, parseInt(num)) + }) + }) + + // delete fields + draft.raw_data && delete draft.raw_data + draft.num_partitions && delete draft.num_partitions + + }, []) + const trainingRewrite = useCallback((draft, data) => { + // envs + const insert2Env = [ + { name: 'DATA_SOURCE', getValue: data => data.datasource }, + { name: 'CODE_KEY', getValue: data => data.code_key, writeTo: ['Worker'] }, + ] + rewriteEnvs(draft, data, insert2Env) + + // delete fields + draft.datasource && delete draft.datasource + draft.code_key && delete draft.code_key + + }, []) + const rewriteFields = useCallback((draft, data) => { + // this function will be call inner immer + commonRewrite(draft, data) + if (datasoure) { + dataSourceRewrite(draft, data) + } + if (training) { + trainingRewrite(draft, data) + } + }, []) + // ---end--- + // form meta convert functions + const mapFormMeta2FullData = useCallback((fields = fields) => { + let data = {} + fields.map((x) => { + if (x.groupName) { + data[x.groupName] = { ...formMeta[x.groupName] } + data[x.groupName][x.groupName] = formMeta[x.groupName] + } else { + data[x.key] = formMeta[x.key] + } + }) + return data + }, []) + const writeJson2FormMeta = useCallback((groupName, data) => { + setFormMeta(produce(formMeta, draft => { + fields.map((x) => { + if (x.groupName === groupName) { + draft[groupName] = JSON.parse(data[groupName][groupName] || x.emptyDefault || '{}') + } else { + draft[x.key] = getParsedValueFromData(data, x) || draft[x.key] + } + }) + + rewriteFields(draft, data) + })) + }, []) + const writeForm2FormMeta = useCallback((groupName, data) => { + setFormMeta(produce(formMeta, draft => { + fields.map(x => { + if (x.groupName === groupName) { + if (!draft[groupName]) { draft[groupName] = {} } + + for (let field of getPublicParamsFields()) { + let value = getParsedValueFromData(data[groupName], field) + handleParamData(draft[groupName], value, field) + } + + } else { + draft[x.key] = getParsedValueFromData(data, x) + } + }) + rewriteFields(draft, data) + })) + }, []) + const formTypeChangeHandler = paramsType => (data, currType, targetType) => { + let newFields + try { + if (targetType === 'json') { + writeForm2FormMeta(paramsType, data) + newFields = mapValueToFields({ + data: mapFormMeta2FullData(fields), + fields: fields.filter(el => el.groupName === paramsType), + targetGroup: paramsType, + type: 'json' + }) + } + if (targetType === 'form') { + writeJson2FormMeta(paramsType, data) + newFields = mapValueToFields({ + data: mapFormMeta2FullData(fields), + fields: fields.filter(el => el.groupName === paramsType), + targetGroup: paramsType, + type: 'form' + }) + } + } catch (error) { + return { error } + } + return { newFields } + } + // --end--- + + const onJobTypeChange = useCallback((value, totalData, groupFormType) => { + jobType = value + writeFormMeta(totalData,groupFormType) + + switch (value) { + case JOB_TYPE.data_join: + TICKET_REPLICA_TYPE = TICKET_DATA_JOIN_REPLICA_TYPE + setFormMeta({...formMeta, ...TICKET_DATA_JOIN_PARAMS}); break + case JOB_TYPE.psi_data_join: + TICKET_REPLICA_TYPE = TICKET_PSI_DATA_JOIN_REPLICA_TYPE + setFormMeta({...formMeta, ...TICKET_PSI_DATA_JOIN_PARAMS}); break + case JOB_TYPE.nn_model: + TICKET_REPLICA_TYPE = TICKET_NN_REPLICA_TYPE + setFormMeta({...formMeta, ...TICKET_NN_PARAMS}); break + case JOB_TYPE.tree_model: + TICKET_REPLICA_TYPE = TICKET_TREE_REPLICA_TYPE + setFormMeta({...formMeta, ...TICKET_TREE_PARAMS}); break + } + + setFields( + mapValueToFields({ + data: mapFormMeta2FullData(fields), + fields: getDefauktFields(), + init: true, + }) + ) + }, []) + + const getPublicParamsFields = useCallback(() => TICKET_REPLICA_TYPE.reduce( + (total, replicaType) => { + const replicaKey = key => `${replicaType}.${key}` + + total.push(...[ + { key: replicaType, type: 'label' }, + { + key: replicaKey('pair'), + label: 'pair', + type: 'bool-select', + path: `spec.flReplicaSpecs.${replicaType}.pair`, + }, + { + key: replicaKey('env'), + label: 'env', + type: 'name-value', + path: `spec.flReplicaSpecs.${replicaType}.template.spec.containers[].env`, + emptyDefault: [], + // props: { + // ignoreKeys: ['PARTITION_NUM', 'CODE_KEY'] + // }, + span: 24, + }, + { + key: replicaKey('command'), + label: 'command', + type: 'json', + path: `spec.flReplicaSpecs.${replicaType}.template.spec.containers[].command`, + emptyDefault: [], + span: 24, + }, + { + key: replicaKey('args'), + label: 'args', + type: 'json', + path: `spec.flReplicaSpecs.${replicaType}.template.spec.containers[].args`, + emptyDefault: [], + span: 24, + } + ]) + return total + }, + [] + ), [TICKET_REPLICA_TYPE]) + + const getDefauktFields = useCallback(() => filterArrayValue([ + { key: 'name', required: true }, + { key: 'federation_id', type: 'federation', label: 'federation', required: true }, + { + key: 'job_type', + type: 'jobType', + label: ( + <> + job_type + change job type will reset all params}> + + + + ), + props: {type: PAGE_NAME}, + required: true, + default: DEFAULT_JOB_TYPE, + onChange: onJobTypeChange, + }, + { key: 'role', type: 'jobRole', required: true }, + { key: 'expire_time' }, + { + key: 'image', + required: true, + path: 'spec.flReplicaSpecs.[replicaType].template.spec.containers[].image', + props: { width: '100%' } + }, + datasoure && { + key: 'raw_data', + type: 'rawData', + callback: updateForm => + value => updateForm('num_partitions', value?.output_partition_num), + }, + datasoure && { + key: 'num_partitions', + label: 'num partitions', + }, + training && { + key: 'datasource', + type: 'datasource', + required: true, + }, + (training && jobType === JOB_TYPE.nn_model) ? { + key: 'code_key', + props: { width: '100%' } + } : undefined, + { key: 'remark', type: 'text', span: 24 }, + { + groupName: 'public_params', + initialVisible: false, + onFormTypeChange: formTypeChangeHandler('public_params'), + formTypes: ['form', 'json'], + fields: { + form: getPublicParamsFields(), + json: [ + { + key: 'public_params', + type: 'json', + span: 24, + hideLabel: true, + props: { + minHeight: '500px' + }, + }, + ] + } + }, + { + groupName: 'private_params', + initialVisible: false, + fields: [ + { + key: 'private_params', + type: 'json', + span: 24, + hideLabel: true, + emptyDefault: {}, + props: { + minHeight: '500px' + }, + }, + ] + } + ]), [TICKET_REPLICA_TYPE, training, datasoure, jobType]) + const [formVisible, setFormVisible] = useState(false); + const [fields, setFields] = useState(getDefauktFields()); + const [currentTicket, setCurrentTicket] = useState(null); + const title = currentTicket ? `Edit Ticket: ${currentTicket.name}` : 'Create Ticket'; + const closeForm = () => { + setCurrentTicket(null) + setFormMeta({}) + setFormVisible(!formVisible) + }; + const onCreate = () => { + setFormMeta({ ...INIT_PARAMS }) + setFields(mapValueToFields({data: formMeta, fields: getDefauktFields(), init: true})) + setFormVisible(true); + } + const onOk = (ticket) => { + mutate({ + data: [...tickets, ticket], + }); + closeForm(); + }; + const handleEdit = (ticket) => { + setCurrentTicket(ticket); + setFormMeta(ticket) + + jobType = ticket.job_type + + setFields(mapValueToFields({data: ticket, fields: getDefauktFields(), editing: true})); + setFormVisible(true); + }; + + const handleClone = (ticket) => { + setFormMeta(ticket) + + jobType = ticket.job_type + + setFields(mapValueToFields({data: ticket, fields: getDefauktFields(), init: true})); + setFormVisible(true); + } + + const writeFormMeta = (data, formTypes) => { + const writer = formTypes['public_params'] === 'json' + ? writeJson2FormMeta : writeForm2FormMeta + writer('public_params', data) + + writeJson2FormMeta('private_params', data) + } + const handleSubmit = (data, formTypes) => { + writeFormMeta(data, formTypes) + + if (currentTicket) { + return updateTicket(currentTicket.id, formMeta); + } + + return createTicket(formMeta); + }; + const operation = (actions, rowData) => { + const onHandleEdit = (e) => { + e.preventDefault(); + handleEdit(rowData.rowValue); + }; + const onHandleClone = (e) => { + e.preventDefault(); + handleClone(rowData.rowValue); + }; + return <> + + Clone + + Edit + + }; + const dataSource = tickets + ? tickets.map((x) => ({ ...x, operation })) + : []; + + return ( + + {formVisible + ? ( +
+ ) + : ( + <> +
+ Tickets + +
+ {tickets && ( + + + {columns.map((x) => )} +
+
+ )} + + )} + + ); +} diff --git a/web_console/components/DataSourceSelect.jsx b/web_console/components/DataSourceSelect.jsx new file mode 100644 index 000000000..5a8bd8dde --- /dev/null +++ b/web_console/components/DataSourceSelect.jsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Select, Popover } from '@zeit-ui/react'; +import useSWR from 'swr'; +import { fetcher } from '../libs/http'; +import { JOB_TYPE_CLASS } from '../constants/job' + +export default function DataSourceSelect({type, ...props}) { + + const filter = el => JOB_TYPE_CLASS.datasource.some(t => el.localdata?.job_type === t) + + const { data } = useSWR(`jobs`, fetcher); + const jobs = data?.data?.filter(filter) || [] + + const actualValue = jobs.find((x) => x.localdata?.name === props.value)?.localdata?.name; + const actualOnChange = (value) => { + props.onChange(value); + }; + + return ( + + ); +} diff --git a/web_console/components/FederationSelect.jsx b/web_console/components/FederationSelect.jsx index fd690bec9..184e9224c 100644 --- a/web_console/components/FederationSelect.jsx +++ b/web_console/components/FederationSelect.jsx @@ -1,8 +1,10 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { Select } from '@zeit-ui/react'; import useSWR from 'swr'; import { fetcher } from '../libs/http'; +let rendered = false + export default function FederationSelect(props) { const { data } = useSWR('federations', fetcher); const federations = data ? data.data : []; @@ -12,9 +14,20 @@ export default function FederationSelect(props) { const federation = federations.find((x) => x.name === value); props.onChange(federation.id); }; + + !rendered + && props.initTrigerChange + && props.value + && props.onChange(props.value) + + rendered = true + + // reset flag + useEffect(() => () => rendered = false, []) + return ( ); } diff --git a/web_console/components/Form.jsx b/web_console/components/Form.jsx index 5386e3bc0..fdc8a0159 100644 --- a/web_console/components/Form.jsx +++ b/web_console/components/Form.jsx @@ -1,4 +1,4 @@ -import React, { useState, useReducer, useMemo } from 'react'; +import React, { useState, useReducer, useMemo, useCallback, useEffect, useRef } from 'react'; import css from 'styled-jsx/css'; import { Button, ButtonGroup, Card, Grid, Text, Input, Toggle, Textarea, Note, useTheme, Collapse, useToasts, Select } from '@zeit-ui/react'; import FederationSelect from './FederationSelect'; @@ -8,6 +8,10 @@ import ServerTicketSelect from './ServerTicketSelect'; import ClientTicketSelect from './ClientTicketSelect'; import DataPortalTypeSelect from './DataPortalTypeSelect'; import NameValueInput from './NameValueGroup'; +import RawDataSelect from './RawDataSelect' +import BooleanSelect from './BooleanSelect' +import DataSourceSelect from './DataSourceSelect' +import produce from 'immer' function useStyles() { return css` @@ -20,6 +24,81 @@ function useStyles() { `; } +const formatGroupFieldKey = (groupName, key) => `[group:${groupName}].${key}` + +// mark group name to handle conflicts +// TODO: handle conflict between form types in one group +const handleFieldsToRender = fields => produce(fields, draft => { + draft.forEach(curr => { + if (!curr.groupName) return + + const handleGroupFields = fields => { + fields.forEach(field => { + if (field.type === 'label') return + field.label = field.label || field.key + field.key = formatGroupFieldKey(curr.groupName, field.key) + }) + } + if (Array.isArray(curr.fields)) { + handleGroupFields(curr.fields) + } else { + Object.values(curr.fields).forEach(el => handleGroupFields(el)) + } + }) +}) + +function deepEqual(x, y) { + if (x === y) { + return true; + } + if (!(typeof x == "object" && x != null) || !(typeof y == "object" && y != null)){ + return false; + } + if (Object.keys(x).length != Object.keys(y).length){ + return false; + } + for (var prop in x) { + if (y.hasOwnProperty(prop)) + { + if (!deepEqual(x[prop], y[prop])){ + return false; + } + } + else{ + return false; + } + } + return true; +} + +const mapFields2Form = (fields, groupType) => { + // flat all group fileds + fields = fields.reduce((total, curr) => { + if (curr.groupName) { + if (Array.isArray(curr.fields)) { + total.push(...curr.fields) + } else { + groupType + ? total.push(...curr.fields[groupType]) + : Object.values(curr.fields).forEach(el => total.push(...el)) + } + } else { + total.push(curr) + } + return total + }, []) + const formData = fields.reduce((total, current) => { + total[current.key] = current.hasOwnProperty('value') + ? current.value + : current.value || current.default || ''; + return total; + }, {}) + return [fields, formData] +} + +const groupFormType = {} +let fieldsCache + /** * interface IField { * key: string; @@ -29,9 +108,9 @@ function useStyles() { * required?: boolean; * span?: number; // Grid layout prop * props?: any; + * callback?: (updateForm: function) => (value: any) => any * } */ - export default function Form({ title, onOk, onSubmit, onCancel, gap = 2, fields = [], okText = 'Submit', cancelText = 'Cancel', @@ -39,44 +118,38 @@ export default function Form({ }) { // cache raw fields data const rawFields = fields - // TODO: handle name confilicts + const fieldsToRender = handleFieldsToRender(fields) + + useEffect(() => { + rawFields.forEach(field => { + if (field.groupName && field.formTypes) { + groupFormType[field.groupName] = field.formTypes[0] + } + }) + }, []) - const groupFormType = useMemo(() => ({}), []) const theme = useTheme(); const styles = useStyles(theme); - const mapFields2Form = fields => { - // flat all group fileds - let formFields = fields.reduce((total, curr) => { - if (curr.groupName) { - if (Array.isArray(curr.fields)) { - total.push(...curr.fields) - } else { - Object.values(curr.fields).forEach(el => total.push(...el)) - } - } else { - total.push(curr) - } - return total - }, []) - const formData = formFields.reduce((total, current) => { - total[current.key] = current.hasOwnProperty('value') - ? current.value - : current.value || current.default; - return total; - }, {}) - return [formFields, formData] - } - let [formFields, formData] = mapFields2Form(fields) + let formData + [fields, formData] = mapFields2Form(fieldsToRender) const [form, setForm] = useState(formData); + // update form value in rendering + useEffect(() => { + if (!deepEqual(fields, fieldsCache)) { + fieldsCache = fields + setForm(formData) + } + }) + const getFormatFormData = () => rawFields.reduce((total, curr) => { if (curr.groupName) { total[curr.groupName] = {} const fillGroupFields = fields => { for (let field of fields) { - total[curr.groupName][field.key] = form[field.key] + total[curr.groupName][field.key] = form[formatGroupFieldKey(curr.groupName, field.key)] } } // handle multi formType @@ -93,15 +166,15 @@ export default function Form({ return total }, {}) - const disabled = formFields.filter((x) => x.required).some((x) => !form[x.key]); + const disabled = fields.filter((x) => x.required).some((x) => !form[x.key]); const updateForm = (key, value) => { - const data = { + setForm(form => ({ ...form, [key]: value, - }; - setForm(data); + })); }; - const renderField = ({ key, label, props, type, onChange, hideLabel }) => { + + const renderField = ({ key, label, props, type, onChange, hideLabel, callback }) => { const valueProps = { ...props, style: { @@ -157,9 +230,11 @@ export default function Form({ { - updateForm(key, value); + updateForm(key, value) + let formData = getFormatFormData() + formData[key] = value if (onChange) { - onChange(value); + onChange(value, formData); } }} {...valueProps} @@ -176,7 +251,14 @@ export default function Form({
updateForm(key, value)} + onChange={(value) => { + updateForm(key, value) + let data = getFormatFormData() + data[key] = value + if (onChange) { + onChange(value, data, groupFormType); + } + }} {...valueProps} />
@@ -264,6 +346,7 @@ export default function Form({ updateForm(key, value)} + {...valueProps} /> + : + : + } + + : null + ); +} diff --git a/web_console/components/JobCommonInfo.jsx b/web_console/components/JobCommonInfo.jsx index dff2f937c..c793cfebe 100644 --- a/web_console/components/JobCommonInfo.jsx +++ b/web_console/components/JobCommonInfo.jsx @@ -1,10 +1,10 @@ import React, { useMemo } from 'react'; import css from 'styled-jsx/css'; -import { Table, Link, Text, Card, Description, Popover, useTheme } from '@zeit-ui/react'; +import { Table, Link, Text, Card, Description, Popover, useTheme, Button } from '@zeit-ui/react'; import useSWR from 'swr'; import { fetcher } from '../libs/http'; -import { getStatusColor, handleStatus, FLAppStatus } from '../utils/job'; +import { getStatusColor, handleStatus, FLAppStatus, getJobStatus } from '../utils/job'; import Layout from './Layout'; import Dot from './Dot'; import Empty from './Empty'; @@ -100,19 +100,21 @@ export default function JobCommonInfo(props) { ? (logsData && logsData.data) ? logsData.data : ['logs error ' + (logsData?.error || logsError?.message)] : null; + const status = props.jobStatus || getJobStatus(job) + const tableData = useMemo(() => { if (pods) { return pods.map((item) => ({ status: item.status, pod: item.name.replace( - `${job.localdata?.name}-${job.spec?.role.toLowerCase()}-${item.type.toLowerCase()}-`, + `${job?.localdata?.name}-${job?.spec?.role.toLowerCase()}-${item.type.toLowerCase()}-`, '', ), type: item.type, link: ( <> { - job.status?.appState === FLAppStatus.Running && item.status === podStatus.active + job?.status?.appState === FLAppStatus.Running && item.status === podStatus.active ? ( - - {handleStatus(job?.status?.appState) || '-'} + + {status || '-'} )} /> diff --git a/web_console/components/JobTypeSelect.jsx b/web_console/components/JobTypeSelect.jsx index 0a112258f..4166a4395 100644 --- a/web_console/components/JobTypeSelect.jsx +++ b/web_console/components/JobTypeSelect.jsx @@ -1,11 +1,12 @@ import React from 'react'; import { Select } from '@zeit-ui/react'; -import { JOB_TYPE } from '../constants/job'; +import { JOB_TYPE_CLASS } from '../constants/job'; export default function JobTypeSelect(props) { - const options = JOB_TYPE.map((x) => ({ label: x, value: x })); + const types = props.type ? JOB_TYPE_CLASS[props.type] : JOB_TYPE_CLASS.all + const options = types.map((x) => ({ label: x, value: x })); return ( - {options.map((x) => {x.label})} ); diff --git a/web_console/components/NameValueGroup.jsx b/web_console/components/NameValueGroup.jsx index 047368695..5518f2331 100644 --- a/web_console/components/NameValueGroup.jsx +++ b/web_console/components/NameValueGroup.jsx @@ -69,7 +69,7 @@ function NameValuePair({value, onChange, onDelete, ...props}) { ) } -export default function NameValueInput({value, onChange, ...props}) { +export default function NameValueInput({value, onChange, ignoreKeys = [], ...props}) { let value_ = JSON.parse(value || '[]') const onItemChange = (idx, name, value) => { const copy = value_ @@ -85,7 +85,9 @@ export default function NameValueInput({value, onChange, ...props}) {
{ value_.map((el, idx) => - el.name === key) + ? undefined + : onItemChange(idx, name, value)} diff --git a/web_console/components/RawDataSelect.jsx b/web_console/components/RawDataSelect.jsx new file mode 100644 index 000000000..fbb020ec3 --- /dev/null +++ b/web_console/components/RawDataSelect.jsx @@ -0,0 +1,33 @@ +import React, { useEffect } from 'react'; +import { Select } from '@zeit-ui/react'; +import useSWR from 'swr'; +import { fetcher } from '../libs/http'; + +let initialUpdated = false + +export default function RawDataSelect(props) { + const { data } = useSWR('raw_datas', fetcher); + const rawDatas = data ? data.data : []; + const actualValue = typeof props.value === 'string' + ? props.value : props.value?.name + const actualOnChange = (value) => { + const rawData = rawDatas.find((x) => x.name === value); + props.onChange(rawData); + }; + + if (!initialUpdated) { + typeof props.value === 'string' + && rawDatas.length > 0 + && actualOnChange(props.value) + + initialUpdated = true + } + + useEffect(() => () => initialUpdated = false, []) + + return ( + + ); +} diff --git a/web_console/components/ServerTicketSelect.jsx b/web_console/components/ServerTicketSelect.jsx index 9a3d69b7e..27ede5d63 100644 --- a/web_console/components/ServerTicketSelect.jsx +++ b/web_console/components/ServerTicketSelect.jsx @@ -2,23 +2,58 @@ import React from 'react'; import { Select, Popover, Code } from '@zeit-ui/react'; import useSWR from 'swr'; import { fetcher } from '../libs/http'; +import { JOB_TYPE_CLASS } from '../constants/job' + +// const mockRes = { +// "data": [ +// { +// "name": "ticket-360-k4", +// "job_type": "data_join", +// "role": "Follower", +// "sdk_version": "049ad50", +// "expire_time": "Fri Jun 18 2021 00:00:00 GMT+0000 (Coordinated Universal Time)", +// "remark": "", +// "public_params": "null" +// }, +// { +// "name": "test-training", +// "job_type": "nn_model", +// "role": "Follower", +// "sdk_version": "049ad50", +// "expire_time": "Fri Jun 18 2021 00:00:00 GMT+0000 (Coordinated Universal Time)", +// "remark": "", +// "public_params": "null" +// } +// ] +// } + +let filter = () => true +export default function ServerTicketSelect({type, ...props}) { + + if (type) { + filter = el => JOB_TYPE_CLASS[type].some(t => el.job_type === t) + } -export default function ServerTicketSelect(props) { const { data } = useSWR( props.federation_id ? `federations/${props.federation_id}/tickets` : null, fetcher, ); - const tickets = (data && data.data) || []; - const actualValue = tickets.find((x) => x.name === props.value)?.value; + const tickets = data?.data?.filter(filter) || [] + // const tickets = mockRes.data; + + const actualValue = tickets.find((x) => x.name === props.value)?.name; const actualOnChange = (value) => { const ticket = tickets.find((x) => x.name === value); props.onChange(ticket.name); }; const popoverContent = (content) => { + if (typeof content.public_params === 'string') { + content.public_params = JSON.parse(content.public_params || '{}') + } return (
-        {JSON.stringify(JSON.parse(content || '{}'), null, 2)}
+        {JSON.stringify(content, null, 2)}
         
@@ -74,11 +115,10 @@ function FederationItem({ data, onEdit }) {
 }
 
 const K8S_SETTINGS_FIELDS = [
-  { key: 'namespace', default: K8S_SETTINGS.namespace },
-  { key: 'storage_root_path', default: K8S_SETTINGS.storage_root_path },
+  { key: 'namespace' },
+  { key: 'storage_root_path' },
   {
     key: 'imagePullSecrets',
-    default: K8S_SETTINGS.imagePullSecrets,
     type: 'json',
     span: 24,
     path: 'global_replica_spec.template.spec.imagePullSecrets',
@@ -88,7 +128,6 @@ const K8S_SETTINGS_FIELDS = [
     key: 'env',
     type: 'name-value',
     span: 24,
-    default: K8S_SETTINGS.env,
     props: {
       minHeight: '150px',
     },
@@ -99,7 +138,6 @@ const K8S_SETTINGS_FIELDS = [
     key: 'grpc_spec',
     type: 'json',
     span: 24,
-    default: K8S_SETTINGS.grpc_spec,
     props: {
       minHeight: '150px',
     },
@@ -109,7 +147,6 @@ const K8S_SETTINGS_FIELDS = [
     key: 'leader_peer_spec',
     type: 'json',
     span: 24,
-    default: K8S_SETTINGS.leader_peer_spec,
     props: {
       minHeight: '150px',
     },
@@ -119,7 +156,6 @@ const K8S_SETTINGS_FIELDS = [
     key: 'follower_peer_spec',
     type: 'json',
     span: 24,
-    default: K8S_SETTINGS.follower_peer_spec,
     props: {
       minHeight: '150px',
     },
@@ -132,7 +168,7 @@ const K8S_SETTINGS_FIELDS = [
  */
 function fillField(data, field, edit=false) {
   let v = getValueFromJson(data, field.path || field.key)
-  if (typeof v === 'object') {
+  if (typeof v === 'object'  && v !== null) {
     v = JSON.stringify(v, null, 2)
   }
   field.value = v
@@ -228,7 +264,7 @@ export default function FederationList() {
           if (!draft.k8s_settings) { draft.k8s_settings = {} }
           for (let field of K8S_SETTINGS_FIELDS) {
             fillJSON(
-              draft.k8s_settings, field.path || [field.key],
+              draft.k8s_settings, field.path || field.key,
               getParsedValueFromData(data.k8s_settings, field)
             )
           }
@@ -259,10 +295,10 @@ export default function FederationList() {
     }
     return { newFields }
   }
-  const DEFAULT_FIELDS = [
+  const DEFAULT_FIELDS = useMemo(() => [
     { key: 'name', required: true },
-    { key: 'x-federation' },
     { key: 'trademark' },
+    { key: 'x-federation' },
     { key: 'email' },
     { key: 'tel', label: 'telephone' },
     { key: 'avatar' },
@@ -285,9 +321,14 @@ export default function FederationList() {
         ]
       },
     },
-  ];
+  ], []);
   const [fields, setFields] = useState(DEFAULT_FIELDS);
 
+  const onClickCreate = () => {
+    setFormMeta({ k8s_settings: K8S_SETTINGS })
+    setFields(mapValueToFields({federation: mapFormMeta2Form(), fields}))
+    setFormVisible(true)
+  }
   const toggleForm = () => {
     setFormVisible(!formVisible);
     if (formVisible) {
@@ -333,10 +374,12 @@ export default function FederationList() {
   }
 
   const handleSubmit = (value, groupFormType) => {
-    for (let field of K8S_SETTINGS_FIELDS) {
-      let error = checkK8sSetting(field, value.k8s_settings[field.key])
-      if (error) {
-        return {error}
+    if (groupFormType.k8s_settings === 'form') {
+      for (let field of K8S_SETTINGS_FIELDS) {
+        let error = checkK8sSetting(field, value.k8s_settings[field.key])
+        if (error) {
+          return {error}
+        }
       }
     }
 
@@ -367,7 +410,7 @@ export default function FederationList() {
           <>
             
Federations - +
{federations && federations.map((x) => ( diff --git a/web_console/pages/job/charts/[id].jsx b/web_console/pages/charts/[id].jsx similarity index 88% rename from web_console/pages/job/charts/[id].jsx rename to web_console/pages/charts/[id].jsx index 1813ea005..4ff807f77 100644 --- a/web_console/pages/job/charts/[id].jsx +++ b/web_console/pages/charts/[id].jsx @@ -3,9 +3,9 @@ import useSWR from 'swr'; import { useRouter } from 'next/router'; import css from 'styled-jsx/css'; import { Text, Card, Grid } from '@zeit-ui/react'; -import { fetcher } from '../../../libs/http'; -import Layout from '../../../components/Layout'; -import getJobDashboardUrls from '../../../utils/kibana'; +import { fetcher } from '../../libs/http'; +import Layout from '../../components/Layout'; +import getJobDashboardUrls from '../../utils/kibana'; function useStyle() { return css` diff --git a/web_console/pages/datasource/job/[id].jsx b/web_console/pages/datasource/job/[id].jsx new file mode 100644 index 000000000..c6c67993b --- /dev/null +++ b/web_console/pages/datasource/job/[id].jsx @@ -0,0 +1,9 @@ +import React from 'react'; +import { useRouter } from 'next/router'; + +import Job from '../../../components/Job'; + +export default function Jobs() { + const router = useRouter(); + return +} diff --git a/web_console/pages/datasource/job/index.jsx b/web_console/pages/datasource/job/index.jsx new file mode 100644 index 000000000..7a038ee26 --- /dev/null +++ b/web_console/pages/datasource/job/index.jsx @@ -0,0 +1,5 @@ +import CommonJobList from '../../../components/CommonJobList' + +export default function JobList() { + return +} \ No newline at end of file diff --git a/web_console/pages/datasource/tickets/index.jsx b/web_console/pages/datasource/tickets/index.jsx new file mode 100644 index 000000000..5e8be8d9c --- /dev/null +++ b/web_console/pages/datasource/tickets/index.jsx @@ -0,0 +1,5 @@ +import CommonTicket from '../../../components/CommonTicket' + +export default function Ticket() { + return +} \ No newline at end of file diff --git a/web_console/pages/index.jsx b/web_console/pages/index.jsx index fc495aab8..e6e2c0a07 100644 --- a/web_console/pages/index.jsx +++ b/web_console/pages/index.jsx @@ -69,7 +69,7 @@ export default function Overview() { const router = useRouter(); const { data } = useSWR('jobs', fetcher); const jobs = data ? data.data.filter((x) => x.metadata) : []; - const goToJob = () => router.push('/job'); + const goToJob = () => router.push('/datasource/job'); return (
diff --git a/web_console/pages/job/[id].jsx b/web_console/pages/job/[id].jsx index c77c0f79f..a3bbc1d8a 100644 --- a/web_console/pages/job/[id].jsx +++ b/web_console/pages/job/[id].jsx @@ -13,7 +13,7 @@ export default function Job() { const job = jobData ? jobData.data : null; return ( - + job ? + : null ); } diff --git a/web_console/pages/raw_data/[id].jsx b/web_console/pages/raw_data/[id].jsx index 11df26a74..c0203930e 100644 --- a/web_console/pages/raw_data/[id].jsx +++ b/web_console/pages/raw_data/[id].jsx @@ -43,7 +43,7 @@ export default function RawDataJob() { }, [rawData?.localdata?.id]); return ( - + rawData ? Delete Job + : null ); } diff --git a/web_console/pages/raw_data/index.jsx b/web_console/pages/raw_data/index.jsx index 0f701a78d..0124f35cf 100644 --- a/web_console/pages/raw_data/index.jsx +++ b/web_console/pages/raw_data/index.jsx @@ -1,5 +1,5 @@ -import React, { useState } from 'react'; -import { Table, Button, Card, Text, Link } from '@zeit-ui/react'; +import React, { useState, useMemo } from 'react'; +import { Table, Button, Card, Text, Link, useTheme } from '@zeit-ui/react'; import NextLink from 'next/link'; import useSWR from 'swr'; import produce from 'immer' @@ -16,18 +16,19 @@ const RESOURCE_PATH_PREFIX = 'yaml_spec.spec.flReplicaSpecs.[replicaType].templa const IMAGE_PATH = 'yaml_spec.spec.flReplicaSpecs.[replicaType].template.spec.containers[].image' const WORKER_REPLICAS_PATH = 'yaml_spec.spec.flReplicaSpecs.Worker.replicas' +const REPLICA_TYPES = ['Master', 'Worker'] + const DATA_FORMAT_OPTIONS = [ { label: 'TF_RECORD', value: 'TF_RECORD' }, { label: 'CSV_DICT', value: 'CSV_DICT' }, ] const CONTEXT_FIELDS = [ - { key: 'file_wildcard', default: '*', default: RAW_DATA_CONTEXT.file_wildcard }, + { key: 'file_wildcard' }, { key: 'input_data_format', type: 'select', required: true, - default: RAW_DATA_CONTEXT.input_data_format, props: { options: DATA_FORMAT_OPTIONS } @@ -36,7 +37,6 @@ const CONTEXT_FIELDS = [ key: 'output_data_format', type: 'select', required: true, - default: RAW_DATA_CONTEXT.output_data_format, props: { options: DATA_FORMAT_OPTIONS } @@ -45,7 +45,6 @@ const CONTEXT_FIELDS = [ key: 'compressed_type', type: 'select', required: true, - default: RAW_DATA_CONTEXT.compressed_type, props: { options: [ { label: 'GZIP', value: 'GZIP' }, @@ -54,36 +53,32 @@ const CONTEXT_FIELDS = [ ] } }, - { key: 'batch_size', default: RAW_DATA_CONTEXT.batch_size }, - { key: 'max_flying_item', default: RAW_DATA_CONTEXT.max_flying_item }, - { key: 'write_buffer_size', default: RAW_DATA_CONTEXT.write_buffer_size }, + { key: 'batch_size' }, + { key: 'max_flying_item' }, + { key: 'write_buffer_size' }, { key: 'Master Resources', type: 'label', span: 24}, { key: 'resource.Master.cpu_request', label: 'cpu request', path: RESOURCE_PATH_PREFIX + '.requests.cpu', - default: RAW_DATA_CONTEXT.resource_master_cpu_request, span: 12, }, { key: 'resource.Master.cpu_limit', label: 'cpu limit', path: RESOURCE_PATH_PREFIX + '.limits.cpu', - default: RAW_DATA_CONTEXT.resource_master_cpu_limit, span: 12 }, { key: 'resource.Master.memory_request', label: 'memory request', path: RESOURCE_PATH_PREFIX + '.requests.memory', - default: RAW_DATA_CONTEXT.resource_master_memory_request, span: 12, }, { key: 'resource.Master.memory_limit', label: 'memory limit', path: RESOURCE_PATH_PREFIX + '.limits.memory', - default: RAW_DATA_CONTEXT.resource_master_memory_limit, span: 12 }, { key: 'worker Resources', type: 'label', span: 24 }, @@ -91,39 +86,37 @@ const CONTEXT_FIELDS = [ key: 'resource.Worker.cpu_request', label: 'cpu request', path: RESOURCE_PATH_PREFIX + '.limits.cpu', - default: RAW_DATA_CONTEXT.resource_master_cpu_request, span: 12 }, { key: 'resource.Worker.cpu_limit', label: 'cpu limit', path: RESOURCE_PATH_PREFIX + '.limits.cpu', - default: RAW_DATA_CONTEXT.resource_master_cpu_limit, span: 12 }, { key: 'resource.Worker.memory_request', label: 'memory request', path: RESOURCE_PATH_PREFIX + '.requests.memory', - default: RAW_DATA_CONTEXT.resource_master_memory_request, span: 12, }, { key: 'resource.Worker.memory_limit', label: 'memory limit', path: RESOURCE_PATH_PREFIX + '.limits.memory', - default: RAW_DATA_CONTEXT.resource_master_memory_limit, span: 12 }, { key: 'num_workers', label: 'num workers', span: 12, - default: 4, path: WORKER_REPLICAS_PATH }, ] +/** + * write context data to form meta + */ function handleContextData(container, data, field) { if (field.type === 'label') { return } @@ -140,7 +133,7 @@ function handleContextData(container, data, field) { } else if (field.key === 'num_workers') { - value = parseInt(value || field.default) + value = parseInt(value) } fillJSON(container, path, value) @@ -156,16 +149,23 @@ function fillField(data, field) { const [, replicaType,] = field.key.split('.') v = getValueFromJson(data, field.path.replace('[replicaType]', replicaType)) } - else if (field.key === 'compressed_type') { v = v === '' ? 'None' : data.compressed_type } + else if (field.key === 'image') { + for (let replicaType of REPLICA_TYPES) { + v = getValueFromJson(data.context, IMAGE_PATH.replace('[replicaType]', replicaType)) + if (v) break + } + } - if (typeof v === 'object') { + if (typeof v === 'object' && v !== null) { v = JSON.stringify(v, null, 2) } - field.value = v + if (v) { + field.value = v + } field.editing = true return field @@ -187,6 +187,9 @@ let formMeta = {} const setFormMeta = value => { formMeta = value } export default function RawDataList() { + + const theme = useTheme() + const { data, mutate } = useSWR('raw_datas', fetcher); const rawDatas = data ? data.data : null; const columns = [ @@ -196,12 +199,12 @@ export default function RawDataList() { // form meta convert functions const rewriteFields = (draft, data) => { // image - ['Master', 'Worker'].forEach(replicaType => { + REPLICA_TYPES.forEach(replicaType => { fillJSON(draft.context, IMAGE_PATH.replace('[replicaType]', replicaType), data['image']) }) // output_partition_num - // data['output_partition_num'] && - // fillJSON(draft.context, WORKER_REPLICAS_PATH, data['output_partition_num']) + data['output_partition_num'] && + fillJSON(draft.context, WORKER_REPLICAS_PATH, parseInt(data['output_partition_num'])) } const mapFormMeta2Json = () => { let data = {} @@ -274,7 +277,7 @@ export default function RawDataList() { return { newFields } } - const DEFAULT_FIELDS = [ + const DEFAULT_FIELDS = useMemo(() => [ { key: 'name', required: true }, { key: 'federation_id', type: 'federation', label: 'federation', required: true }, { key: 'output_partition_num', required: true, default: 4 }, @@ -303,9 +306,23 @@ export default function RawDataList() { ] } }, - ]; + ], []); const [fields, setFields] = useState(DEFAULT_FIELDS) + const handleClone = data => { + data.context = JSON.parse(data.context) + + setFormMeta(data) + + setFields(fields => mapValueToFields({ + data: mapFormMeta2Form(fields), + fields, + type: 'form', + })) + + toggleForm() + } + // eslint-disable-next-line arrow-body-style const operation = (actions, rowData) => { // const onConfirm = () => revokeRawData(rowData.rowValue.id); @@ -314,10 +331,18 @@ export default function RawDataList() { // }; return ( <> + handleClone(rowData.rowValue)} + type="success" + style={{marginRight: `${theme.layout.gapHalf}`}} + > + Clone + - View Detail + Detail {/* { }} onOk={() => { }}> Revoke @@ -338,6 +363,17 @@ export default function RawDataList() { : []; const [formVisible, setFormVisible] = useState(false); + + const onCreate = () => { + setFormMeta({context: RAW_DATA_CONTEXT}) + setFields(mapValueToFields({ + data: mapFormMeta2Form(), + fields + })) + + toggleForm() + } + const toggleForm = () => setFormVisible(!formVisible); const onOk = (rawData) => { mutate({ @@ -369,7 +405,7 @@ export default function RawDataList() { <>
RawDatas - +
{rawDatas && ( diff --git a/web_console/pages/training/job/[id].jsx b/web_console/pages/training/job/[id].jsx new file mode 100644 index 000000000..c6c67993b --- /dev/null +++ b/web_console/pages/training/job/[id].jsx @@ -0,0 +1,9 @@ +import React from 'react'; +import { useRouter } from 'next/router'; + +import Job from '../../../components/Job'; + +export default function Jobs() { + const router = useRouter(); + return +} diff --git a/web_console/pages/training/job/index.jsx b/web_console/pages/training/job/index.jsx new file mode 100644 index 000000000..48498132f --- /dev/null +++ b/web_console/pages/training/job/index.jsx @@ -0,0 +1,5 @@ +import CommonJobList from '../../../components/CommonJobList' + +export default function JobList_() { + return +} \ No newline at end of file diff --git a/web_console/pages/training/tickets/index.jsx b/web_console/pages/training/tickets/index.jsx new file mode 100644 index 000000000..4ee8b4dd8 --- /dev/null +++ b/web_console/pages/training/tickets/index.jsx @@ -0,0 +1,5 @@ +import CommonTicket from '../../../components/CommonTicket' + +export default function Ticket() { + return +} \ No newline at end of file diff --git a/web_console/rpc/client.js b/web_console/rpc/client.js index 592e8661e..a637cfcaa 100644 --- a/web_console/rpc/client.js +++ b/web_console/rpc/client.js @@ -62,6 +62,14 @@ class FederationClient { deleteJob(params) { return this._request('deleteJob', params); } + + updateJob(params) { + return this._request('updateJob', params); + } + + heartBeat() { + return this._request('heartBeat', {}); + } } module.exports = FederationClient; diff --git a/web_console/rpc/meta.proto b/web_console/rpc/meta.proto index a906816fa..2e4e81966 100644 --- a/web_console/rpc/meta.proto +++ b/web_console/rpc/meta.proto @@ -53,6 +53,27 @@ message DeleteJobResponse { string message = 1; } +message UpdateJobRequest { + string name = 1; + string job_type = 2; + string client_ticket_name = 3; + string server_ticket_name = 4; + string server_params = 5; + string status = 6; +} + +message UpdateJobResponse { + Job data = 1; + string status = 2; +} + +message HeartBeatRequest { +} + +message HeartBeatResponse { + string status = 1; +} + service Federation { // Obtain available tickets rpc GetTickets(GetTicketsRequest) returns (GetTicketsResponse) {} @@ -62,4 +83,10 @@ service Federation { // Obtain token from preset cipher rpc DeleteJob(DeleteJobRequest) returns (DeleteJobResponse) {} + + // Obtain token from preset cipher + rpc UpdateJob(UpdateJobRequest) returns (UpdateJobResponse) {} + + // Obtain token from preset cipher + rpc HeartBeat(HeartBeatRequest) returns (HeartBeatResponse) {} } diff --git a/web_console/rpc/server.js b/web_console/rpc/server.js index 2e2b38107..b7321fd06 100644 --- a/web_console/rpc/server.js +++ b/web_console/rpc/server.js @@ -88,7 +88,7 @@ async function createJob(call, callback) { job_type, client_ticket_name: server_ticket_name, server_ticket_name: client_ticket_name, - server_params, + server_params: client_params, } = call.request; const ticketRecord = await Ticket.findOne({ where: { @@ -96,7 +96,9 @@ async function createJob(call, callback) { }, }); if (!ticketRecord) throw new Error('Ticket not found'); - const params = JSON.parse(server_params); + if (ticketRecord.federation_id != federation.id) throw new Error("Invalid ticket name"); + + const params = JSON.parse(client_params); validateTicket(ticketRecord, params); const [data, created] = await Job.findOrCreate({ @@ -109,7 +111,9 @@ async function createJob(call, callback) { job_type, client_ticket_name, server_ticket_name, - server_params: JSON.parse(server_params), + client_params: JSON.parse(client_params), + status: 'started', + federation_id: federation.id, }, }); if (!created) throw new Error('Job already exists'); @@ -123,7 +127,9 @@ async function createJob(call, callback) { job_type: data.job_type, client_ticket_name: data.server_ticket_name, server_ticket_name: data.client_ticket_name, - server_params: JSON.stringify(data.server_params), + server_params: client_params, + status: 'started', + federation_id: federation.id, }, }); } catch (err) { @@ -143,7 +149,9 @@ async function deleteJob(call, callback) { }, }); if (!job) throw new Error('Job not found'); - await k8s.deleteFLApp(NAMESPACE, job.name); + if (!job.status || job.status == 'started') { + await k8s.deleteFLApp(NAMESPACE, job.name); + } await job.destroy({ force: true }); callback(null, { message: 'Delete job successfully' }); } catch (err) { @@ -151,12 +159,111 @@ async function deleteJob(call, callback) { } } +async function updateJob(call, callback) { + try { + const federation = await authenticate(call.metadata); + + const { + name, + job_type, + client_ticket_name: server_ticket_name, + server_ticket_name: client_ticket_name, + server_params: client_params, + status, + } = call.request; + + const new_job = { + name, job_type, client_ticket_name, server_ticket_name, + client_params: JSON.parse(client_params), status, + } + + const old_job = await Job.findOne({ + where: { + name: { [Op.eq]: name }, + }, + }); + if (!old_job) throw new Error(`Job ${name} not found`); + + if (old_job.status === 'error') { + throw new Error("Cannot update errored job"); + } + if (old_job.status === 'started' && new_job.status != 'stopped') { + throw new Error("Cannot change running job"); + } + if (job_type != old_job.job_type) { + throw new Error("Cannot change job type"); + } + + const ticketRecord = await Ticket.findOne({ + where: { + name: { [Op.eq]: client_ticket_name }, + }, + }); + if (!ticketRecord) throw new Error('Ticket not found'); + if (ticketRecord.federation_id != federation.id) throw new Error("Invalid ticket name"); + + if (ticketRecord.federation_id != old_job.federation_id) { + throw new Error("Cannot change job federation"); + } + + // const params = JSON.parse(client_params); + // validateTicket(ticketRecord, params); + + if (old_job.status === 'started' && status === 'stopped') { + flapp = (await k8s.getFLApp(NAMESPACE, name)).flapp; + pods = (await k8s.getFLAppPods(NAMESPACE, name)).pods; + old_job.k8s_meta_snapshot = JSON.stringify({flapp, pods}); + await k8s.deleteFLApp(NAMESPACE, name); + } else if (old_job.status === 'stopped' && new_job.status === 'started') { + const args = serverGenerateYaml(federation, new_job, ticketRecord); + await k8s.createFLApp(NAMESPACE, args); + } + + old_job.client_ticket_name = new_job.client_ticket_name; + old_job.server_ticket_name = new_job.server_ticket_name; + if (new_job.client_params) { + old_job.client_params = new_job.client_params; + } + old_job.status = new_job.status; + old_job.federation_id = new_job.federation_id; + + const data = await old_job.save(); + + callback(null, { + data: { + name: data.name, + job_type: data.job_type, + client_ticket_name: data.server_ticket_name, + server_ticket_name: data.client_ticket_name, + server_params: client_params, + }, + status: data.status, + }); + } catch (err) { + callback(err); + } +} + +/** + * get available tickets of current federation + */ +async function heartBeat(call, callback) { + try { + const federation = await authenticate(call.metadata); + callback(null, { status: 'success' }); + } catch (err) { + callback(err); + } +} + const server = new grpc.Server(); server.addService(pkg.federation.Federation.service, { getTickets, createJob, deleteJob, + updateJob, + heartBeat, }); module.exports = server; diff --git a/web_console/services/index.js b/web_console/services/index.js index 37d163025..809892442 100644 --- a/web_console/services/index.js +++ b/web_console/services/index.js @@ -16,6 +16,10 @@ export async function updateFederation(id, json) { return client.put(`federations/${id}`, { json }).json(); } +export async function federationHeartbeat(id) { + return client.get(`federations/${id}/heartbeat`).json(); +} + export async function createUser(json) { return client.post('users', { json }).json(); } diff --git a/web_console/services/job.js b/web_console/services/job.js index 6b81675d3..913c22c90 100644 --- a/web_console/services/job.js +++ b/web_console/services/job.js @@ -7,3 +7,7 @@ export async function deleteJob(name) { export async function createJob(json) { return client.post('job', { json }).json(); } + +export async function updateJobStatus(id, json) { + return client.post(`job/${id}/update`, { json }).json(); +} \ No newline at end of file diff --git a/web_console/startup.sh b/web_console/startup.sh new file mode 100755 index 000000000..70f2279b6 --- /dev/null +++ b/web_console/startup.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -ex + +npx sequelize-cli db:migrate \ + --url="${DB_DIALECT:-mysql}://${DB_USERNAME:-fedlearner}:${DB_PASSWORD:-fedlearner}@${DB_HOST:-127.0.0.1}:${DB_PORT:-3306}/${DB_DATABASE:-fedlearner}" + +node bootstrap.js & + +while true; do sleep 1000; done diff --git a/web_console/tests/utils/form_utils.test.js b/web_console/tests/utils/form_utils.test.js index f4db54f05..7357a89fb 100644 --- a/web_console/tests/utils/form_utils.test.js +++ b/web_console/tests/utils/form_utils.test.js @@ -46,4 +46,55 @@ describe('form_utils', () => { } }) }) + + it('test push new object and insert to the first item', () => { + let paths = [ + 'foo.bar[].baz.qux', + 'foo.bar[-1].baz.quux', + ] + let value = 1 + let obj = {} + paths.forEach(path => fillJSON(obj, path, value)) + assert.deepStrictEqual(obj, { + foo: { + bar: [ + { baz: { qux: value} }, + { baz: { quux: value} }, + ] + } + }) + }) + + it('test multi push', () => { + let paths = [ + 'foo.bar[].baz[].qux', + 'foo.bar[].baz[-1].quux', + ] + let value = 1 + let obj = {} + paths.forEach(path => fillJSON(obj, path, value)) + assert.deepStrictEqual(obj, { + foo: { + bar: [ + { baz: [{ qux: value }, { quux: value } ] }, + ] + } + }) + }) +}) + +describe('filter value', () => { + it('should filter undefined', () => { + let arr = [1, 2, undefined, 3, undefined] + filterArrayValue(arr) + + assert.deepStrictEqual(arr, [1, 2, 3]) + }) + + it('should filter value', () => { + let arr = [1, 2, undefined, 3, undefined] + filterArrayValue(arr, 3) + + assert.deepStrictEqual(arr, [1, 2, undefined, undefined]) + }) }) \ No newline at end of file diff --git a/web_console/utils/form_utils.js b/web_console/utils/form_utils.js index d3fb66552..1be3a0219 100644 --- a/web_console/utils/form_utils.js +++ b/web_console/utils/form_utils.js @@ -2,38 +2,49 @@ * fill a json object with given path * path rules: * - `path.of.some.key` will make obj.path.of.some.key === value - * - `array[].key` will make obj.array[0].key === value + * - `array[].key` will always make obj.array[0].key === value + * - `array[-1].key` will push a new object to array + * TODO: `array[],[]key` */ -export const fillJSON = (container, path, value) => { - let containerIsArray = Array.isArray(container) - if (typeof path === 'string') { - path = path.split('.') - } - if (path.length === 1) { - if (containerIsArray) { - container[0] - ? container[0][path[0]]= value - : (container[0] = { [path[0]]: value }) - } else { - container[path[0]] = value +export function fillJSON(container, path, value) { + let paths = path.split('.') + let currLayer = container + + let cursor = 0 + while (cursor < paths.length) { + let arrayMarks = new RegExp(/\[([\s\S]*?)\]$/).exec(paths[cursor]) + let isArray = !!arrayMarks + let currKey = isArray ? paths[cursor].replace(arrayMarks[0], '') : paths[cursor] + + // insert value + if (cursor === paths.length - 1) { + currLayer[paths[cursor]] = value + break } - return - } - let currLayer = container - let currLayerIsArray = path[0].endsWith('[]') - let currPath = currLayerIsArray ? path[0].replace('[]', '') : path[0] + // handle layer + if (isArray) { + let posToInsert = parseInt(arrayMarks[1] || '0') - if (containerIsArray) { - !container[0] && (container[0] = {}) - currLayer = container[0] - } + if (!currLayer[currKey]) { currLayer[currKey] = [] } - if (currLayer[currPath] === undefined) { - currLayer[currPath] = currLayer[currPath] || (currLayerIsArray ? [] : {}) - } + switch (posToInsert) { + case 0: + currLayer[currKey][0] = currLayer[currKey][0] || {} + currLayer = currLayer[currKey][0] + break + case -1: + let newObj = {} + currLayer[currKey] = currLayer[currKey].concat(newObj) + currLayer = newObj + } + } else { + if (!currLayer[currKey]) { currLayer[currKey] = {} } - fillJSON(currLayer[currPath], path.slice(1), value) + currLayer = currLayer[currKey] + } + cursor++ + } } /** @@ -43,6 +54,7 @@ export const fillJSON = (container, path, value) => { * - `array[].key` will return obj.array[0].key */ export const getValueFromJson = (data, path) => { + if (!data) return if (typeof path === 'string') { path = path.split('.') } @@ -57,9 +69,43 @@ export const getValueFromJson = (data, path) => { } export function getParsedValueFromData (data, field) { - let value = (data && data[field.key]) || (field.emptyDefault || '') + let value = (data && data[field.key]) + if (['json', 'name-value'].some(el => el === field.type)) { - value = JSON.parse(value || '{}') + value = value ? JSON.parse(value) : field.emptyDefault || {} + } + else if (field.type === 'bool-select') { + value = typeof value === 'boolean' ? value : true } + else { + value = value || data[field.key] || '' + } + return value +} + +/** + * filter a value from an array. + * example: [1, 2, undefined] -> filterArrayValue(arr) -> [1, 2] + */ +export function filterArrayValue (arr, value = undefined) { + for (let i = arr.length - 1; i >= 0; i--) { + if (arr[i] === value) { + arr.splice(i, 1) + } + } + return arr +} + +/** + * get value of an item of env array + */ +export function getValueFromEnv(data, envPath, name) { + let envs = getValueFromJson(data, envPath) + if (!envs) { envs = [] } + let envNames = envs.map(env => env.name) + let v = envs[envNames.indexOf(name)] + v = v && v.value || '' + + return v } \ No newline at end of file diff --git a/web_console/utils/job.js b/web_console/utils/job.js index 6e2fc3b56..0ce7d384d 100644 --- a/web_console/utils/job.js +++ b/web_console/utils/job.js @@ -5,6 +5,14 @@ export const FLAppStatus = { ShutDown: 'FLStateShutDown', }; +export const JobStatus = { + Running: 'Running', + Failed: 'Failed', + Complete: 'Complete', + ShutDown: 'ShutDown', + Killed: 'Killed', +} + export function handleStatus(statusStr) { if (typeof statusStr !== 'string') return statusStr; return statusStr.replace('FLState', ''); @@ -13,14 +21,25 @@ export function handleStatus(statusStr) { export function getStatusColor(statusStr) { switch (statusStr) { case FLAppStatus.Running: + case JobStatus.Running: return 'lightblue'; case FLAppStatus.Failed: + case JobStatus.Failed: return 'red'; case FLAppStatus.Complete: + case JobStatus.Complete: return 'limegreen'; case FLAppStatus.ShutDown: + case JobStatus.ShutDown: return 'brown'; default: return undefined; } } + +export function getJobStatus(job) { + if (job.localdata?.status === 'stopped') { + return JobStatus.Killed + } + return handleStatus(job?.status?.appState) +} \ No newline at end of file diff --git a/web_console/utils/job_builder.js b/web_console/utils/job_builder.js index ab64a5347..ecd9c1fbd 100644 --- a/web_console/utils/job_builder.js +++ b/web_console/utils/job_builder.js @@ -12,10 +12,17 @@ const assert = require('assert'); const lodash = require('lodash'); const getConfig = require('./get_confg'); -const { NAMESPACE, ES_HOST, ES_PORT } = getConfig({ +const { NAMESPACE, ES_HOST, ES_PORT, DB_HOST, DB_PORT, + DB_DATABASE, DB_USERNAME, DB_PASSWORD, KVSTORE_TYPE } = getConfig({ NAMESPACE: process.env.NAMESPACE, ES_HOST: process.env.ES_HOST, ES_PORT: process.env.ES_PORT, + DB_HOST: process.env.DB_HOST, + DB_PORT: process.env.DB_PORT, + DB_DATABASE: process.env.DB_DATABASE, + DB_USERNAME: process.env.DB_USERNAME, + DB_PASSWORD: process.env.DB_PASSWORD, + KVSTORE_TYPE: process.env.KVSTORE_TYPE, }); function joinPath(base, ...rest) { @@ -68,13 +75,30 @@ function validateTicket(ticket, params) { } function clientValidateJob(job, client_ticket, server_ticket) { + if (job.job_type != client_ticket.job_type) { + throw new Error(`client_ticket.job_type ${client_ticket.job_type} does not match job.job_type ${job.job_type}`); + } + if (job.job_type != server_ticket.job_type) { + throw new Error(`server_ticket.job_type ${server_ticket.job_type} does not match job.job_type ${job.job_type}`); + } + if (client_ticket.role === server_ticket.role) { + throw new Error(`client_ticket.role ${client_ticket.role} must be different from server_ticket.role ${server_ticket.role}`); + } + + if (job.server_params) { + let client_replicas = job.client_params.spec.flReplicaSpecs["Worker"]["replicas"]; + let server_replicas = job.server_params.spec.flReplicaSpecs["Worker"]["replicas"]; + if (client_replicas != server_replicas) { + throw new Error(`replicas in client_params ${client_replicas} is different from replicas in server_params ${server_replicas}`); + } + } return true; } -// Only allow some fields to be used from job.server_params because +// Only allow some fields to be used from job params because // it is received from peers and cannot be totally trusted. function extractPermittedJobParams(job) { - const params = job.server_params; + const params = job.client_params; const permitted_envs = permittedJobEnvs[job.job_type]; const extracted = {}; @@ -168,6 +192,7 @@ function generateYaml(federation, job, job_params, ticket) { restartPolicy: 'Never', containers: [{ env: [ + { name: 'STORAGE_ROOT_PATH', value: k8s_settings.storage_root_path }, { name: 'POD_IP', valueFrom: { fieldRef: { fieldPath: 'status.podIP' } } }, { name: 'POD_NAME', valueFrom: { fieldRef: { fieldPath: 'metadata.name' } } }, { name: 'ROLE', value: ticket.role.toLowerCase() }, @@ -179,6 +204,12 @@ function generateYaml(federation, job, job_params, ticket) { { name: 'MEM_LIMIT', valueFrom: { resourceFieldRef: { resource: 'limits.memory' } } }, { name: 'ES_HOST', value: ES_HOST }, { name: 'ES_PORT', value: `${ES_PORT}` }, + { name: 'DB_HOST', value: `${DB_HOST}` }, + { name: 'DB_PORT', value: `${DB_PORT}` }, + { name: 'DB_DATABASE', value: `${DB_DATABASE}` }, + { name: 'DB_USERNAME', value: `${DB_USERNAME}` }, + { name: 'DB_PASSWORD', value: `${DB_PASSWORD}` }, + { name: 'KVSTORE_TYPE', value: `${KVSTORE_TYPE}` }, ], imagePullPolicy: 'IfNotPresent', name: 'tensorflow', @@ -252,10 +283,17 @@ function portalGenerateYaml(federation, raw_data) { restartPolicy: 'Never', containers: [{ env: [ + { name: 'STORAGE_ROOT_PATH', value: k8s_settings.storage_root_path }, { name: 'POD_IP', valueFrom: { fieldRef: { fieldPath: 'status.podIP' } } }, { name: 'POD_NAME', valueFrom: { fieldRef: { fieldPath: 'metadata.name' } } }, { name: 'ES_HOST', value: ES_HOST }, { name: 'ES_PORT', value: `${ES_PORT}` }, + { name: 'DB_HOST', value: `${DB_HOST}` }, + { name: 'DB_PORT', value: `${DB_PORT}` }, + { name: 'DB_DATABASE', value: `${DB_DATABASE}` }, + { name: 'DB_USERNAME', value: `${DB_USERNAME}` }, + { name: 'DB_PASSWORD', value: `${DB_PASSWORD}` }, + { name: 'KVSTORE_TYPE', value: `${KVSTORE_TYPE}` }, { name: 'APPLICATION_ID', value: raw_data.name }, { name: 'DATA_PORTAL_NAME', value: raw_data.name }, { name: 'OUTPUT_PARTITION_NUM', value: `${raw_data.output_partition_num}` }, @@ -281,6 +319,7 @@ function portalGenerateYaml(federation, raw_data) { restartPolicy: 'Never', containers: [{ env: [ + { name: 'STORAGE_ROOT_PATH', value: k8s_settings.storage_root_path }, { name: 'POD_IP', valueFrom: { fieldRef: { fieldPath: 'status.podIP' } } }, { name: 'POD_NAME', valueFrom: { fieldRef: { fieldPath: 'metadata.name' } } }, { name: 'CPU_REQUEST', valueFrom: { resourceFieldRef: { resource: 'requests.cpu' } } }, @@ -289,6 +328,12 @@ function portalGenerateYaml(federation, raw_data) { { name: 'MEM_LIMIT', valueFrom: { resourceFieldRef: { resource: 'limits.memory' } } }, { name: 'ES_HOST', value: ES_HOST }, { name: 'ES_PORT', value: `${ES_PORT}` }, + { name: 'DB_HOST', value: `${DB_HOST}` }, + { name: 'DB_PORT', value: `${DB_PORT}` }, + { name: 'DB_DATABASE', value: `${DB_DATABASE}` }, + { name: 'DB_USERNAME', value: `${DB_USERNAME}` }, + { name: 'DB_PASSWORD', value: `${DB_PASSWORD}` }, + { name: 'KVSTORE_TYPE', value: `${KVSTORE_TYPE}` }, { name: 'APPLICATION_ID', value: raw_data.name }, { name: 'BATCH_SIZE', value: raw_data.context.batch_size ? `${raw_data.context.batch_size}` : "" }, { name: 'INPUT_DATA_FORMAT', value: raw_data.context.input_data_format }, diff --git a/web_console/utils/kibana.js b/web_console/utils/kibana.js index d76e4f7ea..f3b0aa249 100644 --- a/web_console/utils/kibana.js +++ b/web_console/utils/kibana.js @@ -2,8 +2,160 @@ const dayjs = require('dayjs'); const getConfig = require('./get_confg'); const JOB_METRICS = { - data_join: [], - psi_data_join: [], + data_join: [ + { + query: "name%20:%22data_block_dump_duration%22", + mode: "avg", + title: "data_block_dump_duration" + }, + { + query: "name%20:%22data_block_index%22", + mode: "avg", + title: "data_block_index" + }, + { + query: "name%20:%22stats_cum_join_num%22", + mode: "avg", + title: "stats_cum_join_num" + }, + { + query: "name%20:%22actual_cum_join_num%22", + mode: "avg", + title: "actual_cum_join_num" + }, + { + query: "name%20:%22leader_stats_index%22", + mode: "avg", + title: "leader_stats_index" + }, + { + query: "name%20:%22follower_stats_index%22", + mode: "avg", + title: "follower_stats_index" + }, + { + query: "name%20:%22example_id_dump_duration%22", + mode: "avg", + title: "example_id_dump_duration" + }, + { + query: "name%20:%22example_dump_file_index%22", + mode: "avg", + title: "example_dump_file_index" + }, + { + query: "name%20:%22example_id_dumped_index%22", + mode: "avg", + title: "example_id_dumped_index" + }, + { + query: "name%20:%22stats_cum_join_num%22", + mode: "avg", + title: "stats_cum_join_num" + }, + { + query: "name%20:%22actual_cum_join_num%22", + mode: "avg", + title: "actual_cum_join_num" + }, + { + query: "name%20:%22leader_stats_index%22", + mode: "avg", + title: "leader_stats_index" + }, + { + query: "name%20:%22follower_stats_index%22", + mode: "avg", + title: "follower_stats_index" + }, + { + query: "name%20:%22leader_join_rate_percent%22", + mode: "avg", + title: "leader_join_rate_percent" + }, + { + query: "name%20:%22follower_join_rate_percent%22", + mode: "avg", + title: "follower_join_rate_percent" + } + ], + psi_data_join: [ + { + query: "name%20:%22data_block_dump_duration%22", + mode: "avg", + title: "data_block_dump_duration" + }, + { + query: "name%20:%22data_block_index%22", + mode: "avg", + title: "data_block_index" + }, + { + query: "name%20:%22stats_cum_join_num%22", + mode: "avg", + title: "stats_cum_join_num" + }, + { + query: "name%20:%22actual_cum_join_num%22", + mode: "avg", + title: "actual_cum_join_num" + }, + { + query: "name%20:%22leader_stats_index%22", + mode: "avg", + title: "leader_stats_index" + }, + { + query: "name%20:%22follower_stats_index%22", + mode: "avg", + title: "follower_stats_index" + }, + { + query: "name%20:%22example_id_dump_duration%22", + mode: "avg", + title: "example_id_dump_duration" + }, + { + query: "name%20:%22example_dump_file_index%22", + mode: "avg", + title: "example_dump_file_index" + }, + { + query: "name%20:%22example_id_dumped_index%22", + mode: "avg", + title: "example_id_dumped_index" + }, + { + query: "name%20:%22stats_cum_join_num%22", + mode: "avg", + title: "stats_cum_join_num" + }, + { + query: "name%20:%22actual_cum_join_num%22", + mode: "avg", + title: "actual_cum_join_num" + }, + { + query: "name%20:%22leader_stats_index%22", + mode: "avg", + title: "leader_stats_index" + }, + { + query: "name%20:%22follower_stats_index%22", + mode: "avg", + title: "follower_stats_index" + }, + { + query: "name%20:%22leader_join_rate_percent%22", + mode: "avg", + title: "leader_join_rate_percent" + }, + { + query: "name%20:%22follower_join_rate_percent%22", + mode: "avg", + title: "follower_join_rate_percent" + } + ], tree_model: [], nn_model: [ {