├── tests ├── __init__.py ├── unit │ ├── __init__.py │ ├── flows │ │ ├── normalized_flows │ │ │ ├── flow5.yml │ │ │ ├── flow3.yml │ │ │ ├── flow4.yml │ │ │ ├── flow1.yml │ │ │ ├── flow8.yml │ │ │ ├── flow7.yml │ │ │ ├── flow9.yml │ │ │ ├── flow.yml │ │ │ ├── flow2.yml │ │ │ └── flow6.yml │ │ ├── failed_flows │ │ │ └── failed_flow.yml │ │ ├── not │ │ │ ├── flow4.yml │ │ │ ├── flow1.yml │ │ │ ├── flow3.yml │ │ │ └── flow2.yml │ │ ├── local_flow │ │ │ ├── executor1 │ │ │ │ ├── config.yml │ │ │ │ └── executor.py │ │ │ ├── executor2 │ │ │ │ ├── config.yml │ │ │ │ └── executor.py │ │ │ └── flow.yml │ │ ├── mixed_flow │ │ │ ├── executor1 │ │ │ │ ├── config.yml │ │ │ │ └── executor.py │ │ │ └── flow.yml │ │ ├── flow2.yml │ │ ├── flow1.yml │ │ ├── flow-with-obj-label.yml │ │ ├── flow-with-labels.yml │ │ └── flow1-test.yml │ ├── test_flow.py │ └── test_normalize.py ├── utils │ ├── __init__.py │ └── utils.py └── integration │ ├── __init__.py │ ├── flow │ ├── basic │ │ ├── __init__.py │ │ ├── flows │ │ │ ├── grpc-flow.yml │ │ │ ├── http-flow.yml │ │ │ ├── websocket-flow.yml │ │ │ └── http-flow-new-syntax.yml │ │ ├── test_basic_grpc.py │ │ ├── test_basic_http.py │ │ ├── test_basic_websocket.py │ │ └── test_basic_http_new_syntax.py │ ├── envvars │ │ ├── __init__.py │ │ ├── flows │ │ │ └── envs-in-flow.yml │ │ ├── test_yaml_env_file.py │ │ ├── test_envvars_context_syntax.py │ │ └── test_envvars_default_file.py │ ├── expose │ │ ├── __init__.py │ │ ├── flows │ │ │ ├── single-executor-stateless.yml │ │ │ ├── single-executor-stateful.yml │ │ │ └── gateway-and-executors.yml │ │ ├── test_single_executor_stateless.py │ │ ├── test_single_executor_stateful.py │ │ └── test_multiple_executors.py │ ├── jobs │ │ ├── __init__.py │ │ └── test_jobs.py │ ├── secrets │ │ ├── __init__.py │ │ └── test_secrets.py │ ├── stateful │ │ ├── __init__.py │ │ ├── flows │ │ │ ├── grpc-stateful.yml │ │ │ ├── http-stateful.yml │ │ │ └── websocket-stateful.yml │ │ ├── test_stateful_flow_grpc.py │ │ ├── test_stateful_flow_http.py │ │ └── test_stateful_flow_websocket.py │ ├── update │ │ ├── __init__.py │ │ ├── flows │ │ │ ├── base_flow.yml │ │ │ ├── update_image.yml │ │ │ ├── scale_out.yml │ │ │ ├── custom_name_executor.yml │ │ │ ├── expose_executor.yml │ │ │ ├── modify_env.yml │ │ │ ├── modify_delete_labels.yml │ │ │ ├── custom_name_exposed_executor.yml │ │ │ ├── add_args.yml │ │ │ ├── add_labels.yml │ │ │ ├── update_resources.yml │ │ │ └── add_env.yml │ │ ├── test_update_executor_image.py │ │ ├── test_update_executor_args.py │ │ ├── test_update_executor_resources.py │ │ ├── test_update_env.py │ │ ├── test_update_labels.py │ │ ├── test_update_expose_executor.py │ │ └── test_rename_executor.py │ ├── validate │ │ ├── __init__.py │ │ ├── flows │ │ │ ├── valid-flow.yml │ │ │ └── invalid-flow.yml │ │ └── test_validate.py │ ├── autoscale │ │ ├── __init__.py │ │ ├── flows │ │ │ └── executors-autoscaled.yml │ │ └── test_executors_autoscaled.py │ ├── custom_labels │ │ ├── __init__.py │ │ ├── flows │ │ │ └── valid-labels.yml │ │ └── test_custom_labels.py │ ├── custom_name │ │ ├── __init__.py │ │ ├── flows │ │ │ ├── valid-name.yml │ │ │ └── invalid-name.yml │ │ └── test_custom_name.py │ ├── executor_uses │ │ ├── __init__.py │ │ ├── flow.yml │ │ └── test_executor_uses.py │ ├── jina_version │ │ ├── __init__.py │ │ ├── flows │ │ │ └── custom-jina-version.yml │ │ └── test_custom_jina_version.py │ ├── custom_actions │ │ ├── __init__.py │ │ ├── flows │ │ │ └── base_flow.yml │ │ ├── test_recreate.py │ │ ├── test_scale.py │ │ ├── test_pause_resume.py │ │ └── test_restart.py │ ├── custom_resources │ │ ├── __init__.py │ │ ├── flows │ │ │ ├── gateway-resources.yml │ │ │ └── executor-resources.yml │ │ ├── test_gateway_resources.py │ │ └── test_executor_resources.py │ ├── remove_by_phase │ │ └── __init__.py │ ├── remove_multiple │ │ ├── __init__.py │ │ ├── flows │ │ │ ├── flow1.yml │ │ │ └── flow2.yml │ │ └── test_multi_flows_removal.py │ ├── test_expose_version_arg.py │ ├── __init__.py │ ├── projects │ │ ├── simple │ │ │ ├── executor1 │ │ │ │ ├── requirements.txt │ │ │ │ ├── config.yml │ │ │ │ └── executor.py │ │ │ ├── flow.yml │ │ │ └── app.py │ │ ├── multi_executors │ │ │ ├── executor1 │ │ │ │ ├── requirements.txt │ │ │ │ ├── config.yml │ │ │ │ └── executor.py │ │ │ ├── executor2 │ │ │ │ ├── requirements.txt │ │ │ │ ├── config.yml │ │ │ │ └── executor.py │ │ │ └── flow.yml │ │ ├── envvars_context_syntax │ │ │ ├── executor1 │ │ │ │ ├── requirements.txt │ │ │ │ ├── config.yml │ │ │ │ └── executor.py │ │ │ ├── .env │ │ │ └── flow.yml │ │ ├── envvars_default_file │ │ │ ├── executor1 │ │ │ │ ├── requirements.txt │ │ │ │ ├── config.yml │ │ │ │ └── executor.py │ │ │ ├── .env │ │ │ └── flow.yml │ │ └── executors_with_shards │ │ │ ├── executor1 │ │ │ ├── requirements.txt │ │ │ ├── config.yml │ │ │ └── executor.py │ │ │ └── flow.yml │ ├── executor_with_no_uses │ │ ├── flow │ │ │ ├── executor1 │ │ │ │ ├── config.yml │ │ │ │ └── executor.py │ │ │ ├── executor2 │ │ │ │ ├── config.yml │ │ │ │ └── executor.py │ │ │ └── flow.yml │ │ └── test_executor_with_no_uses.py │ ├── normalized_flow_env_vars │ │ ├── flow.yml │ │ └── test_normalized_flow_env_vars.py │ ├── test_simple.py │ ├── test_multi_executors.py │ ├── test_executors_with_shards.py │ └── test_jina_new.py │ └── deployment │ ├── basic │ ├── __init__.py │ ├── deployments │ │ ├── grpc-deployment.yml │ │ ├── http-deployment.yml │ │ └── multi-protocol-deployment.yml │ ├── test_basic_grpc.py │ └── test_basic_http.py │ ├── envvars │ ├── __init__.py │ ├── deployments │ │ └── envvars.yml │ └── test_yaml_env_file.py │ ├── update │ ├── __init__.py │ ├── deployments │ │ ├── update_image.yml │ │ ├── base_deployment.yml │ │ ├── scale_out.yml │ │ ├── rename_executor.yml │ │ ├── modify_env.yml │ │ ├── update_resources.yml │ │ ├── add_secret.yml │ │ ├── modify_delete_labels.yml │ │ ├── add_args.yml │ │ ├── add_labels.yml │ │ └── add_env.yml │ ├── test_rename_executor.py │ ├── test_update_executor_args.py │ ├── test_update_executor_image.py │ ├── test_update_executor_resources.py │ ├── test_update_env.py │ └── test_update_labels.py │ ├── autoscale │ ├── __init__.py │ ├── deployments │ │ └── autoscale-http.yml │ └── test_autoscale.py │ ├── custom_name │ ├── __init__.py │ ├── deployments │ │ ├── valid-name-deployment.yml │ │ └── invalid-name-deployment.yml │ └── test_custom_name.py │ ├── jina_version │ ├── __init__.py │ ├── deployments │ │ └── deployment-3.21.0.yml │ └── test_custom_jina_version.py │ ├── custom_action │ ├── __init__.py │ ├── deployments │ │ └── base_deployment.yml │ ├── test_restart.py │ ├── test_scale.py │ ├── test_recreate.py │ └── test_pause_resume.py │ ├── custom_labels │ ├── __init__.py │ ├── deployments │ │ └── deployment-with-labels.yml │ └── test_custom_labels.py │ ├── custom_resources │ ├── __init__.py │ ├── deployments │ │ └── deployment-with-custom-resources.yml │ └── test_custom_resources.py │ └── __init__.py ├── jcloud ├── resources │ └── project-template │ │ ├── .env │ │ ├── executor1 │ │ ├── requirements.txt │ │ ├── config.yml │ │ └── executor.py │ │ └── flow.yml ├── __init__.py ├── parsers │ ├── get.py │ ├── deploy.py │ ├── status.py │ ├── base.py │ ├── normalize.py │ ├── logs.py │ ├── create.py │ ├── list.py │ ├── remove.py │ ├── update.py │ └── __init__.py ├── __main__.py └── constants.py ├── MANIFEST.in ├── pytest.ini ├── pyproject.toml ├── .github ├── pull_request_template.md ├── labeler.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug-report--flow-deployment-failed-.md ├── workflows │ ├── tag.yml │ ├── force-release.yml │ ├── force-docs-build.yml │ ├── cd.yml │ ├── label-pr.yml │ └── integration-tests.yml └── README-img │ └── logo.svg ├── scripts ├── get-last-release-note.py ├── black.sh ├── get-all-test-paths.sh ├── docstrings_lint.sh └── release.sh ├── .pre-commit-config.yaml ├── README.md ├── .gitignore └── setup.py /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /jcloud/resources/project-template/.env: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/flow/basic/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/flow/envvars/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/flow/expose/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/flow/jobs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/flow/secrets/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/flow/stateful/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/flow/update/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/flow/validate/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /jcloud/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.3.1' 2 | -------------------------------------------------------------------------------- /tests/integration/deployment/basic/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/deployment/envvars/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/deployment/update/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/flow/autoscale/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/flow/custom_labels/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/flow/custom_name/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/flow/executor_uses/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/flow/jina_version/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/deployment/autoscale/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/deployment/custom_name/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/deployment/jina_version/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/flow/custom_actions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/flow/custom_resources/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/flow/remove_by_phase/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/flow/remove_multiple/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/flow/test_expose_version_arg.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/deployment/custom_action/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/deployment/custom_labels/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/deployment/custom_resources/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/flows/normalized_flows/flow5.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow -------------------------------------------------------------------------------- /jcloud/resources/project-template/executor1/requirements.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/flow/__init__.py: -------------------------------------------------------------------------------- 1 | FlowAlive = 'FlowAlive' 2 | -------------------------------------------------------------------------------- /tests/integration/flow/projects/simple/executor1/requirements.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/flow/projects/multi_executors/executor1/requirements.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/deployment/__init__.py: -------------------------------------------------------------------------------- 1 | DeploymentAlive = 'DeploymentAlive' 2 | -------------------------------------------------------------------------------- /tests/integration/flow/projects/envvars_context_syntax/executor1/requirements.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/flow/projects/envvars_default_file/executor1/requirements.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/flow/projects/executors_with_shards/executor1/requirements.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/flow/projects/multi_executors/executor2/requirements.txt: -------------------------------------------------------------------------------- 1 | requests -------------------------------------------------------------------------------- /tests/integration/flow/projects/envvars_context_syntax/.env: -------------------------------------------------------------------------------- 1 | VALUE_A=56 2 | VALUE_B=abcd 3 | -------------------------------------------------------------------------------- /tests/integration/flow/projects/envvars_default_file/.env: -------------------------------------------------------------------------------- 1 | VALUE_A=56 2 | VALUE_B=abcd 3 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | prune tests/ 3 | prune **/tests/ 4 | recursive-include jcloud/resources * -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | asyncio_mode = auto 3 | log_cli_level = DEBUG 4 | env = 5 | JCLOUD_LOGLEVEL=DEBUG -------------------------------------------------------------------------------- /tests/unit/flows/failed_flows/failed_flow.yml: -------------------------------------------------------------------------------- 1 | jtype: Flows 2 | version: "1" 3 | with: 4 | port: 51000 5 | -------------------------------------------------------------------------------- /tests/unit/flows/not/flow4.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | executors: 3 | - name: E1 4 | uses: ${{ E1_USES }} 5 | -------------------------------------------------------------------------------- /tests/unit/flows/not/flow1.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | executors: 3 | - name: ABC 4 | uses: ABC/config.yml 5 | -------------------------------------------------------------------------------- /tests/unit/flows/not/flow3.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | executors: 3 | - name: ABC 4 | uses: something_random 5 | -------------------------------------------------------------------------------- /tests/unit/flows/normalized_flows/flow3.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | executors: 3 | - name: ABC 4 | uses: docker://ABC -------------------------------------------------------------------------------- /tests/unit/flows/normalized_flows/flow4.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | executors: 3 | - name: ABC 4 | uses: docker://ABC -------------------------------------------------------------------------------- /jcloud/resources/project-template/executor1/config.yml: -------------------------------------------------------------------------------- 1 | jtype: MyExecutor 2 | metas: 3 | py_modules: 4 | - executor.py -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=18.0", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /tests/unit/flows/local_flow/executor1/config.yml: -------------------------------------------------------------------------------- 1 | jtype: MyExecutor 2 | metas: 3 | py_modules: 4 | - executor.py 5 | -------------------------------------------------------------------------------- /tests/unit/flows/local_flow/executor2/config.yml: -------------------------------------------------------------------------------- 1 | jtype: MyExecutor 2 | metas: 3 | py_modules: 4 | - executor.py 5 | -------------------------------------------------------------------------------- /tests/unit/flows/mixed_flow/executor1/config.yml: -------------------------------------------------------------------------------- 1 | jtype: MyExecutor 2 | metas: 3 | py_modules: 4 | - executor.py 5 | -------------------------------------------------------------------------------- /tests/unit/flows/normalized_flows/flow1.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | executors: 3 | - name: ABC 4 | uses: jinahub+docker://ABC -------------------------------------------------------------------------------- /tests/unit/flows/normalized_flows/flow8.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | executors: 3 | - name: E1 4 | uses: ${{ E1_USES }} 5 | -------------------------------------------------------------------------------- /jcloud/resources/project-template/flow.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | executors: 3 | - name: executor1 4 | uses: executor1/config.yml -------------------------------------------------------------------------------- /tests/integration/flow/projects/simple/executor1/config.yml: -------------------------------------------------------------------------------- 1 | jtype: MyExecutor 2 | metas: 3 | py_modules: 4 | - executor.py -------------------------------------------------------------------------------- /tests/unit/flows/normalized_flows/flow7.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | executors: 3 | - name: E1 4 | uses: ${{ ENV.E1_USES }} 5 | -------------------------------------------------------------------------------- /tests/unit/flows/normalized_flows/flow9.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | executors: 3 | - name: E1 4 | uses: ${{ CONTEXT.E1_USES }} 5 | -------------------------------------------------------------------------------- /tests/integration/flow/projects/simple/flow.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | executors: 3 | - name: executor1 4 | uses: executor1/config.yml -------------------------------------------------------------------------------- /tests/integration/flow/executor_uses/flow.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | executors: 3 | - name: encoder 4 | uses: jinahub://Sentencizer 5 | -------------------------------------------------------------------------------- /tests/integration/flow/projects/multi_executors/executor1/config.yml: -------------------------------------------------------------------------------- 1 | jtype: MyExecutor1 2 | metas: 3 | py_modules: 4 | - executor.py -------------------------------------------------------------------------------- /tests/integration/flow/projects/multi_executors/executor2/config.yml: -------------------------------------------------------------------------------- 1 | jtype: MyExecutor2 2 | metas: 3 | py_modules: 4 | - executor.py -------------------------------------------------------------------------------- /tests/integration/deployment/update/deployments/update_image.yml: -------------------------------------------------------------------------------- 1 | jtype: Deployment 2 | with: 3 | uses: jinahub+docker://SimpleIndexer 4 | -------------------------------------------------------------------------------- /tests/integration/flow/executor_with_no_uses/flow/executor1/config.yml: -------------------------------------------------------------------------------- 1 | jtype: MyExecutor 2 | metas: 3 | py_modules: 4 | - executor.py 5 | -------------------------------------------------------------------------------- /tests/integration/flow/executor_with_no_uses/flow/executor2/config.yml: -------------------------------------------------------------------------------- 1 | jtype: MyExecutor 2 | metas: 3 | py_modules: 4 | - executor.py 5 | -------------------------------------------------------------------------------- /tests/integration/flow/projects/envvars_context_syntax/executor1/config.yml: -------------------------------------------------------------------------------- 1 | jtype: MyExecutor 2 | metas: 3 | py_modules: 4 | - executor.py -------------------------------------------------------------------------------- /tests/integration/flow/projects/envvars_default_file/executor1/config.yml: -------------------------------------------------------------------------------- 1 | jtype: MyExecutor 2 | metas: 3 | py_modules: 4 | - executor.py -------------------------------------------------------------------------------- /tests/integration/flow/projects/executors_with_shards/executor1/config.yml: -------------------------------------------------------------------------------- 1 | jtype: MyExecutor 2 | metas: 3 | py_modules: 4 | - executor.py -------------------------------------------------------------------------------- /tests/unit/flows/not/flow2.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | executors: 3 | - name: ABC 4 | uses: ABC/config.yml 5 | - name: DEF 6 | uses: DEF/config.yml -------------------------------------------------------------------------------- /tests/integration/flow/update/flows/base_flow.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | with: 3 | protocol: http 4 | executors: 5 | - uses: jinahub+docker://Sentencizer 6 | -------------------------------------------------------------------------------- /tests/integration/flow/custom_actions/flows/base_flow.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | with: 3 | protocol: http 4 | executors: 5 | - uses: jinahub+docker://Sentencizer 6 | -------------------------------------------------------------------------------- /tests/integration/flow/update/flows/update_image.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | with: 3 | protocol: http 4 | executors: 5 | - uses: jinahub+docker://SimpleIndexer 6 | -------------------------------------------------------------------------------- /tests/unit/flows/flow2.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | executors: 3 | - name: abc 4 | uses: jinahub+docker://Sentencizer 5 | - name: def 6 | - needs: [abc, def] 7 | -------------------------------------------------------------------------------- /tests/integration/flow/projects/executors_with_shards/flow.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | executors: 3 | - name: executor1 4 | uses: executor1/config.yml 5 | shards: 2 -------------------------------------------------------------------------------- /tests/integration/flow/update/flows/scale_out.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | with: 3 | protocol: http 4 | executors: 5 | - uses: jinahub+docker://Sentencizer 6 | replicas: 2 7 | -------------------------------------------------------------------------------- /tests/integration/flow/basic/flows/grpc-flow.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | with: 3 | protocol: grpc 4 | executors: 5 | - name: sentencizer 6 | uses: jinahub+docker://Sentencizer 7 | -------------------------------------------------------------------------------- /tests/integration/flow/validate/flows/valid-flow.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | with: 3 | protocol: http 4 | executors: 5 | - name: sentencizer 6 | uses: jinahub+docker://Sentencizer 7 | -------------------------------------------------------------------------------- /tests/unit/flows/flow1.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | executors: 3 | - name: abc 4 | uses: jinahub+docker://Sentencizer 5 | - name: def 6 | - name: joiner 7 | needs: [abc, def] 8 | -------------------------------------------------------------------------------- /tests/integration/deployment/update/deployments/base_deployment.yml: -------------------------------------------------------------------------------- 1 | jtype: Deployment 2 | with: 3 | uses: jinaai+docker://auth0-unified-64c3e19b1d2f398f/JCloudCISentencizer:latest 4 | -------------------------------------------------------------------------------- /tests/unit/flows/local_flow/flow.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | executors: 3 | - name: executor1 4 | uses: executor1/config.yml 5 | - name: executor2 6 | uses: executor2/config.yml 7 | -------------------------------------------------------------------------------- /tests/integration/flow/custom_name/flows/valid-name.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | jcloud: 3 | name: fashion-data 4 | executors: 5 | - name: sentencizer 6 | uses: jinahub+docker://Sentencizer 7 | -------------------------------------------------------------------------------- /tests/integration/flow/stateful/flows/grpc-stateful.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | with: 3 | protocol: grpc 4 | executors: 5 | - name: simpleindexer 6 | uses: jinahub+docker://SimpleIndexer 7 | -------------------------------------------------------------------------------- /tests/integration/flow/stateful/flows/http-stateful.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | with: 3 | protocol: http 4 | executors: 5 | - name: simpleindexer 6 | uses: jinahub+docker://SimpleIndexer 7 | -------------------------------------------------------------------------------- /tests/unit/flows/flow-with-obj-label.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | jcloud: 3 | labels: 4 | test-label-invalid-obj: 5 | foo: bar 6 | executors: 7 | - uses: jinahub+docker://Sentencizer 8 | -------------------------------------------------------------------------------- /tests/integration/deployment/update/deployments/scale_out.yml: -------------------------------------------------------------------------------- 1 | jtype: Deployment 2 | with: 3 | uses: jinaai+docker://auth0-unified-64c3e19b1d2f398f/JCloudCISentencizer:latest 4 | replicas: 2 5 | -------------------------------------------------------------------------------- /tests/integration/flow/update/flows/custom_name_executor.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | with: 3 | protocol: http 4 | executors: 5 | - name: newsentencizer 6 | uses: jinahub+docker://Sentencizer 7 | -------------------------------------------------------------------------------- /tests/unit/flows/mixed_flow/flow.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | executors: 3 | - name: executor1 4 | uses: executor1/config.yml 5 | - name: sentencizer 6 | uses: jinahub+docker://Sentencizer 7 | -------------------------------------------------------------------------------- /tests/integration/flow/stateful/flows/websocket-stateful.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | with: 3 | protocol: websocket 4 | executors: 5 | - name: simpleindexer 6 | uses: jinahub+docker://SimpleIndexer 7 | -------------------------------------------------------------------------------- /tests/integration/flow/update/flows/expose_executor.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | with: 3 | protocol: http 4 | executors: 5 | - uses: jinahub+docker://Sentencizer 6 | jcloud: 7 | expose: true 8 | -------------------------------------------------------------------------------- /tests/integration/flow/update/flows/modify_env.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | with: 3 | protocol: http 4 | executors: 5 | - uses: jinahub+docker://Sentencizer 6 | env: 7 | JINA_LOG_LEVEL : INFO 8 | -------------------------------------------------------------------------------- /tests/unit/flows/normalized_flows/flow.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | executors: 3 | - name: executor1 4 | uses: jinahub+docker://Executor1 5 | - name: executor2 6 | uses: jinahub+docker://Executor2 7 | -------------------------------------------------------------------------------- /tests/integration/deployment/basic/deployments/grpc-deployment.yml: -------------------------------------------------------------------------------- 1 | jtype: Deployment 2 | with: 3 | protocol: grpc 4 | uses: jinaai+docker://auth0-unified-64c3e19b1d2f398f/JCloudCISentencizer:latest 5 | -------------------------------------------------------------------------------- /tests/integration/deployment/basic/deployments/http-deployment.yml: -------------------------------------------------------------------------------- 1 | jtype: Deployment 2 | with: 3 | protocol: http 4 | uses: jinaai+docker://auth0-unified-64c3e19b1d2f398f/JCloudCISentencizer:latest 5 | -------------------------------------------------------------------------------- /tests/integration/deployment/jina_version/deployments/deployment-3.21.0.yml: -------------------------------------------------------------------------------- 1 | jtype: Deployment 2 | with: 3 | uses: jinahub+docker://Sentencizer 4 | jcloud: 5 | version: 3.21.0 6 | docarray: 0.20.0 7 | -------------------------------------------------------------------------------- /tests/integration/flow/custom_name/flows/invalid-name.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | jcloud: 3 | name: abc_def#1/ # invalid name 4 | executors: 5 | - name: sentencizer 6 | uses: jinahub+docker://Sentencizer 7 | -------------------------------------------------------------------------------- /tests/integration/flow/jina_version/flows/custom-jina-version.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | with: 3 | protocol: http 4 | jcloud: 5 | version: 3.9.3 6 | executors: 7 | - uses: jinahub+docker://Sentencizer 8 | -------------------------------------------------------------------------------- /tests/integration/deployment/custom_action/deployments/base_deployment.yml: -------------------------------------------------------------------------------- 1 | jtype: Deployment 2 | with: 3 | protocol: grpc 4 | uses: jinaai+docker://auth0-unified-64c3e19b1d2f398f/JCloudCISentencizer:latest 5 | -------------------------------------------------------------------------------- /tests/integration/deployment/update/deployments/rename_executor.yml: -------------------------------------------------------------------------------- 1 | jtype: Deployment 2 | with: 3 | name: newsentencizer 4 | uses: jinaai+docker://auth0-unified-64c3e19b1d2f398f/JCloudCISentencizer:latest 5 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | **Goal** 2 | 3 | - [ ] Run [Integration tests GHA](https://github.com/jina-ai/jcloud/actions/workflows/integration-tests.yml) manually & comment the link. 4 | 5 | @jina-ai/team-wolf 6 | -------------------------------------------------------------------------------- /tests/integration/deployment/update/deployments/modify_env.yml: -------------------------------------------------------------------------------- 1 | jtype: Deployment 2 | with: 3 | uses: jinaai+docker://auth0-unified-64c3e19b1d2f398f/JCloudCISentencizer:latest 4 | env: 5 | JINA_LOG_LEVEL: INFO 6 | -------------------------------------------------------------------------------- /tests/integration/flow/normalized_flow_env_vars/flow.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | executors: 3 | - uses: jinaai+docker://auth0-unified-64c3e19b1d2f398f/JCloudCISentencizer:latest 4 | env: 5 | TEST: ${{ ENV.TEST }} 6 | -------------------------------------------------------------------------------- /tests/integration/flow/basic/flows/http-flow.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | with: 3 | protocol: http 4 | executors: 5 | - name: sentencizer 6 | uses: jinahub+docker://Sentencizer 7 | env: 8 | JINA_LOG_LEVEL : DEBUG 9 | -------------------------------------------------------------------------------- /tests/integration/flow/custom_labels/flows/valid-labels.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | jcloud: 3 | labels: 4 | label1: value1 5 | label2: value2 6 | executors: 7 | - name: sentencizer 8 | uses: jinahub+docker://Sentencizer 9 | -------------------------------------------------------------------------------- /tests/integration/flow/projects/envvars_context_syntax/flow.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | executors: 3 | - name: executor1 4 | uses: executor1/config.yml 5 | uses_with: 6 | var_a: ${{ VALUE_A }} 7 | var_b: ${{ VALUE_B }} -------------------------------------------------------------------------------- /tests/integration/deployment/update/deployments/update_resources.yml: -------------------------------------------------------------------------------- 1 | jtype: Deployment 2 | with: 3 | uses: jinaai+docker://auth0-unified-64c3e19b1d2f398f/JCloudCISentencizer:latest 4 | jcloud: 5 | resources: 6 | instance: C2 7 | -------------------------------------------------------------------------------- /tests/integration/flow/basic/flows/websocket-flow.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | with: 3 | protocol: websocket 4 | executors: 5 | - name: sentencizer 6 | uses: jinahub+docker://Sentencizer 7 | env: 8 | JINA_LOG_LEVEL : DEBUG 9 | -------------------------------------------------------------------------------- /tests/integration/flow/projects/envvars_default_file/flow.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | executors: 3 | - name: executor1 4 | uses: executor1/config.yml 5 | uses_with: 6 | var_a: ${{ ENV.VALUE_A }} 7 | var_b: ${{ ENV.VALUE_B }} -------------------------------------------------------------------------------- /tests/integration/flow/update/flows/modify_delete_labels.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | with: 3 | protocol: http 4 | jcloud: 5 | labels: 6 | "jina.ai/application": "retail-search" 7 | executors: 8 | - uses: jinahub+docker://Sentencizer 9 | -------------------------------------------------------------------------------- /tests/integration/flow/update/flows/custom_name_exposed_executor.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | with: 3 | protocol: http 4 | executors: 5 | - name: newsentencizer 6 | uses: jinahub+docker://Sentencizer 7 | jcloud: 8 | expose: true 9 | -------------------------------------------------------------------------------- /tests/integration/flow/basic/flows/http-flow-new-syntax.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | with: 3 | protocol: http 4 | executors: 5 | - name: sentencizer 6 | uses: jinaai+docker://jina-ai/Sentencizer:latest 7 | env: 8 | JINA_LOG_LEVEL: DEBUG 9 | -------------------------------------------------------------------------------- /tests/integration/flow/projects/simple/app.py: -------------------------------------------------------------------------------- 1 | from docarray import Document 2 | from jina import Flow 3 | 4 | f = Flow().add(uses='executor1/config.yml') 5 | 6 | with f: 7 | da = f.post('/', [Document(), Document()]) 8 | print(da.texts) 9 | -------------------------------------------------------------------------------- /tests/unit/flows/normalized_flows/flow2.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | executors: 3 | - name: ABC 4 | uses: jinaai+docker://ABC/hello 5 | - name: DEF 6 | uses: jinaai+sandbox://DEF/hello 7 | - name: XYZ 8 | uses: jinaai+serverless://XYZ/hello 9 | -------------------------------------------------------------------------------- /tests/integration/deployment/update/deployments/add_secret.yml: -------------------------------------------------------------------------------- 1 | jtype: Deployment 2 | with: 3 | uses: jinaai+docker://auth0-unified-64c3e19b1d2f398f/JCloudCISentencizer:latest 4 | env_from_secret: 5 | env1: 6 | name: secret1 7 | key: key1 8 | -------------------------------------------------------------------------------- /tests/integration/deployment/update/deployments/modify_delete_labels.yml: -------------------------------------------------------------------------------- 1 | jtype: Deployment 2 | with: 3 | uses: jinaai+docker://auth0-unified-64c3e19b1d2f398f/JCloudCISentencizer:latest 4 | jcloud: 5 | labels: 6 | "jina.ai/application": "retail-search" 7 | -------------------------------------------------------------------------------- /tests/integration/flow/expose/flows/single-executor-stateless.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | jcloud: 3 | gateway: 4 | expose: false 5 | executors: 6 | - name: sentencizer 7 | uses: jinahub+docker://Sentencizer 8 | jcloud: 9 | expose: true 10 | -------------------------------------------------------------------------------- /tests/unit/flows/local_flow/executor1/executor.py: -------------------------------------------------------------------------------- 1 | from jina import Executor, requests, DocumentArray 2 | 3 | 4 | class MyExecutor(Executor): 5 | @requests 6 | def foo(self, docs: DocumentArray, **kwargs): 7 | docs[:, 'text'] = 'hello, world!' 8 | -------------------------------------------------------------------------------- /tests/unit/flows/local_flow/executor2/executor.py: -------------------------------------------------------------------------------- 1 | from jina import Executor, requests, DocumentArray 2 | 3 | 4 | class MyExecutor(Executor): 5 | @requests 6 | def foo(self, docs: DocumentArray, **kwargs): 7 | docs[:, 'text'] = 'hello, world!' 8 | -------------------------------------------------------------------------------- /tests/unit/flows/mixed_flow/executor1/executor.py: -------------------------------------------------------------------------------- 1 | from jina import Executor, requests, DocumentArray 2 | 3 | 4 | class MyExecutor(Executor): 5 | @requests 6 | def foo(self, docs: DocumentArray, **kwargs): 7 | docs[:, 'text'] = 'hello, world!' 8 | -------------------------------------------------------------------------------- /jcloud/resources/project-template/executor1/executor.py: -------------------------------------------------------------------------------- 1 | from jina import Executor, requests, DocumentArray 2 | 3 | 4 | class MyExecutor(Executor): 5 | @requests 6 | def foo(self, docs: DocumentArray, **kwargs): 7 | docs[:, 'text'] = 'hello, world!' 8 | -------------------------------------------------------------------------------- /tests/integration/flow/update/flows/add_args.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | with: 3 | protocol: http 4 | executors: 5 | - uses: jinahub+docker://Sentencizer 6 | env: 7 | PUNCT_CHARS: '(!,)' 8 | uses_with: 9 | punct_chars: ${{ ENV.PUNCT_CHARS }} 10 | -------------------------------------------------------------------------------- /tests/integration/deployment/update/deployments/add_args.yml: -------------------------------------------------------------------------------- 1 | jtype: Deployment 2 | with: 3 | uses: jinaai+docker://auth0-unified-64c3e19b1d2f398f/JCloudCISentencizer:latest 4 | env: 5 | PUNCT_CHARS: "(!,)" 6 | uses_with: 7 | punct_chars: ${{ ENV.PUNCT_CHARS }} 8 | -------------------------------------------------------------------------------- /tests/integration/flow/executor_with_no_uses/flow/flow.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | executors: 3 | - name: executor1 4 | uses: executor1/config.yml 5 | - name: executor2 6 | uses: jinahub+docker://Sentencizer 7 | - name: joiner 8 | needs: [executor1, executor2] 9 | -------------------------------------------------------------------------------- /tests/integration/flow/update/flows/add_labels.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | with: 3 | protocol: http 4 | jcloud: 5 | labels: 6 | "jina.ai/username": "johndoe" 7 | "jina.ai/application": "fashion-search" 8 | executors: 9 | - uses: jinahub+docker://Sentencizer 10 | -------------------------------------------------------------------------------- /tests/integration/flow/validate/flows/invalid-flow.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | gateway: 3 | protocol: http 4 | executors: 5 | - name: sentencizer 6 | uses: jinahub+docker://Sentencizer 7 | jcloud: 8 | monitor: 9 | traces: 10 | host: 123 11 | -------------------------------------------------------------------------------- /tests/integration/deployment/custom_name/deployments/valid-name-deployment.yml: -------------------------------------------------------------------------------- 1 | jtype: Deployment # Tests only for API layer 2 | with: 3 | protocol: http 4 | uses: jinaai+docker://auth0-unified-64c3e19b1d2f398f/JCloudCISentencizer:latest 5 | jcloud: 6 | name: fashion-data 7 | -------------------------------------------------------------------------------- /tests/integration/flow/executor_with_no_uses/flow/executor1/executor.py: -------------------------------------------------------------------------------- 1 | from jina import Executor, requests, DocumentArray 2 | 3 | 4 | class MyExecutor(Executor): 5 | @requests 6 | def foo(self, docs: DocumentArray, **kwargs): 7 | docs[:, 'text'] = 'hello, world!' 8 | -------------------------------------------------------------------------------- /tests/integration/flow/executor_with_no_uses/flow/executor2/executor.py: -------------------------------------------------------------------------------- 1 | from jina import Executor, requests, DocumentArray 2 | 3 | 4 | class MyExecutor(Executor): 5 | @requests 6 | def foo(self, docs: DocumentArray, **kwargs): 7 | docs[:, 'text'] = 'hello, world!' 8 | -------------------------------------------------------------------------------- /tests/integration/flow/remove_multiple/flows/flow1.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | with: 3 | protocol: http 4 | jcloud: 5 | name: remove-multiple-1 6 | executors: 7 | - name: sentencizer 8 | uses: jinahub+docker://Sentencizer 9 | env: 10 | JINA_LOG_LEVEL : DEBUG 11 | -------------------------------------------------------------------------------- /tests/integration/flow/remove_multiple/flows/flow2.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | with: 3 | protocol: http 4 | jcloud: 5 | name: remove-multiple-2 6 | executors: 7 | - name: sentencizer 8 | uses: jinahub+docker://Sentencizer 9 | env: 10 | JINA_LOG_LEVEL : DEBUG 11 | -------------------------------------------------------------------------------- /tests/integration/deployment/custom_labels/deployments/deployment-with-labels.yml: -------------------------------------------------------------------------------- 1 | jtype: Deployment 2 | with: 3 | protocol: grpc 4 | uses: jinaai+docker://auth0-unified-64c3e19b1d2f398f/JCloudCISentencizer:latest 5 | jcloud: 6 | labels: 7 | label1: value1 8 | label2: value2 9 | -------------------------------------------------------------------------------- /tests/integration/deployment/update/deployments/add_labels.yml: -------------------------------------------------------------------------------- 1 | jtype: Deployment 2 | with: 3 | uses: jinaai+docker://auth0-unified-64c3e19b1d2f398f/JCloudCISentencizer:latest 4 | jcloud: 5 | labels: 6 | "jina.ai/username": "johndoe" 7 | "jina.ai/application": "fashion-search" 8 | -------------------------------------------------------------------------------- /tests/integration/flow/custom_resources/flows/gateway-resources.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | jcloud: 3 | gateway: 4 | resources: 5 | requests: 6 | memory: 800M 7 | cpu: 0.4 8 | executors: 9 | - name: sentencizer 10 | uses: jinahub+docker://Sentencizer 11 | -------------------------------------------------------------------------------- /tests/integration/flow/update/flows/update_resources.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | with: 3 | protocol: http 4 | executors: 5 | - uses: jinahub+docker://Sentencizer 6 | jcloud: 7 | resources: 8 | capacity: on-demand 9 | cpu: 0.2 10 | memory: "200M" 11 | -------------------------------------------------------------------------------- /tests/integration/deployment/custom_name/deployments/invalid-name-deployment.yml: -------------------------------------------------------------------------------- 1 | jtype: Deployment # Tests only for API layer 2 | with: 3 | protocol: http 4 | uses: jinaai+docker://auth0-unified-64c3e19b1d2f398f/JCloudCISentencizer:latest 5 | jcloud: 6 | name: abc_def#1/ # invalid name 7 | -------------------------------------------------------------------------------- /tests/integration/flow/expose/flows/single-executor-stateful.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | with: 3 | protocol: http 4 | jcloud: 5 | gateway: 6 | expose: false 7 | executors: 8 | - name: simpleindexer 9 | uses: jinahub+docker://SimpleIndexer 10 | jcloud: 11 | expose: true 12 | -------------------------------------------------------------------------------- /tests/integration/deployment/envvars/deployments/envvars.yml: -------------------------------------------------------------------------------- 1 | jtype: Deployment 2 | with: 3 | uses: jinaai+docker://auth0-unified-64c3e19b1d2f398f/JCloudCISentencizer:latest 4 | env: 5 | JINA_LOG_LEVEL: DEBUG 6 | PUNCT_CHARS: "(!,)" 7 | uses_with: 8 | punct_chars: ${{ ENV.PUNCT_CHARS }} 9 | -------------------------------------------------------------------------------- /tests/integration/deployment/update/deployments/add_env.yml: -------------------------------------------------------------------------------- 1 | jtype: Deployment 2 | with: 3 | uses: jinaai+docker://auth0-unified-64c3e19b1d2f398f/JCloudCISentencizer:latest 4 | env: 5 | JINA_LOG_LEVEL: DEBUG 6 | PUNCT_CHARS: "(!,)" 7 | uses_with: 8 | punct_chars: ${{ ENV.PUNCT_CHARS }} 9 | -------------------------------------------------------------------------------- /tests/integration/flow/update/flows/add_env.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | with: 3 | protocol: http 4 | executors: 5 | - uses: jinahub+docker://Sentencizer 6 | env: 7 | JINA_LOG_LEVEL : DEBUG 8 | PUNCT_CHARS: '(!,)' 9 | uses_with: 10 | punct_chars: ${{ ENV.PUNCT_CHARS }} 11 | -------------------------------------------------------------------------------- /tests/integration/deployment/autoscale/deployments/autoscale-http.yml: -------------------------------------------------------------------------------- 1 | jtype: Deployment 2 | with: 3 | protocol: http 4 | uses: jinaai+docker://auth0-unified-64c3e19b1d2f398f/JCloudCISentencizer:latest 5 | jcloud: 6 | autoscale: 7 | max: 3 8 | min: 1 9 | target: 40 10 | metric: rps 11 | -------------------------------------------------------------------------------- /tests/integration/flow/projects/simple/executor1/executor.py: -------------------------------------------------------------------------------- 1 | from jina import DocumentArray, Executor, requests 2 | 3 | 4 | class MyExecutor(Executor): 5 | @requests 6 | def foo(self, docs: DocumentArray, **kwargs): 7 | docs[0].text = 'hello, world!' 8 | docs[1].text = 'goodbye, world!' 9 | -------------------------------------------------------------------------------- /tests/integration/flow/envvars/flows/envs-in-flow.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | with: 3 | protocol: http 4 | executors: 5 | - name: sentencizer 6 | uses: jinahub+docker://Sentencizer 7 | env: 8 | JINA_LOG_LEVEL : DEBUG 9 | PUNCT_CHARS: '(!,)' 10 | uses_with: 11 | punct_chars: ${{ ENV.PUNCT_CHARS }} 12 | -------------------------------------------------------------------------------- /tests/integration/flow/projects/executors_with_shards/executor1/executor.py: -------------------------------------------------------------------------------- 1 | from jina import Document, DocumentArray, Executor, requests 2 | 3 | 4 | class MyExecutor(Executor): 5 | @requests 6 | def get_shard_id(self, docs: DocumentArray, **kwargs): 7 | return DocumentArray([Document(text=str(self.runtime_args.shard_id))]) 8 | -------------------------------------------------------------------------------- /tests/integration/deployment/custom_resources/deployments/deployment-with-custom-resources.yml: -------------------------------------------------------------------------------- 1 | jtype: Deployment 2 | with: 3 | name: c2instance 4 | uses: jinaai+docker://auth0-unified-64c3e19b1d2f398f/JCloudCISentencizer:latest 5 | jcloud: 6 | nodeSelector: 7 | karpenter.sh/capacity-type: on-demand 8 | resources: 9 | instance: C2 10 | nodeGroup: ALL 11 | -------------------------------------------------------------------------------- /scripts/get-last-release-note.py: -------------------------------------------------------------------------------- 1 | ## under jina root dir 2 | # python scripts/get-last-release-note.py 3 | ## result in root/tmp.md 4 | 5 | with open('CHANGELOG.md') as fp: 6 | n = [] 7 | for v in fp: 8 | if v.startswith('## Release Note'): 9 | n.clear() 10 | n.append(v) 11 | 12 | with open('tmp.md', 'w') as fp: 13 | fp.writelines(n) 14 | -------------------------------------------------------------------------------- /tests/integration/flow/expose/flows/gateway-and-executors.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | with: 3 | protocol: http 4 | jcloud: 5 | expose: true 6 | executors: 7 | - name: sentencizer 8 | uses: jinahub+docker://Sentencizer 9 | jcloud: 10 | expose: true 11 | - name: simpleindexer 12 | uses: jinahub+docker://SimpleIndexer 13 | jcloud: 14 | expose: true 15 | -------------------------------------------------------------------------------- /tests/integration/flow/projects/multi_executors/flow.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | with: 3 | protocol: http 4 | executors: 5 | - name: executor1 6 | uses: executor1/config.yml 7 | uses_with: 8 | init_var: init_var_ex1 9 | - name: executor2 10 | uses: executor2/config.yml 11 | install_requirements: true 12 | uses_with: 13 | init_var: init_var_ex2 14 | -------------------------------------------------------------------------------- /tests/unit/flows/flow-with-labels.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | jcloud: 3 | labels: 4 | test-label-one: 1 5 | test-label-two: 231 6 | executors: 7 | - uses: jinahub+docker://Sentencizer 8 | jcloud: 9 | labels: 10 | test-executor-label-one: 352 11 | test-executor-label-two: "hello" 12 | test-label-bool: true 13 | test-label-complex: 1j 14 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/ambv/black 3 | rev: 22.3.0 4 | hooks: 5 | - id: black 6 | types: [python] 7 | exclude: ^(docarray/proto/docarray_pb2.py|docs/|docarray/resources/) 8 | args: 9 | - -S 10 | - repo: https://github.com/asottile/blacken-docs 11 | rev: v1.12.1 12 | hooks: 13 | - id: blacken-docs 14 | args: 15 | - -S -------------------------------------------------------------------------------- /tests/unit/flows/flow1-test.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | executors: 3 | - name: abc 4 | uses: jinahub+docker://Sentencizer 5 | - name: def 6 | - name: joiner 7 | needs: 8 | - abc 9 | - def 10 | jcloud: 11 | docarray: 0.21.1 12 | version: 3.20.3 13 | with: 14 | env_from_secret: 15 | env1: 16 | name: test 17 | key: env1 18 | env2: 19 | name: test 20 | key: env2 21 | -------------------------------------------------------------------------------- /tests/integration/flow/autoscale/flows/executors-autoscaled.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | with: 3 | protocol: http 4 | jcloud: 5 | gateway: 6 | ingress: kong 7 | executors: 8 | - name: auto1 9 | uses: jinahub+docker://Sentencizer 10 | jcloud: 11 | autoscale: 12 | min: 1 13 | max: 2 14 | metric: rps 15 | target: 2 16 | - name: auto2 17 | uses: jinahub+serverless://Sentencizer 18 | -------------------------------------------------------------------------------- /tests/integration/flow/projects/multi_executors/executor1/executor.py: -------------------------------------------------------------------------------- 1 | from jina import DocumentArray, Executor, requests 2 | 3 | 4 | class MyExecutor1(Executor): 5 | def __init__(self, init_var, **kwargs): 6 | super().__init__(**kwargs) 7 | self.init_var = init_var 8 | 9 | @requests 10 | def foo(self, docs: DocumentArray, **kwargs): 11 | for d in docs: 12 | d.tags.update({'MyExecutor1': self.init_var}) 13 | -------------------------------------------------------------------------------- /tests/integration/flow/projects/multi_executors/executor2/executor.py: -------------------------------------------------------------------------------- 1 | from jina import DocumentArray, Executor, requests 2 | 3 | 4 | class MyExecutor2(Executor): 5 | def __init__(self, init_var, **kwargs): 6 | super().__init__(**kwargs) 7 | self.init_var = init_var 8 | 9 | @requests 10 | def foo(self, docs: DocumentArray, **kwargs): 11 | for d in docs: 12 | d.tags.update({'MyExecutor2': self.init_var}) 13 | -------------------------------------------------------------------------------- /tests/integration/flow/projects/envvars_context_syntax/executor1/executor.py: -------------------------------------------------------------------------------- 1 | from jina import DocumentArray, Executor, requests 2 | 3 | 4 | class MyExecutor(Executor): 5 | def __init__(self, var_a, var_b, *args, **kwargs): 6 | super().__init__(*args, **kwargs) 7 | self.var_a = var_a 8 | self.var_b = var_b 9 | 10 | @requests 11 | def foo(self, docs: DocumentArray, **kwargs): 12 | docs[:, 'tags'] = {'var_a': self.var_a, 'var_b': self.var_b} 13 | -------------------------------------------------------------------------------- /tests/integration/flow/projects/envvars_default_file/executor1/executor.py: -------------------------------------------------------------------------------- 1 | from jina import DocumentArray, Executor, requests 2 | 3 | 4 | class MyExecutor(Executor): 5 | def __init__(self, var_a, var_b, *args, **kwargs): 6 | super().__init__(*args, **kwargs) 7 | self.var_a = var_a 8 | self.var_b = var_b 9 | 10 | @requests 11 | def foo(self, docs: DocumentArray, **kwargs): 12 | docs[:, 'tags'] = {'var_a': self.var_a, 'var_b': self.var_b} 13 | -------------------------------------------------------------------------------- /tests/integration/flow/custom_resources/flows/executor-resources.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | executors: 3 | - name: sentencizer 4 | uses: jinahub+docker://Sentencizer 5 | jcloud: 6 | capacity: spot 7 | resources: 8 | memory: 800M 9 | cpu: 1 10 | - name: simpleindexer 11 | uses: jinahub+docker://SimpleIndexer 12 | jcloud: 13 | capacity: on-demand 14 | resources: 15 | memory: 200M 16 | storage: 17 | kind: ebs 18 | size: 1Gi 19 | -------------------------------------------------------------------------------- /tests/integration/deployment/basic/deployments/multi-protocol-deployment.yml: -------------------------------------------------------------------------------- 1 | jtype: Deployment 2 | gateway: 3 | uses_with: 4 | http_port: "8000" 5 | grpc_port: "9000" 6 | port: 7 | - 8000 8 | - 9000 9 | protocol: 10 | - http 11 | - grpc 12 | jcloud: 13 | custom_dns_http: 14 | - operator-test.docsqa.jina.ai 15 | custom_dns_grpc: 16 | - operator-test.wolf.jina.ai 17 | with: 18 | uses: jinaai+docker://auth0-unified-64c3e19b1d2f398f/JCloudCISentencizer:latest 19 | -------------------------------------------------------------------------------- /scripts/black.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | pip install black==22.3.0 3 | arrVar=() 4 | echo we ignore non-*.py files and files generated from protobuf 5 | excluded_files=( 6 | docarray/proto/docarray_pb2.py 7 | docs/conf.py 8 | ) 9 | for changed_file in $CHANGED_FILES; do 10 | if [[ ${changed_file} == *.py ]] && ! [[ " ${excluded_files[@]} " =~ " ${changed_file} " ]]; then 11 | echo checking ${changed_file} 12 | arrVar+=(${changed_file}) 13 | fi 14 | done 15 | if [ ${#arrVar[@]} -ne 0 ]; then 16 | black -S --check "${arrVar[@]}" 17 | fi 18 | -------------------------------------------------------------------------------- /jcloud/parsers/get.py: -------------------------------------------------------------------------------- 1 | from .helper import _chf 2 | 3 | 4 | def set_get_resource_parser(subparser, resource): 5 | get_parser = subparser.add_parser( 6 | 'get', 7 | help=f'Get the details of a {resource.title()}.', 8 | formatter_class=_chf, 9 | ) 10 | 11 | get_parser.add_argument( 12 | 'name', 13 | type=str, 14 | help=f'The name of the {resource.title()}.', 15 | ) 16 | 17 | get_parser.add_argument( 18 | 'flow', 19 | type=str, 20 | help='The string ID of the Flow.', 21 | ) 22 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | # Add 'label1' to any changes within 'example' folder or any subfolders 2 | area/docs: 3 | - docs/**/* 4 | - ./*.md 5 | 6 | area/testing: 7 | - tests/**/* 8 | 9 | area/setup: 10 | - setup.py 11 | - extra-requirements.txt 12 | - requirements.txt 13 | - MANIFEST.in 14 | 15 | area/core: 16 | - jcloud/**/* 17 | 18 | area/entrypoint: 19 | - jcloud/__init__.py 20 | 21 | area/housekeeping: 22 | - .github/**/* 23 | - ./.gitignore 24 | - ./*.yaml 25 | - ./*.yml 26 | 27 | area/cicd: 28 | - .github/workflows/**/* 29 | 30 | area/docker: 31 | - Dockerfiles/**/* 32 | - ./.dockerignore 33 | 34 | area/script: 35 | - script/**/* 36 | 37 | 38 | -------------------------------------------------------------------------------- /tests/integration/flow/test_simple.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | from jina import Client, DocumentArray 5 | 6 | from jcloud.flow import CloudFlow 7 | 8 | cur_dir = os.path.dirname(os.path.abspath(__file__)) 9 | 10 | 11 | def test_project_simple(): 12 | with CloudFlow(path=os.path.join(cur_dir, 'projects', 'simple')) as flow: 13 | assert flow.endpoints != {} 14 | assert 'gateway' in flow.endpoints 15 | gateway = flow.endpoints['gateway'] 16 | 17 | da = Client(host=gateway).post( 18 | on='/', 19 | inputs=DocumentArray.empty(2), 20 | ) 21 | assert da.texts == ['hello, world!', 'goodbye, world!'] 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /tests/integration/flow/jina_version/test_custom_jina_version.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from jina import Client, Document 4 | 5 | from jcloud.flow import CloudFlow 6 | 7 | flows_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'flows') 8 | flow_file = 'custom-jina-version.yml' 9 | 10 | 11 | def test_expose_version_arg(): 12 | with CloudFlow(path=os.path.join(flows_dir, flow_file)) as flow: 13 | assert flow.endpoints != {} 14 | assert 'gateway' in flow.endpoints 15 | gateway = flow.endpoints['gateway'] 16 | da = Client(host=gateway).post(on="/", inputs=Document(text="Hello. World.")) 17 | assert da[0].chunks[0].text == "Hello." and da[0].chunks[1].text == "World." 18 | -------------------------------------------------------------------------------- /tests/integration/flow/autoscale/test_executors_autoscaled.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | from jina import Client, Document 5 | 6 | from jcloud.flow import CloudFlow 7 | 8 | cur_dir = os.path.dirname(os.path.abspath(__file__)) 9 | 10 | 11 | @pytest.mark.skip('unskip once autoscaling is implemented') 12 | def test_customized_resources(): 13 | FLOW_FILE_PATH = os.path.join(cur_dir, "flows", "executors-autoscaled.yml") 14 | with CloudFlow(path=FLOW_FILE_PATH, name="executors-autoscaled") as flow: 15 | da = Client(host=flow.gateway).post( 16 | on="/", inputs=Document(text="Hello. World.") 17 | ) 18 | assert da[0].chunks[0].text == "Hello." and da[0].chunks[1].text == "World." 19 | -------------------------------------------------------------------------------- /scripts/get-all-test-paths.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -ex 4 | 5 | TEST_SUITE=$1 6 | DEFAULT_BATCH_SIZE=5 7 | BATCH_SIZE="${2:-$DEFAULT_BATCH_SIZE}" 8 | 9 | declare -a unit_tests=($(find tests/unit -name "test_*.py")) 10 | declare -a integration_tests=($(find tests/integration -name "test_*.py")) 11 | declare -a all_tests=("${unit_tests[@]}" "${integration_tests[@]}") 12 | 13 | if [ "$TEST_SUITE" == "unit" ]; then 14 | dest="$(echo "${unit_tests[@]}" | xargs -n$BATCH_SIZE)" 15 | elif [[ "$TEST_SUITE" == "integration" ]]; then 16 | dest="$(echo "${integration_tests[@]}" | xargs -n$BATCH_SIZE)" 17 | else 18 | dest="$(echo "${all_tests[@]}" | xargs -n$BATCH_SIZE)" 19 | fi 20 | 21 | printf '%s\n' "${dest[@]}" | jq -R . | jq -cs . 22 | -------------------------------------------------------------------------------- /tests/integration/flow/executor_uses/test_executor_uses.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | from jina import Client, Document 5 | 6 | from jcloud.flow import CloudFlow 7 | 8 | cur_dir = os.path.dirname(os.path.abspath(__file__)) 9 | flow_file = 'flow.yml' 10 | 11 | 12 | def test_legacy_executor_syntax(): 13 | with CloudFlow(path=os.path.join(cur_dir, flow_file)) as flow: 14 | assert flow.endpoints != {} 15 | assert 'gateway' in flow.endpoints 16 | gateway = flow.endpoints['gateway'] 17 | 18 | da = Client(host=gateway).post( 19 | on='/', 20 | inputs=Document(text='Hello. World.'), 21 | ) 22 | assert da[0].chunks[0].text == 'Hello.' and da[0].chunks[1].text == 'World.' 23 | -------------------------------------------------------------------------------- /tests/integration/flow/executor_with_no_uses/test_executor_with_no_uses.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | from jina import Client, Document, DocumentArray 5 | 6 | from jcloud.flow import CloudFlow 7 | 8 | cur_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'flow') 9 | flow_file = 'flow.yml' 10 | 11 | 12 | def test_executor_with_no_uses(): 13 | with CloudFlow(path=os.path.join(cur_dir, flow_file)) as flow: 14 | assert flow.endpoints != {} 15 | assert 'gateway' in flow.endpoints 16 | gateway = flow.endpoints['gateway'] 17 | 18 | da = Client(host=gateway).post( 19 | on='/', 20 | inputs=DocumentArray(Document(text=f'text-{i}') for i in range(50)), 21 | ) 22 | assert len(da.texts) == 50 23 | -------------------------------------------------------------------------------- /tests/integration/flow/validate/test_validate.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from jina import Client, Document, DocumentArray 4 | 5 | from jcloud.flow import CloudFlow 6 | 7 | flows_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'flows') 8 | valid_flow_file = 'valid-flow.yml' 9 | invalid_flow_file = 'invalid-flow.yml' 10 | 11 | 12 | async def test_valid_flow(): 13 | validate_response = await CloudFlow( 14 | path=os.path.join(flows_dir, valid_flow_file) 15 | ).validate() 16 | 17 | assert len(validate_response['errors']) == 0 18 | 19 | 20 | async def test_invalid_flow(): 21 | validate_response = await CloudFlow( 22 | path=os.path.join(flows_dir, invalid_flow_file) 23 | ).validate() 24 | assert len(validate_response['errors']) == 2 25 | -------------------------------------------------------------------------------- /tests/integration/flow/test_multi_executors.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | from jina import Client, DocumentArray 5 | 6 | from jcloud.flow import CloudFlow 7 | 8 | cur_dir = os.path.dirname(os.path.abspath(__file__)) 9 | 10 | 11 | def test_project_multi_executors(): 12 | with CloudFlow(path=os.path.join(cur_dir, 'projects', 'multi_executors')) as flow: 13 | assert flow.endpoints != {} 14 | assert 'gateway' in flow.endpoints 15 | gateway = flow.endpoints['gateway'] 16 | 17 | da: DocumentArray = Client(host=gateway).post( 18 | on='/', 19 | inputs=DocumentArray.empty(2), 20 | ) 21 | for d in da: 22 | assert d.tags['MyExecutor1'] == 'init_var_ex1' 23 | assert d.tags['MyExecutor2'] == 'init_var_ex2' 24 | -------------------------------------------------------------------------------- /tests/integration/flow/basic/test_basic_grpc.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from jina import Client, Document, DocumentArray 4 | 5 | from jcloud.flow import CloudFlow 6 | 7 | flows_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'flows') 8 | flow_file = 'grpc-flow.yml' 9 | protocol = 'grpc' 10 | 11 | 12 | def test_basic_grpc_flow(): 13 | with CloudFlow(path=os.path.join(flows_dir, flow_file)) as flow: 14 | assert flow.endpoints != {} 15 | assert 'gateway' in flow.endpoints 16 | gateway = flow.endpoints['gateway'] 17 | assert gateway.startswith(f'{protocol}s://') 18 | 19 | da = Client(host=gateway).post( 20 | on='/', 21 | inputs=DocumentArray(Document(text=f'text-{i}') for i in range(50)), 22 | ) 23 | assert len(da.texts) == 50 24 | -------------------------------------------------------------------------------- /tests/integration/flow/basic/test_basic_http.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from jina import Client, Document, DocumentArray 4 | 5 | from jcloud.flow import CloudFlow 6 | 7 | flows_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'flows') 8 | flow_file = 'http-flow.yml' 9 | protocol = 'http' 10 | 11 | 12 | def test_basic_http_flow(): 13 | with CloudFlow(path=os.path.join(flows_dir, flow_file)) as flow: 14 | assert flow.endpoints != {} 15 | assert 'gateway' in flow.endpoints 16 | gateway = flow.endpoints['gateway'] 17 | assert gateway.startswith(f'{protocol}s://') 18 | 19 | da = Client(host=gateway).post( 20 | on='/', 21 | inputs=DocumentArray(Document(text=f'text-{i}') for i in range(50)), 22 | ) 23 | assert len(da.texts) == 50 24 | -------------------------------------------------------------------------------- /tests/integration/flow/envvars/test_yaml_env_file.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | from jina import Client, Document, DocumentArray 5 | 6 | from jcloud.flow import CloudFlow 7 | 8 | flows_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'flows') 9 | flow_file = 'envs-in-flow.yml' 10 | 11 | 12 | def test_yaml_env_file(): 13 | with CloudFlow(path=os.path.join(flows_dir, flow_file)) as flow: 14 | assert flow.endpoints != {} 15 | assert 'gateway' in flow.endpoints 16 | gateway = flow.endpoints['gateway'] 17 | 18 | da = Client(host=gateway).post( 19 | on='/', 20 | inputs=DocumentArray(Document(text='hello! There? abc')), 21 | ) 22 | assert da[0].chunks[0].text == 'hello!' 23 | assert da[0].chunks[1].text == 'There? abc' 24 | -------------------------------------------------------------------------------- /tests/integration/flow/basic/test_basic_websocket.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from jina import Client, Document, DocumentArray 4 | 5 | from jcloud.flow import CloudFlow 6 | 7 | flows_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'flows') 8 | flow_file = 'websocket-flow.yml' 9 | protocol = 'ws' 10 | 11 | 12 | def test_basic_websocket_flow(): 13 | with CloudFlow(path=os.path.join(flows_dir, flow_file)) as flow: 14 | assert flow.endpoints != {} 15 | assert 'gateway' in flow.endpoints 16 | gateway = flow.endpoints['gateway'] 17 | assert gateway.startswith(f'{protocol}s://') 18 | 19 | da = Client(host=gateway).post( 20 | on='/', 21 | inputs=DocumentArray(Document(text=f'text-{i}') for i in range(50)), 22 | ) 23 | assert len(da.texts) == 50 24 | -------------------------------------------------------------------------------- /jcloud/__main__.py: -------------------------------------------------------------------------------- 1 | def main(): 2 | import logging 3 | import os 4 | 5 | from .parsers import get_main_parser 6 | 7 | args = get_main_parser().parse_args() 8 | if args.loglevel: 9 | os.environ['JCLOUD_LOGLEVEL'] = args.loglevel 10 | 11 | logging.getLogger('asyncio').setLevel(logging.WARNING) 12 | 13 | try: 14 | if 'NO_VERSION_CHECK' not in os.environ: 15 | from .helper import is_latest_version 16 | 17 | is_latest_version() 18 | from jcloud import api 19 | 20 | if hasattr(args, 'subcommand'): 21 | getattr(api, args.subcommand.replace('-', '_'))(args) 22 | else: 23 | getattr(api, args.jc_cli.replace('-', '_'))(args) 24 | except KeyboardInterrupt: 25 | pass 26 | 27 | 28 | if __name__ == '__main__': 29 | main() 30 | -------------------------------------------------------------------------------- /tests/integration/flow/custom_resources/test_gateway_resources.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from jina import Client, Document, DocumentArray 4 | 5 | from jcloud.flow import CloudFlow 6 | 7 | flows_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'flows') 8 | protocol = 'grpc' 9 | flow_file = 'gateway-resources.yml' 10 | 11 | 12 | def test_gateway_resources(): 13 | with CloudFlow(path=os.path.join(flows_dir, flow_file)) as flow: 14 | assert flow.endpoints != {} 15 | assert 'gateway' in flow.endpoints 16 | gateway = flow.endpoints['gateway'] 17 | assert gateway.startswith(f'{protocol}s://') 18 | 19 | da = Client(host=gateway).post( 20 | on='/', 21 | inputs=DocumentArray(Document(text=f'text-{i}') for i in range(50)), 22 | ) 23 | assert len(da.texts) == 50 24 | -------------------------------------------------------------------------------- /tests/integration/flow/custom_resources/test_executor_resources.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | from jina import Client, Document, DocumentArray 5 | 6 | from jcloud.flow import CloudFlow 7 | 8 | flows_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'flows') 9 | protocol = 'grpc' 10 | flow_file = 'executor-resources.yml' 11 | 12 | 13 | def test_executor_resources(): 14 | with CloudFlow(path=os.path.join(flows_dir, flow_file)) as flow: 15 | assert flow.endpoints != {} 16 | assert 'gateway' in flow.endpoints 17 | gateway = flow.endpoints['gateway'] 18 | assert gateway.startswith(f'{protocol}s://') 19 | 20 | da = Client(host=gateway).post( 21 | on='/', 22 | inputs=DocumentArray(Document(text=f'text-{i}') for i in range(50)), 23 | ) 24 | assert len(da.texts) == 50 25 | -------------------------------------------------------------------------------- /tests/integration/deployment/jina_version/test_custom_jina_version.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from jina import Client, Document 4 | 5 | from jcloud.deployment import CloudDeployment 6 | 7 | deployments_dir = os.path.join( 8 | os.path.dirname(os.path.abspath(__file__)), 'deployments' 9 | ) 10 | deployment_file = 'deployment-3.21.0.yml' 11 | executor_name = 'executor' 12 | 13 | 14 | def test_expose_version_arg(): 15 | with CloudDeployment( 16 | path=os.path.join(deployments_dir, deployment_file) 17 | ) as deployment: 18 | assert deployment.endpoints != {} 19 | assert executor_name in deployment.endpoints 20 | endpoint = deployment.endpoints[executor_name] 21 | da = Client(host=endpoint).post(on="/", inputs=Document(text="Hello. World.")) 22 | assert da[0].chunks[0].text == "Hello." and da[0].chunks[1].text == "World." 23 | -------------------------------------------------------------------------------- /tests/unit/flows/normalized_flows/flow6.yml: -------------------------------------------------------------------------------- 1 | jtype: Flow 2 | with: 3 | cors: true 4 | protocol: http 5 | port_expose: 12345 6 | expose_endpoints: 7 | /abc: 8 | methods: ['POST'] 9 | executors: 10 | - name: E1 11 | uses: jinahub+docker://E1 12 | 13 | - name: E2 14 | uses: jinahub+docker://E1 15 | shards: 2 16 | 17 | - name: E3 18 | needs: E1 19 | when: 20 | tags__answered: 21 | $exists: False 22 | uses: jinahub+docker://E3 23 | uses_with: 24 | limit: 5 25 | replicas: 3 26 | 27 | - name: E4 28 | needs: E2 29 | when: 30 | tags__answered: 31 | $exists: False 32 | uses: jinahub+docker://E4 33 | install_requirements: true 34 | uses_with: 35 | abc: def 36 | timeout_ready: -1 37 | 38 | - name: E5 39 | needs: [E3, E4] 40 | uses: jinahub+docker://E5 41 | -------------------------------------------------------------------------------- /tests/integration/deployment/basic/test_basic_grpc.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from jcloud.deployment import CloudDeployment 4 | from jcloud.constants import Phase 5 | 6 | from tests.utils import utils 7 | 8 | deployments_dir = os.path.join( 9 | os.path.dirname(os.path.abspath(__file__)), 'deployments' 10 | ) 11 | deployment_file = 'grpc-deployment.yml' 12 | protocol = 'grpc' 13 | executor_name = 'executor' 14 | 15 | 16 | def test_basic_grpc_deployment(): 17 | with CloudDeployment( 18 | path=os.path.join(deployments_dir, deployment_file) 19 | ) as deployment: 20 | assert utils.eventually_reaches_phase(deployment, Phase.Serving) 21 | assert deployment.endpoints != {} 22 | assert executor_name in deployment.endpoints 23 | endpoint = deployment.endpoints[executor_name] 24 | assert endpoint.startswith(f'{protocol}s://') 25 | 26 | assert utils.eventually_serve_requests(endpoint) 27 | -------------------------------------------------------------------------------- /tests/integration/deployment/basic/test_basic_http.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from jcloud.deployment import CloudDeployment 4 | from jcloud.constants import Phase 5 | 6 | from tests.utils import utils 7 | 8 | deployments_dir = os.path.join( 9 | os.path.dirname(os.path.abspath(__file__)), 'deployments' 10 | ) 11 | deployment_file = 'http-deployment.yml' 12 | protocol = 'http' 13 | executor_name = 'executor' 14 | 15 | 16 | def test_basic_grpc_deployment(): 17 | with CloudDeployment( 18 | path=os.path.join(deployments_dir, deployment_file) 19 | ) as deployment: 20 | assert utils.eventually_reaches_phase(deployment, Phase.Serving) 21 | assert deployment.endpoints != {} 22 | assert executor_name in deployment.endpoints 23 | endpoint = deployment.endpoints[executor_name] 24 | assert endpoint.startswith(f'{protocol}s://') 25 | 26 | assert utils.eventually_serve_requests(endpoint) 27 | -------------------------------------------------------------------------------- /tests/integration/flow/custom_labels/test_custom_labels.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | from jina import Client, Document, DocumentArray 5 | 6 | from jcloud.flow import CloudFlow 7 | 8 | flows_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'flows') 9 | protocol = 'grpc' 10 | 11 | 12 | def test_custom_labels(): 13 | labels_file = 'valid-labels.yml' 14 | with CloudFlow(path=os.path.join(flows_dir, labels_file)) as flow: 15 | assert flow.endpoints != {} 16 | assert 'gateway' in flow.endpoints 17 | gateway = flow.endpoints['gateway'] 18 | assert gateway.startswith(f'{protocol}s://') 19 | 20 | da = Client(host=gateway).post( 21 | on='/', 22 | inputs=DocumentArray(Document(text=f'text-{i}') for i in range(50)), 23 | ) 24 | assert len(da.texts) == 50 25 | 26 | # TODO: check that the labels are actually set by searching for them 27 | -------------------------------------------------------------------------------- /tests/integration/deployment/autoscale/test_autoscale.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from jcloud.deployment import CloudDeployment 4 | from jcloud.constants import Phase 5 | 6 | from tests.utils import utils 7 | 8 | cur_dir = os.path.dirname(os.path.abspath(__file__)) 9 | 10 | protocol = 'http' 11 | executor_name = 'executor' 12 | 13 | 14 | # @pytest.mark.skip('unskip once autoscaling is implemented') 15 | def test_autoscale_deployment(): 16 | deployment_file_path = os.path.join(cur_dir, "deployments", "autoscale-http.yml") 17 | with CloudDeployment(path=deployment_file_path) as deployment: 18 | assert utils.eventually_reaches_phase(deployment, Phase.Serving) 19 | assert deployment.endpoints != {} 20 | assert executor_name in deployment.endpoints 21 | endpoint = deployment.endpoints[executor_name] 22 | assert endpoint.startswith(f'{protocol}s://') 23 | assert utils.eventually_serve_requests(endpoint) 24 | -------------------------------------------------------------------------------- /tests/integration/deployment/envvars/test_yaml_env_file.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | from jina import Client, Document, DocumentArray 5 | 6 | from jcloud.deployment import CloudDeployment 7 | 8 | deployments_dir = os.path.join( 9 | os.path.dirname(os.path.abspath(__file__)), 'deployments' 10 | ) 11 | deployment_file = 'envvars.yml' 12 | executor_name = 'executor' 13 | 14 | 15 | def test_yaml_env_file(): 16 | with CloudDeployment( 17 | path=os.path.join(deployments_dir, deployment_file) 18 | ) as deployment: 19 | assert deployment.endpoints != {} 20 | assert executor_name in deployment.endpoints 21 | endpoint = deployment.endpoints[executor_name] 22 | 23 | da = Client(host=endpoint).post( 24 | on='/', 25 | inputs=DocumentArray(Document(text='hello! There? abc')), 26 | ) 27 | assert da[0].chunks[0].text == 'hello!' 28 | assert da[0].chunks[1].text == 'There? abc' 29 | -------------------------------------------------------------------------------- /tests/integration/flow/envvars/test_envvars_context_syntax.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Dict 3 | 4 | import pytest 5 | from jina import Client, DocumentArray 6 | 7 | from jcloud.flow import CloudFlow 8 | 9 | projects_dir = os.path.join( 10 | os.path.dirname(os.path.abspath(__file__)), '..', 'projects' 11 | ) 12 | protocol = 'grpc' 13 | 14 | 15 | def sorted_dict(d: Dict): 16 | return dict(sorted(d.items())) 17 | 18 | 19 | def test_envvars_context_syntax(): 20 | with CloudFlow( 21 | path=os.path.join(projects_dir, 'envvars_context_syntax'), 22 | ) as flow: 23 | assert flow.endpoints != {} 24 | assert 'gateway' in flow.endpoints 25 | gateway = flow.endpoints['gateway'] 26 | assert gateway.startswith(f'{protocol}s://') 27 | 28 | da = Client(host=gateway).post(on='/', inputs=DocumentArray.empty(2)) 29 | for d in da: 30 | assert sorted_dict(d.tags) == sorted_dict({'var_a': 56.0, 'var_b': 'abcd'}) 31 | -------------------------------------------------------------------------------- /tests/integration/deployment/custom_labels/test_custom_labels.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from jcloud.deployment import CloudDeployment 4 | from jcloud.constants import Phase 5 | 6 | from tests.utils import utils 7 | 8 | deployments_dir = os.path.join( 9 | os.path.dirname(os.path.abspath(__file__)), 'deployments' 10 | ) 11 | protocol = 'grpc' 12 | executor_name = 'executor' 13 | 14 | 15 | def test_custom_labels(): 16 | labels_file = 'deployment-with-labels.yml' 17 | with CloudDeployment(path=os.path.join(deployments_dir, labels_file)) as deployment: 18 | assert utils.eventually_reaches_phase(deployment, Phase.Serving) 19 | 20 | assert deployment.endpoints != {} 21 | assert executor_name in deployment.endpoints 22 | endpoint = deployment.endpoints[executor_name] 23 | assert endpoint.startswith(f'{protocol}s://') 24 | 25 | assert utils.eventually_serve_requests(endpoint) 26 | 27 | # TODO: check that the labels are actually set by searching for them 28 | -------------------------------------------------------------------------------- /tests/integration/flow/basic/test_basic_http_new_syntax.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from jina import Client, Document, DocumentArray 4 | 5 | from jcloud.flow import CloudFlow 6 | 7 | from tests.utils import utils 8 | from .. import FlowAlive 9 | 10 | flows_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'flows') 11 | flow_file = 'http-flow-new-syntax.yml' 12 | protocol = 'http' 13 | 14 | 15 | def test_basic_http_flow_with_new_syntax(): 16 | with CloudFlow(path=os.path.join(flows_dir, flow_file)) as flow: 17 | assert flow.endpoints != {} 18 | assert 'gateway' in flow.endpoints 19 | gateway = flow.endpoints['gateway'] 20 | assert gateway.startswith(f'{protocol}s://') 21 | 22 | ltt = utils.get_last_transition_time(flow, FlowAlive) 23 | assert ltt 24 | 25 | da = Client(host=gateway).post( 26 | on='/', 27 | inputs=DocumentArray(Document(text=f'text-{i}') for i in range(50)), 28 | ) 29 | assert len(da.texts) == 50 30 | -------------------------------------------------------------------------------- /tests/integration/flow/expose/test_single_executor_stateless.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from jina import Document, DocumentArray, Flow 4 | 5 | from jcloud.flow import CloudFlow 6 | from jcloud.helper import remove_prefix 7 | 8 | flows_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'flows') 9 | flow_file = 'single-executor-stateless.yml' 10 | 11 | 12 | def test_single_executor_stateless(): 13 | with CloudFlow(path=os.path.join(flows_dir, flow_file)) as flow: 14 | assert flow.endpoints != {} 15 | assert 'sentencizer' in flow.endpoints 16 | sentencizer_host = flow.endpoints['sentencizer'] 17 | with Flow().add( 18 | host=remove_prefix(sentencizer_host, 'grpcs://'), 19 | external=True, 20 | port=443, 21 | tls=True, 22 | ) as f: 23 | da = f.post( 24 | on='/', 25 | inputs=DocumentArray(Document(text=f'text-{i}') for i in range(50)), 26 | ) 27 | assert len(da.texts) == 50 28 | -------------------------------------------------------------------------------- /tests/integration/deployment/custom_resources/test_custom_resources.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | from jina import Client, Document, DocumentArray 5 | 6 | from jcloud.deployment import CloudDeployment 7 | 8 | deployments_dir = os.path.join( 9 | os.path.dirname(os.path.abspath(__file__)), 'deployments' 10 | ) 11 | protocol = 'grpc' 12 | deployment_file = 'deployment-with-custom-resources.yml' 13 | executor_name = 'c2instance' 14 | 15 | 16 | def test_executor_resources(): 17 | with CloudDeployment( 18 | path=os.path.join(deployments_dir, deployment_file) 19 | ) as deployment: 20 | assert deployment.endpoints != {} 21 | assert executor_name in deployment.endpoints 22 | endpoint = deployment.endpoints[executor_name] 23 | assert endpoint.startswith(f'{protocol}s://') 24 | 25 | da = Client(host=endpoint).post( 26 | on='/', 27 | inputs=DocumentArray(Document(text=f'text-{i}') for i in range(50)), 28 | ) 29 | assert len(da.texts) == 50 30 | -------------------------------------------------------------------------------- /jcloud/parsers/deploy.py: -------------------------------------------------------------------------------- 1 | from .helper import _chf 2 | from ..constants import Resources 3 | 4 | 5 | def set_deploy_parser(subparser, parser_prog): 6 | if Resources.Flow in parser_prog: 7 | set_flow_deploy_parser(subparser) 8 | elif Resources.Deployment in parser_prog: 9 | set_deployment_deploy_parser(subparser) 10 | 11 | 12 | def set_flow_deploy_parser(subparser): 13 | deploy_parser = subparser.add_parser( 14 | 'deploy', 15 | help='Deploy a Flow.', 16 | formatter_class=_chf, 17 | ) 18 | 19 | deploy_parser.add_argument( 20 | 'path', 21 | type=str, 22 | help='The local path to a Jina flow project.', 23 | ) 24 | 25 | 26 | def set_deployment_deploy_parser(subparser): 27 | deploy_parser = subparser.add_parser( 28 | 'deploy', 29 | help='Deploy a Deployment.', 30 | formatter_class=_chf, 31 | ) 32 | 33 | deploy_parser.add_argument( 34 | 'path', 35 | type=str, 36 | help='The local path to a Jina deployment project.', 37 | ) 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report--flow-deployment-failed-.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report (Flow deployment failed) 3 | about: Create a report to help us improve 4 | title: 'Flow deployment failed (Request ID: ...)' 5 | labels: bug 6 | assignees: zac-li, deepankarm 7 | 8 | --- 9 | 10 | ## Flow deployment failed 11 | 12 | #### Command 13 | ```bash 14 | # Please paste here the command that failed 15 | ``` 16 | 17 | #### Flow yaml 18 | ```yaml 19 | # Paste your Flow yaml here 20 | ``` 21 | 22 | #### Flow ID / Request ID 23 | ```text 24 | Flow ID: N/A 25 | Request ID: N/A 26 | ``` 27 | 28 | 29 | --- 30 | 31 | #### `.env` file 32 | 35 | N/A 36 | 37 | 38 | #### `jina` version 39 | ```bash 40 | jina -vf 41 | ... 42 | ``` 43 | 44 | #### Logs 45 | 48 | N/A 49 | 50 | --- 51 | 52 | #### Additional context 53 | Add any other context about the problem here. 54 | -------------------------------------------------------------------------------- /tests/integration/flow/test_executors_with_shards.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from jina import Client 4 | 5 | from jcloud.flow import CloudFlow 6 | 7 | cur_dir = os.path.dirname(os.path.abspath(__file__)) 8 | 9 | 10 | def test_project_with_shards(): 11 | with CloudFlow( 12 | path=os.path.join(cur_dir, 'projects', 'executors_with_shards') 13 | ) as flow: 14 | 15 | assert flow.endpoints != {} 16 | assert 'gateway' in flow.endpoints 17 | gateway = flow.endpoints['gateway'] 18 | 19 | shard_0_counter = shard_1_counter = 0 20 | for _ in range(5): 21 | da = Client(host=gateway).post(on='/', inputs=[]) 22 | shard_id = da[0].text 23 | 24 | if shard_id == "0": 25 | shard_0_counter += 1 26 | elif shard_id == "1": 27 | shard_1_counter += 1 28 | else: 29 | assert False, "Unexpected shard encountered." 30 | 31 | # Both shard-0 and shard-1 should be used at least once. 32 | assert shard_0_counter > 0 and shard_0_counter > 0 33 | -------------------------------------------------------------------------------- /tests/integration/flow/secrets/test_secrets.py: -------------------------------------------------------------------------------- 1 | import os 2 | import asyncio 3 | 4 | from jcloud.flow import CloudFlow 5 | from jcloud.constants import Resources 6 | 7 | flows_dir = os.path.join( 8 | os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'basic', 'flows' 9 | ) 10 | flow_file = 'http-flow.yml' 11 | 12 | 13 | def test_create_secret(): 14 | with CloudFlow(path=os.path.join(flows_dir, flow_file)) as flow: 15 | 16 | secret_response = asyncio.run(flow.create_secret('mysecret', {'env1': 'value'})) 17 | assert 'name' in secret_response 18 | 19 | secret = asyncio.run(flow.get_resource(Resources.Secret, 'mysecret')) 20 | 21 | assert secret['name'] == 'mysecret' 22 | assert secret['data'] == {'env1': 'value'} 23 | 24 | asyncio.run(flow.update_secret('mysecret', {'env1': 'value2'})) 25 | 26 | secret = asyncio.run(flow.get_resource(Resources.Secret, 'mysecret')) 27 | 28 | assert secret['name'] == 'mysecret' 29 | assert secret['data'] == {'env1': 'value2'} 30 | 31 | asyncio.run(flow.delete_resource(Resources.Secret, 'mysecret')) 32 | -------------------------------------------------------------------------------- /tests/integration/flow/normalized_flow_env_vars/test_normalized_flow_env_vars.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from jina import Client, Document, DocumentArray 4 | 5 | from jcloud.flow import CloudFlow 6 | 7 | from jcloud.helper import get_dict_list_key_path 8 | 9 | cur_dir = os.path.dirname(os.path.abspath(__file__)) 10 | flow_file = 'flow.yml' 11 | protocol = 'grpc' 12 | 13 | 14 | def test_update_executor_args(): 15 | os.environ["TEST"] = "test" 16 | with CloudFlow(path=os.path.join(cur_dir, flow_file)) as flow: 17 | 18 | assert flow.endpoints != {} 19 | assert 'gateway' in flow.endpoints 20 | gateway = flow.endpoints['gateway'] 21 | assert gateway.startswith(f'{protocol}s://') 22 | 23 | status = flow._loop.run_until_complete(flow.status) 24 | assert ( 25 | get_dict_list_key_path(status, ['spec', 'executors', 0, 'env', 'TEST']) 26 | == 'test' 27 | ) 28 | 29 | da = Client(host=gateway).post( 30 | on='/', 31 | inputs=DocumentArray(Document(text=f'text-{i}') for i in range(50)), 32 | ) 33 | assert len(da.texts) == 50 34 | -------------------------------------------------------------------------------- /tests/integration/flow/envvars/test_envvars_default_file.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | from jina import Client, DocumentArray 5 | 6 | from jcloud.flow import CloudFlow 7 | from jcloud.env_helper import EnvironmentVariables 8 | from jcloud.helper import load_envs 9 | 10 | projects_dir = os.path.join( 11 | os.path.dirname(os.path.abspath(__file__)), '..', 'projects' 12 | ) 13 | protocol = 'grpc' 14 | 15 | 16 | def sorted_dict(d): 17 | return dict(sorted(d.items())) 18 | 19 | 20 | def test_envvars_default_file(): 21 | envs = load_envs(os.path.join(projects_dir, 'envvars_default_file', '.env')) 22 | with EnvironmentVariables(envs) as _: 23 | with CloudFlow(path=os.path.join(projects_dir, 'envvars_default_file')) as flow: 24 | assert flow.endpoints != {} 25 | assert 'gateway' in flow.endpoints 26 | gateway = flow.endpoints['gateway'] 27 | assert gateway.startswith(f'{protocol}s://') 28 | 29 | da = Client(host=gateway).post(on='/', inputs=DocumentArray.empty(2)) 30 | for d in da: 31 | assert sorted_dict(d.tags) == sorted_dict( 32 | {'var_a': 56.0, 'var_b': 'abcd'} 33 | ) 34 | -------------------------------------------------------------------------------- /tests/integration/flow/jobs/test_jobs.py: -------------------------------------------------------------------------------- 1 | import os 2 | import asyncio 3 | 4 | from jcloud.flow import CloudFlow 5 | from jcloud.constants import Resources 6 | 7 | flows_dir = os.path.join( 8 | os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'basic', 'flows' 9 | ) 10 | flow_file = 'http-flow.yml' 11 | 12 | 13 | def test_jobs(): 14 | with CloudFlow(path=os.path.join(flows_dir, flow_file)) as flow: 15 | 16 | job_response = asyncio.run( 17 | flow.create_job( 18 | 'test-job', 19 | 'docker://jinaai/jina:3.18-standard', 20 | ['jina', '-v'], 21 | 600, 22 | 5, 23 | ) 24 | ) 25 | assert 'name' in job_response 26 | assert 'status' in job_response 27 | 28 | job_logs = '' 29 | while len(job_logs) == 0: 30 | job_logs = asyncio.run(flow.job_logs('test-job')) 31 | assert '3.18.0' in job_logs 32 | 33 | job = asyncio.run(flow.get_resource(Resources.Job, 'test-job')) 34 | assert job['name'] == 'test-job' 35 | 36 | jobs = asyncio.run(flow.list_resources(Resources.Job)) 37 | assert len(jobs) == 1 38 | assert jobs[0]['name'] == 'test-job' 39 | 40 | asyncio.run(flow.delete_resource(Resources.Job, 'test-job')) 41 | -------------------------------------------------------------------------------- /tests/integration/flow/custom_name/test_custom_name.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | from jina import Client, Document, DocumentArray 5 | 6 | from jcloud.flow import CloudFlow 7 | 8 | flows_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'flows') 9 | protocol = 'grpc' 10 | 11 | 12 | def test_valid_custom_name(): 13 | valid_flow_file = 'valid-name.yml' 14 | valid_custom_name = 'fashion-data' 15 | 16 | with CloudFlow(path=os.path.join(flows_dir, valid_flow_file)) as flow: 17 | assert valid_custom_name in flow.flow_id 18 | assert flow.endpoints != {} 19 | assert 'gateway' in flow.endpoints 20 | gateway = flow.endpoints['gateway'] 21 | assert gateway.startswith(f'{protocol}s://') 22 | assert valid_custom_name in gateway 23 | 24 | da = Client(host=gateway).post( 25 | on='/', 26 | inputs=DocumentArray(Document(text=f'text-{i}') for i in range(50)), 27 | ) 28 | assert len(da.texts) == 50 29 | 30 | 31 | def test_invalid_custom_name(capsys): 32 | invalid_flow_file = 'invalid-name.yml' 33 | invalid_custom_name = 'abc_def#1/' 34 | 35 | with pytest.raises(SystemExit): 36 | with CloudFlow(path=os.path.join(flows_dir, invalid_flow_file)): 37 | pass 38 | 39 | captured = capsys.readouterr() 40 | assert f'invalid name {invalid_custom_name}' in captured.out 41 | -------------------------------------------------------------------------------- /jcloud/parsers/status.py: -------------------------------------------------------------------------------- 1 | from .helper import _chf 2 | from ..constants import Resources 3 | 4 | 5 | def set_status_parser(subparser, parser_prog): 6 | if Resources.Flow in parser_prog: 7 | set_flow_status_parser(subparser) 8 | elif Resources.Deployment in parser_prog: 9 | set_deployment_status_parser(subparser) 10 | 11 | 12 | def set_flow_status_parser(subparser): 13 | status_parser = subparser.add_parser( 14 | 'status', 15 | help='Get the status of a Flow.', 16 | formatter_class=_chf, 17 | ) 18 | 19 | status_parser.add_argument( 20 | 'flow', 21 | type=str, 22 | help='The string ID of a flow.', 23 | ) 24 | 25 | status_parser.add_argument( 26 | '--verbose', 27 | action='store_true', 28 | default=False, 29 | help='Pass if you want to see the full details of the Flow.', 30 | ) 31 | 32 | 33 | def set_deployment_status_parser(subparser): 34 | status_parser = subparser.add_parser( 35 | 'status', 36 | help='Get the status of a Deployment.', 37 | formatter_class=_chf, 38 | ) 39 | 40 | status_parser.add_argument( 41 | 'deployment', 42 | type=str, 43 | help='The string ID of a deployment.', 44 | ) 45 | 46 | status_parser.add_argument( 47 | '--verbose', 48 | action='store_true', 49 | default=False, 50 | help='Pass if you want to see the full details of the Deployment.', 51 | ) 52 | -------------------------------------------------------------------------------- /tests/integration/flow/expose/test_single_executor_stateful.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import numpy as np 4 | from jina import Document, Flow 5 | 6 | from jcloud.flow import CloudFlow 7 | from jcloud.helper import remove_prefix 8 | 9 | flows_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'flows') 10 | flow_file = 'single-executor-stateful.yml' 11 | 12 | 13 | def test_single_executor_stateful(): 14 | index_docs = [ 15 | Document(text=f'text-{i}', embedding=np.array([i, i + 1, i + 2])) 16 | for i in range(5) 17 | ] 18 | query_doc = index_docs[0] 19 | 20 | with CloudFlow(path=os.path.join(flows_dir, flow_file)) as flow: 21 | assert flow.endpoints != {} 22 | assert 'simpleindexer' in flow.endpoints 23 | simpleindexer_host = flow.endpoints['simpleindexer'] 24 | with Flow().add( 25 | host=remove_prefix(simpleindexer_host, 'grpcs://'), 26 | external=True, 27 | port=443, 28 | tls=True, 29 | ) as f1: 30 | da_index = f1.index(inputs=index_docs) 31 | assert da_index.texts == [f'text-{i}' for i in range(5)] 32 | for limit in [3, 5]: 33 | da_search = f1.search( 34 | inputs=query_doc, 35 | parameters={'limit': limit}, 36 | ) 37 | assert len(da_search[0].matches.texts) == limit 38 | assert da_search[0].matches.texts == [f'text-{i}' for i in range(limit)] 39 | -------------------------------------------------------------------------------- /.github/workflows/tag.yml: -------------------------------------------------------------------------------- 1 | name: Release CD 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" # push to version tags trigger the build 7 | 8 | #on: 9 | # push: 10 | # branches-ignore: 11 | # - '**' # temporally disable this action 12 | 13 | jobs: 14 | update-doc: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: benc-uk/workflow-dispatch@v1 18 | with: 19 | workflow: Manual Docs Build 20 | token: ${{ secrets.JINA_DEV_BOT }} 21 | inputs: '{ "release_token": "${{ env.release_token }}", "triggered_by": "TAG"}' 22 | env: 23 | release_token: ${{ secrets.JCLOUD_RELEASE_TOKEN }} 24 | 25 | create-release: 26 | needs: update-doc 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Checkout code 30 | uses: actions/checkout@v2 31 | with: 32 | ref: 'main' 33 | - uses: actions/setup-python@v2 34 | with: 35 | python-version: 3.7 36 | - run: | 37 | python scripts/get-last-release-note.py 38 | - name: Create Release 39 | id: create_release 40 | uses: actions/create-release@v1 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token 43 | with: 44 | tag_name: ${{ github.ref }} 45 | release_name: 💫 Patch ${{ github.ref }} 46 | body_path: 'tmp.md' 47 | draft: false 48 | prerelease: false 49 | -------------------------------------------------------------------------------- /jcloud/parsers/base.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | 4 | def set_base_parser(parser=None): 5 | import os 6 | 7 | from .. import __version__ 8 | from .helper import _chf, colored 9 | 10 | if not parser: 11 | parser = argparse.ArgumentParser( 12 | description=f'JCloud (v{colored(__version__, "green")}) deploys your Jina Flow to the cloud.', 13 | formatter_class=_chf, 14 | ) 15 | parser.add_argument( 16 | '-v', 17 | '--version', 18 | action='version', 19 | version=__version__, 20 | help='Show version.', 21 | ) 22 | parser.add_argument( 23 | '--loglevel', 24 | type=str, 25 | choices=['DEBUG', 'INFO', 'CRITICAL', 'NOTSET'], 26 | default=os.environ.get('JCLOUD_LOGLEVEL', 'INFO'), 27 | help='Set the loglevel of the logger', 28 | ) 29 | return parser 30 | 31 | 32 | def set_simple_parser(parser=None): 33 | if not parser: 34 | parser = set_base_parser() 35 | 36 | parser.add_argument( 37 | 'flow', 38 | type=str, 39 | help='The string ID of a flow.', 40 | ) 41 | return parser 42 | 43 | 44 | def set_new_project_parser(parser=None): 45 | if not parser: 46 | parser = set_base_parser() 47 | 48 | parser.add_argument( 49 | 'path', 50 | type=str, 51 | nargs='?', 52 | help='The new project will be created at this path.', 53 | default='helloworld', 54 | ) 55 | return parser 56 | -------------------------------------------------------------------------------- /scripts/docstrings_lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # required in order to get the status of all the files at once 3 | pip install darglint==1.6.0 4 | pip install pydocstyle==5.1.1 5 | echo ==================================================================================== 6 | echo DOCSTRINGS LINT: checking $CHANGED_FILES 7 | echo ------------------------------------------------------------------------------------ 8 | echo 'removing files under /tests...' 9 | arrVar=() 10 | # we ignore tests files 11 | for changed_file in $CHANGED_FILES; do 12 | case ${changed_file} in 13 | tests/* | \ 14 | .github/* | \ 15 | scripts/* | \ 16 | docarray/resources/* | \ 17 | docs/* | \ 18 | setup.py | \ 19 | fastentrypoints.py) 20 | ;;*) 21 | echo keeping ${changed_file} 22 | arrVar+=(${changed_file}) 23 | ;; 24 | esac 25 | done 26 | 27 | # if array is empty 28 | if [ ${#arrVar[@]} -eq 0 ]; then 29 | echo 'nothing to check' 30 | exit 0 31 | fi 32 | 33 | DARGLINT_OUTPUT=$(darglint -v 2 -s sphinx "${arrVar[@]}"); PYDOCSTYLE_OUTPUT=$(pydocstyle --select=D101,D102,D103 "${arrVar[@]}") 34 | # status captured here 35 | if [[ -z "$PYDOCSTYLE_OUTPUT" ]] && [[ -z "$DARGLINT_OUTPUT" ]]; then 36 | echo 'OK' 37 | exit 0 38 | else 39 | echo 'failure. make sure to check the guide for docstrings: https://docarray.jina.ai/chapters/docstring.html' 40 | echo $DARGLINT_OUTPUT 41 | echo $PYDOCSTYLE_OUTPUT 42 | exit 1 43 | fi 44 | echo ==================================================================================== 45 | -------------------------------------------------------------------------------- /tests/integration/flow/custom_actions/test_recreate.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from jcloud.flow import CloudFlow 4 | from jcloud.constants import Phase 5 | 6 | from tests.utils import utils 7 | from .. import FlowAlive 8 | 9 | flows_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'flows') 10 | flow_file = 'base_flow.yml' 11 | protocol = 'http' 12 | 13 | 14 | def test_recreate_flow(): 15 | with CloudFlow(path=os.path.join(flows_dir, flow_file)) as flow: 16 | assert utils.eventually_reaches_phase(flow, Phase.Serving) 17 | 18 | assert flow.endpoints != {} 19 | assert 'gateway' in flow.endpoints 20 | gateway = flow.endpoints['gateway'] 21 | assert gateway.startswith(f'{protocol}s://') 22 | 23 | ltt = utils.get_last_transition_time(flow, FlowAlive) 24 | assert ltt 25 | 26 | assert utils.eventually_serve_requests(gateway) 27 | 28 | # terminate the flow 29 | flow._loop.run_until_complete(flow._terminate()) 30 | assert utils.eventually_reaches_phase(flow, Phase.Deleted) 31 | 32 | # recreate the flow 33 | flow._loop.run_until_complete(flow.recreate()) 34 | assert utils.eventually_reaches_phase(flow, Phase.Serving) 35 | assert utils.eventually_condition_gets_updated(flow, FlowAlive, ltt) 36 | 37 | assert flow.endpoints != {} 38 | assert 'gateway' in flow.endpoints 39 | gateway = flow.endpoints['gateway'] 40 | assert gateway.startswith(f'{protocol}s://') 41 | 42 | assert utils.eventually_serve_requests(gateway) 43 | -------------------------------------------------------------------------------- /tests/integration/flow/custom_actions/test_scale.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from jcloud.flow import CloudFlow 4 | from jcloud.constants import Phase 5 | 6 | from tests.utils import utils 7 | from .. import FlowAlive 8 | 9 | flows_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'flows') 10 | flow_file = 'base_flow.yml' 11 | protocol = 'http' 12 | 13 | 14 | def test_scale_flow(): 15 | with CloudFlow(path=os.path.join(flows_dir, flow_file)) as flow: 16 | assert utils.eventually_reaches_phase(flow, Phase.Serving) 17 | 18 | assert flow.endpoints != {} 19 | assert 'gateway' in flow.endpoints 20 | gateway = flow.endpoints['gateway'] 21 | assert gateway.startswith(f'{protocol}s://') 22 | 23 | ltt = utils.get_last_transition_time(flow, FlowAlive) 24 | assert ltt 25 | 26 | assert utils.eventually_serve_requests(gateway) 27 | 28 | # scale executor to 2 replicas 29 | flow._loop.run_until_complete(flow.scale(executor='executor0', replicas=2)) 30 | assert utils.eventually_reaches_phase(flow, Phase.Serving) 31 | assert utils.eventually_condition_gets_updated(flow, FlowAlive, ltt) 32 | 33 | assert flow.endpoints != {} 34 | assert 'gateway' in flow.endpoints 35 | gateway = flow.endpoints['gateway'] 36 | assert gateway.startswith(f'{protocol}s://') 37 | 38 | assert utils.eventually_serve_requests(gateway) 39 | 40 | status = flow._loop.run_until_complete(flow.status) 41 | assert status['spec']['executors'][0]['replicas'] == 2 42 | -------------------------------------------------------------------------------- /tests/integration/flow/test_jina_new.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import shutil 4 | import subprocess 5 | import tempfile 6 | 7 | import pytest 8 | 9 | from pathlib import Path 10 | from venv import create 11 | from jina import Client, DocumentArray 12 | from jcloud.flow import CloudFlow 13 | 14 | cur_dir = os.path.dirname(os.path.abspath(__file__)) 15 | 16 | 17 | def setup_venv(): 18 | v_dir = Path(tempfile.mkdtemp()) 19 | create(v_dir, with_pip=True) 20 | 21 | _pip_path = v_dir / 'bin' / 'pip' 22 | subprocess.run([_pip_path, 'install', '-U', 'pip', '-q']) 23 | subprocess.run([_pip_path, 'install', 'jina[standard]==3.18.0', '-q']) 24 | return v_dir 25 | 26 | 27 | def test_jina_new_project(): 28 | v_dir = setup_venv() 29 | subprocess.run( 30 | [v_dir / 'bin' / 'jina', 'new', os.path.join(cur_dir, 'hello-world')], 31 | ) 32 | assert os.path.exists(os.path.join(cur_dir, 'hello-world')) 33 | assert os.path.isdir(os.path.join(cur_dir, 'hello-world')) 34 | 35 | with CloudFlow(path=os.path.join(cur_dir, 'hello-world')) as flow: 36 | assert flow.endpoints != {} 37 | assert 'gateway (grpc)' in flow.endpoints 38 | assert 'gateway (http)' in flow.endpoints 39 | assert 'gateway (websocket)' in flow.endpoints 40 | gateway = flow.endpoints['gateway (grpc)'] 41 | 42 | da = Client(host=gateway).post(on='/', inputs=DocumentArray.empty(2)) 43 | assert da.texts == ['hello, world!', 'goodbye, world!'] 44 | 45 | shutil.rmtree(os.path.join(cur_dir, 'hello-world')) 46 | 47 | assert not os.path.exists(os.path.join(cur_dir, 'hello-world')) 48 | -------------------------------------------------------------------------------- /tests/integration/deployment/custom_name/test_custom_name.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | from jina import Client, Document, DocumentArray 5 | 6 | from jcloud.deployment import CloudDeployment 7 | 8 | deployments_dir = os.path.join( 9 | os.path.dirname(os.path.abspath(__file__)), 'deployments' 10 | ) 11 | protocol = 'http' 12 | executor_name = 'executor' 13 | 14 | 15 | def test_valid_custom_name(): 16 | valid_deployment_file = 'valid-name-deployment.yml' 17 | valid_custom_name = 'fashion-data' 18 | 19 | with CloudDeployment( 20 | path=os.path.join(deployments_dir, valid_deployment_file) 21 | ) as deployment: 22 | assert valid_custom_name in deployment.deployment_id 23 | assert deployment.endpoints != {} 24 | assert executor_name in deployment.endpoints 25 | endpoint = deployment.endpoints[executor_name] 26 | assert endpoint.startswith(f'{protocol}s://') 27 | assert valid_custom_name in endpoint 28 | 29 | da = Client(host=endpoint).post( 30 | on='/', 31 | inputs=DocumentArray(Document(text=f'text-{i}') for i in range(50)), 32 | ) 33 | assert len(da.texts) == 50 34 | 35 | 36 | def test_invalid_custom_name(capsys): 37 | invalid_deployment_file = 'invalid-name-deployment.yml' 38 | invalid_custom_name = 'abc_def#1/' 39 | 40 | with pytest.raises(SystemExit): 41 | with CloudDeployment( 42 | path=os.path.join(deployments_dir, invalid_deployment_file) 43 | ): 44 | pass 45 | 46 | captured = capsys.readouterr() 47 | assert f'invalid name {invalid_custom_name}' in captured.out 48 | -------------------------------------------------------------------------------- /tests/integration/deployment/custom_action/test_restart.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from jcloud.deployment import CloudDeployment 4 | from jcloud.constants import Phase 5 | 6 | from tests.utils import utils 7 | from .. import DeploymentAlive 8 | 9 | 10 | deployments_dir = os.path.join( 11 | os.path.dirname(os.path.abspath(__file__)), 'deployments' 12 | ) 13 | deployment_file = 'base_deployment.yml' 14 | protocol = 'grpc' 15 | executor_name = 'executor' 16 | 17 | 18 | def test_restart_deployment(): 19 | with CloudDeployment( 20 | path=os.path.join(deployments_dir, deployment_file) 21 | ) as deployment: 22 | assert utils.eventually_reaches_phase(deployment, Phase.Serving) 23 | 24 | assert deployment.endpoints != {} 25 | assert executor_name in deployment.endpoints 26 | endpoint = deployment.endpoints[executor_name] 27 | assert endpoint.startswith(f'{protocol}s://') 28 | 29 | ltt = utils.get_last_transition_time(deployment, DeploymentAlive) 30 | assert ltt 31 | 32 | assert utils.eventually_serve_requests(endpoint) 33 | 34 | # restart the deployment 35 | deployment._loop.run_until_complete(deployment.restart()) 36 | assert utils.eventually_reaches_phase(deployment, Phase.Serving) 37 | assert utils.eventually_condition_gets_updated(deployment, DeploymentAlive, ltt) 38 | 39 | assert deployment.endpoints != {} 40 | assert executor_name in deployment.endpoints 41 | endpoint = deployment.endpoints[executor_name] 42 | assert endpoint.startswith(f'{protocol}s://') 43 | 44 | assert utils.eventually_serve_requests(endpoint) 45 | -------------------------------------------------------------------------------- /tests/integration/flow/expose/test_multiple_executors.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from jina import Client, Document, DocumentArray, Flow 4 | 5 | from jcloud.flow import CloudFlow 6 | from jcloud.helper import remove_prefix 7 | 8 | flows_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'flows') 9 | flow_file = 'gateway-and-executors.yml' 10 | 11 | 12 | def test_multiple_executors(): 13 | with CloudFlow(path=os.path.join(flows_dir, flow_file)) as flow: 14 | assert flow.endpoints != {} 15 | 16 | # Send data to the gateway 17 | assert 'gateway' in flow.endpoints 18 | gateway = flow.endpoints['gateway'] 19 | da = Client(host=gateway).post( 20 | on='/', 21 | inputs=DocumentArray(Document(text=f'text-{i}') for i in range(50)), 22 | ) 23 | assert len(da.texts) == 50 24 | 25 | assert 'sentencizer' in flow.endpoints 26 | sentencizer_host = flow.endpoints['sentencizer'] 27 | assert 'simpleindexer' in flow.endpoints 28 | simpleindexer_host = flow.endpoints['simpleindexer'] 29 | 30 | # Test local gateway with remote executors 31 | with Flow().add( 32 | host=remove_prefix(sentencizer_host, 'grpcs://'), 33 | external=True, 34 | port=443, 35 | tls=True, 36 | ).add( 37 | host=remove_prefix(simpleindexer_host, 'grpcs://'), 38 | external=True, 39 | port=443, 40 | tls=True, 41 | ) as f: 42 | da = f.post( 43 | on='/', 44 | inputs=DocumentArray(Document(text=f'text-{i}') for i in range(50)), 45 | ) 46 | assert len(da.texts) == 50 47 | -------------------------------------------------------------------------------- /tests/integration/deployment/custom_action/test_scale.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from jcloud.deployment import CloudDeployment 4 | from jcloud.constants import Phase 5 | 6 | from tests.utils import utils 7 | from .. import DeploymentAlive 8 | 9 | deployments_dir = os.path.join( 10 | os.path.dirname(os.path.abspath(__file__)), 'deployments' 11 | ) 12 | deployment_file = 'base_deployment.yml' 13 | protocol = 'grpc' 14 | executor_name = 'executor' 15 | 16 | 17 | def test_scale_deployment(): 18 | with CloudDeployment( 19 | path=os.path.join(deployments_dir, deployment_file) 20 | ) as deployment: 21 | assert utils.eventually_reaches_phase(deployment, Phase.Serving) 22 | 23 | assert deployment.endpoints != {} 24 | assert executor_name in deployment.endpoints 25 | gateway = deployment.endpoints[executor_name] 26 | assert gateway.startswith(f'{protocol}s://') 27 | 28 | ltt = utils.get_last_transition_time(deployment, DeploymentAlive) 29 | assert ltt 30 | 31 | assert utils.eventually_serve_requests(gateway) 32 | 33 | # scale executor to 2 replicas 34 | deployment._loop.run_until_complete(deployment.scale(replicas=2)) 35 | assert utils.eventually_reaches_phase(deployment, Phase.Serving) 36 | assert utils.eventually_condition_gets_updated(deployment, DeploymentAlive, ltt) 37 | 38 | assert deployment.endpoints != {} 39 | assert executor_name in deployment.endpoints 40 | gateway = deployment.endpoints[executor_name] 41 | assert gateway.startswith(f'{protocol}s://') 42 | 43 | assert utils.eventually_serve_requests(gateway) 44 | 45 | status = deployment._loop.run_until_complete(deployment.status) 46 | assert status['spec']['with']['replicas'] == 2 47 | -------------------------------------------------------------------------------- /tests/integration/flow/update/test_update_executor_image.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from jcloud.flow import CloudFlow 4 | from jcloud.constants import Phase 5 | 6 | from jcloud.helper import get_dict_list_key_path 7 | 8 | from tests.utils import utils 9 | from .. import FlowAlive 10 | 11 | flows_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'flows') 12 | flow_file = 'base_flow.yml' 13 | new_exc_image_flow_file = 'update_image.yml' 14 | protocol = 'http' 15 | 16 | 17 | def test_update_executor_image(): 18 | with CloudFlow(path=os.path.join(flows_dir, flow_file)) as flow: 19 | assert utils.eventually_reaches_phase(flow, Phase.Serving) 20 | 21 | assert flow.endpoints != {} 22 | assert 'gateway' in flow.endpoints 23 | gateway = flow.endpoints['gateway'] 24 | assert gateway.startswith(f'{protocol}s://') 25 | 26 | ltt = utils.get_last_transition_time(flow, FlowAlive) 27 | assert ltt 28 | 29 | assert utils.eventually_serve_requests(gateway) 30 | 31 | flow.path = os.path.join(flows_dir, new_exc_image_flow_file) 32 | flow._loop.run_until_complete(flow.update()) 33 | assert utils.eventually_reaches_phase(flow, Phase.Serving) 34 | assert utils.eventually_condition_gets_updated(flow, FlowAlive, ltt) 35 | 36 | assert flow.endpoints != {} 37 | assert 'gateway' in flow.endpoints 38 | gateway = flow.endpoints['gateway'] 39 | assert gateway.startswith(f'{protocol}s://') 40 | 41 | status = flow._loop.run_until_complete(flow.status) 42 | 43 | assert ( 44 | get_dict_list_key_path(status, ['spec', 'executors', 0, 'uses']) 45 | == 'jinahub+docker://SimpleIndexer' 46 | ) 47 | 48 | assert utils.eventually_serve_requests(gateway) 49 | -------------------------------------------------------------------------------- /tests/integration/flow/update/test_update_executor_args.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from jcloud.flow import CloudFlow 4 | from jcloud.constants import Phase 5 | 6 | from jcloud.helper import get_dict_list_key_path 7 | 8 | from tests.utils import utils 9 | from .. import FlowAlive 10 | 11 | flows_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'flows') 12 | flow_file = 'base_flow.yml' 13 | new_exc_args_flow_file = 'add_args.yml' 14 | protocol = 'http' 15 | 16 | 17 | def test_update_executor_args(): 18 | with CloudFlow(path=os.path.join(flows_dir, flow_file)) as flow: 19 | assert utils.eventually_reaches_phase(flow, Phase.Serving) 20 | 21 | assert flow.endpoints != {} 22 | assert 'gateway' in flow.endpoints 23 | gateway = flow.endpoints['gateway'] 24 | assert gateway.startswith(f'{protocol}s://') 25 | 26 | ltt = utils.get_last_transition_time(flow, FlowAlive) 27 | assert ltt 28 | 29 | assert utils.eventually_serve_requests(gateway) 30 | 31 | flow.path = os.path.join(flows_dir, new_exc_args_flow_file) 32 | flow._loop.run_until_complete(flow.update()) 33 | assert utils.eventually_reaches_phase(flow, Phase.Serving) 34 | assert utils.eventually_condition_gets_updated(flow, FlowAlive, ltt) 35 | 36 | assert flow.endpoints != {} 37 | assert 'gateway' in flow.endpoints 38 | gateway = flow.endpoints['gateway'] 39 | assert gateway.startswith(f'{protocol}s://') 40 | 41 | status = flow._loop.run_until_complete(flow.status) 42 | assert ( 43 | get_dict_list_key_path( 44 | status, ['spec', 'executors', 0, 'uses_with', 'punct_chars'] 45 | ) 46 | == '$PUNCT_CHARS' 47 | ) 48 | 49 | assert utils.eventually_serve_requests(gateway) 50 | -------------------------------------------------------------------------------- /tests/integration/deployment/custom_action/test_recreate.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | from jcloud.deployment import CloudDeployment 5 | from jcloud.constants import Phase 6 | 7 | from tests.utils import utils 8 | from .. import DeploymentAlive 9 | 10 | deployments_dir = os.path.join( 11 | os.path.dirname(os.path.abspath(__file__)), 'deployments' 12 | ) 13 | deployment_file = 'base_deployment.yml' 14 | protocol = 'grpc' 15 | executor_name = 'executor' 16 | 17 | 18 | def test_recreate_deployment(): 19 | with CloudDeployment( 20 | path=os.path.join(deployments_dir, deployment_file) 21 | ) as deployment: 22 | assert utils.eventually_reaches_phase(deployment, Phase.Serving) 23 | 24 | assert deployment.endpoints != {} 25 | assert executor_name in deployment.endpoints 26 | endpoint = deployment.endpoints[executor_name] 27 | assert endpoint.startswith(f'{protocol}s://') 28 | 29 | ltt = utils.get_last_transition_time(deployment, DeploymentAlive) 30 | assert ltt 31 | 32 | assert utils.eventually_serve_requests(endpoint) 33 | 34 | # terminate the deployment 35 | deployment._loop.run_until_complete(deployment._terminate()) 36 | assert utils.eventually_reaches_phase(deployment, Phase.Deleted) 37 | 38 | # recreate the deployment 39 | deployment._loop.run_until_complete(deployment.recreate()) 40 | assert utils.eventually_reaches_phase(deployment, Phase.Serving) 41 | assert utils.eventually_condition_gets_updated(deployment, DeploymentAlive, ltt) 42 | 43 | assert deployment.endpoints != {} 44 | assert executor_name in deployment.endpoints 45 | endpoint = deployment.endpoints[executor_name] 46 | assert endpoint.startswith(f'{protocol}s://') 47 | 48 | assert utils.eventually_serve_requests(endpoint) 49 | -------------------------------------------------------------------------------- /jcloud/constants.py: -------------------------------------------------------------------------------- 1 | import os 2 | from enum import Enum 3 | from pathlib import Path 4 | from typing import Dict, Optional 5 | 6 | JCLOUD_API = os.getenv('JCLOUD_API', 'https://api-v2.wolf.jina.ai/') 7 | FLOWS_API = os.path.join(JCLOUD_API, 'flows') 8 | DEPLOYMENTS_API = os.path.join(JCLOUD_API, 'deployments') 9 | JOBS_API = os.path.join(JCLOUD_API, 'jobs') 10 | SECRETS_API = os.path.join(JCLOUD_API, 'secrets') 11 | DASHBOARD_FLOW_URL_MARKDOWN = "[https://cloud.jina.ai/](https://cloud.jina.ai/user/flows?action=detail&id={flow_id}&tab=logs)" 12 | DASHBOARD_FLOW_URL_LINK = "[link=https://cloud.jina.ai/user/flows?action=detail&id={flow_id}&tab=logs]https://cloud.jina.ai/[/link]" 13 | DASHBOARD_DEPLOYMENT_URL_MARKDOWN = "[https://cloud.jina.ai/](https://cloud.jina.ai/user/deployments?action=detail&id={deployment_id}&tab=logs)" 14 | DASHBOARD_DEPLOYMENT_URL_LINK = "[link=https://cloud.jina.ai/user/deployments?action=detail&id={deployment_id}&tab=logs]https://cloud.jina.ai/[/link]" 15 | 16 | 17 | class Phase(str, Enum): 18 | Empty = '' 19 | Pending = 'Pending' 20 | Starting = 'Starting' 21 | Serving = 'Serving' 22 | Failed = 'Failed' 23 | Updating = 'Updating' 24 | Deleted = 'Deleted' 25 | Paused = 'Paused' 26 | 27 | 28 | class CONSTANTS: 29 | DEFAULT_FLOW_FILENAME = 'flow.yml' 30 | DEFAULT_ENV_FILENAME = '.env' 31 | NORMED_FLOWS_DIR = Path('/tmp/flows') 32 | 33 | 34 | class CustomAction(str, Enum): 35 | NoAction = '' 36 | Restart = 'restart' 37 | Pause = 'pause' 38 | Resume = 'resume' 39 | Scale = 'scale' 40 | Recreate = 'recreate' 41 | 42 | 43 | class Resources(str, Enum): 44 | Flow = 'flow' 45 | Job = 'job' 46 | Secret = 'secret' 47 | Deployment = 'deployment' 48 | 49 | 50 | def get_phase_from_response(response: Dict) -> Optional[Phase]: 51 | return Phase(response.get('status', {}).get('phase')) 52 | -------------------------------------------------------------------------------- /.github/workflows/force-release.yml: -------------------------------------------------------------------------------- 1 | name: Manual Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | release_token: 7 | description: 'Your release token' 8 | required: true 9 | release_reason: 10 | description: 'Short reason for this manual release' 11 | required: true 12 | 13 | jobs: 14 | token-check: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/github-script@v3 18 | with: 19 | script: | 20 | core.setFailed('token are not equivalent!') 21 | if: github.event.inputs.release_token != env.release_token 22 | env: 23 | release_token: ${{ secrets.JCLOUD_RELEASE_TOKEN }} 24 | 25 | regular-release: 26 | needs: token-check 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v2 30 | with: 31 | token: ${{ secrets.JINA_DEV_BOT }} 32 | fetch-depth: 100 # means max contribute history is limited to 100 lines 33 | # submodules: true 34 | - uses: actions/setup-python@v2 35 | with: 36 | python-version: 3.7 37 | - run: | 38 | git fetch --depth=1 origin +refs/tags/*:refs/tags/* 39 | npm install git-release-notes 40 | pip install twine wheel 41 | ./scripts/release.sh final "${{ github.event.inputs.release_reason }}" "${{github.actor}}" 42 | env: 43 | TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }} 44 | TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} 45 | JINA_SLACK_WEBHOOK: ${{ secrets.JINA_SLACK_WEBHOOK }} 46 | - if: failure() 47 | run: echo "nothing to release" 48 | - name: bumping master version 49 | uses: ad-m/github-push-action@v0.6.0 50 | with: 51 | github_token: ${{ secrets.JINA_DEV_BOT }} 52 | tags: true 53 | branch: main 54 | -------------------------------------------------------------------------------- /tests/integration/flow/update/test_update_executor_resources.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from jcloud.flow import CloudFlow 4 | from jcloud.constants import Phase 5 | 6 | from jcloud.helper import get_dict_list_key_path 7 | 8 | from tests.utils import utils 9 | from .. import FlowAlive 10 | 11 | flows_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'flows') 12 | flow_file = 'base_flow.yml' 13 | resources_flow_file = 'update_resources.yml' 14 | protocol = 'http' 15 | 16 | 17 | def test_update_executor_resources(): 18 | with CloudFlow(path=os.path.join(flows_dir, flow_file)) as flow: 19 | assert utils.eventually_reaches_phase(flow, Phase.Serving) 20 | 21 | assert flow.endpoints != {} 22 | assert 'gateway' in flow.endpoints 23 | gateway = flow.endpoints['gateway'] 24 | assert gateway.startswith(f'{protocol}s://') 25 | 26 | ltt = utils.get_last_transition_time(flow, FlowAlive) 27 | assert ltt 28 | 29 | assert utils.eventually_serve_requests(gateway) 30 | 31 | flow.path = os.path.join(flows_dir, resources_flow_file) 32 | flow._loop.run_until_complete(flow.update()) 33 | assert utils.eventually_reaches_phase(flow, Phase.Serving) 34 | assert utils.eventually_condition_gets_updated(flow, FlowAlive, ltt) 35 | 36 | assert flow.endpoints != {} 37 | assert 'gateway' in flow.endpoints 38 | gateway = flow.endpoints['gateway'] 39 | assert gateway.startswith(f'{protocol}s://') 40 | 41 | status = flow._loop.run_until_complete(flow.status) 42 | 43 | res = get_dict_list_key_path( 44 | status, ['spec', 'executors', 0, 'jcloud', 'resources'] 45 | ) 46 | assert res is not None 47 | assert 'cpu' in res and res['cpu'] == '0.2' 48 | assert 'memory' in res and res['memory'] == '200M' 49 | 50 | assert utils.eventually_serve_requests(gateway) 51 | -------------------------------------------------------------------------------- /jcloud/parsers/normalize.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from .helper import _chf 3 | from ..constants import Resources 4 | 5 | 6 | def set_normalize_parser(subparser, parser_prog): 7 | if Resources.Flow in parser_prog: 8 | set_flow_normalize_parser(subparser) 9 | # elif Resources.Deployment in parser_prog: 10 | # set_deployment_normalize_parser(subparser) 11 | 12 | 13 | def set_flow_normalize_parser(subparser): 14 | normalize_parser = subparser.add_parser( 15 | 'normalize', 16 | help='Normalize a Flow.', 17 | formatter_class=_chf, 18 | ) 19 | normalize_parser.add_argument( 20 | 'path', 21 | type=Path, 22 | help='The local path to a Jina Flow project directory or yml file.', 23 | ) 24 | normalize_parser.add_argument( 25 | '-o', 26 | '--output', 27 | type=Path, 28 | help='The output path to the normalized Jina Flow yml file.', 29 | ) 30 | normalize_parser.add_argument( 31 | '-v', 32 | '--verbose', 33 | action='store_true', 34 | help='Increase verbosity.', 35 | ) 36 | normalize_parser.set_defaults(verbose=False) 37 | 38 | 39 | def set_deployment_normalize_parser(subparser): 40 | normalize_parser = subparser.add_parser( 41 | 'normalize', 42 | help='Normalize a Deployment.', 43 | formatter_class=_chf, 44 | ) 45 | normalize_parser.add_argument( 46 | 'path', 47 | type=Path, 48 | help='The local path to a Jina Deployment project directory or yml file.', 49 | ) 50 | normalize_parser.add_argument( 51 | '-o', 52 | '--output', 53 | type=Path, 54 | help='The output path to the normalized Jina Deployment yml file.', 55 | ) 56 | normalize_parser.add_argument( 57 | '-v', 58 | '--verbose', 59 | action='store_true', 60 | help='Increase verbosity.', 61 | ) 62 | normalize_parser.set_defaults(verbose=False) 63 | -------------------------------------------------------------------------------- /tests/integration/deployment/update/test_rename_executor.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from jcloud.deployment import CloudDeployment 4 | 5 | from jcloud.helper import get_dict_list_key_path 6 | from jcloud.constants import Phase 7 | 8 | from tests.utils import utils 9 | from .. import DeploymentAlive 10 | 11 | deployments_dir = os.path.join( 12 | os.path.dirname(os.path.abspath(__file__)), 'deployments' 13 | ) 14 | deployment_file = 'base_deployment.yml' 15 | rename_executor_deployment_file = 'rename_executor.yml' 16 | protocol = 'grpc' 17 | executor_name = 'executor' 18 | 19 | 20 | def test_rename_executor(): 21 | with CloudDeployment( 22 | path=os.path.join(deployments_dir, deployment_file) 23 | ) as deployment: 24 | assert utils.eventually_reaches_phase(deployment, Phase.Serving) 25 | 26 | assert deployment.endpoints != {} 27 | assert executor_name in deployment.endpoints 28 | endpoint = deployment.endpoints[executor_name] 29 | assert endpoint.startswith(f'{protocol}s://') 30 | 31 | ltt = utils.get_last_transition_time(deployment, DeploymentAlive) 32 | assert ltt 33 | assert utils.eventually_serve_requests(endpoint) 34 | 35 | deployment.path = os.path.join(deployments_dir, rename_executor_deployment_file) 36 | deployment._loop.run_until_complete(deployment.update()) 37 | assert utils.eventually_reaches_phase(deployment, Phase.Serving) 38 | assert utils.eventually_condition_gets_updated(deployment, DeploymentAlive, ltt) 39 | 40 | new_name = 'newsentencizer' 41 | assert deployment.endpoints != {} 42 | assert new_name in deployment.endpoints 43 | endpoint = deployment.endpoints[new_name] 44 | assert endpoint.startswith(f'{protocol}s://') 45 | 46 | status = deployment._loop.run_until_complete(deployment.status) 47 | 48 | assert get_dict_list_key_path(status, ['spec', 'with', 'name']) == new_name 49 | 50 | assert utils.eventually_serve_requests(endpoint) 51 | -------------------------------------------------------------------------------- /jcloud/parsers/logs.py: -------------------------------------------------------------------------------- 1 | from .helper import _chf 2 | from ..constants import Resources 3 | 4 | 5 | def set_logs_resource_parser(subparser, parser_prog): 6 | 7 | if Resources.Flow in parser_prog: 8 | logs_parser = subparser.add_parser( 9 | 'logs', 10 | help='Get logs of a Flow gateway or executor.', 11 | formatter_class=_chf, 12 | ) 13 | _set_logs_flow_parser(logs_parser) 14 | elif Resources.Deployment in parser_prog: 15 | logs_parser = subparser.add_parser( 16 | 'logs', 17 | help='Get logs of a Deployment.', 18 | formatter_class=_chf, 19 | ) 20 | _set_logs_deployment_parser(logs_parser) 21 | else: 22 | logs_parser = subparser.add_parser( 23 | 'logs', 24 | help='Get logs of a Job.', 25 | formatter_class=_chf, 26 | ) 27 | _set_logs_job_parser(logs_parser) 28 | 29 | 30 | def _set_logs_flow_parser(logs_parser): 31 | logs_parser.add_argument( 32 | 'flow', 33 | type=str, 34 | help='The string ID of a Flow.', 35 | ) 36 | 37 | group = logs_parser.add_mutually_exclusive_group(required=True) 38 | 39 | group.add_argument( 40 | '--gateway', 41 | action='store_true', 42 | required=False, 43 | help='Get logs for gateway.', 44 | ) 45 | group.add_argument( 46 | '--executor', 47 | type=str, 48 | required=False, 49 | help='Get logs for executor.', 50 | ) 51 | 52 | 53 | def _set_logs_job_parser(logs_parser): 54 | logs_parser.add_argument( 55 | 'name', 56 | type=str, 57 | help='The name of the Job.', 58 | ) 59 | 60 | logs_parser.add_argument( 61 | 'flow', 62 | type=str, 63 | help='The string ID of a Flow.', 64 | ) 65 | 66 | 67 | def _set_logs_deployment_parser(logs_parser): 68 | logs_parser.add_argument( 69 | 'deployment', 70 | type=str, 71 | help='The string ID of a Deployment.', 72 | ) 73 | -------------------------------------------------------------------------------- /tests/integration/deployment/update/test_update_executor_args.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from jcloud.deployment import CloudDeployment 4 | from jcloud.constants import Phase 5 | 6 | from jcloud.helper import get_dict_list_key_path 7 | 8 | from tests.utils import utils 9 | from .. import DeploymentAlive 10 | 11 | deployments_dir = os.path.join( 12 | os.path.dirname(os.path.abspath(__file__)), 'deployments' 13 | ) 14 | deployment_file = 'base_deployment.yml' 15 | new_exc_args_deployment_file = 'add_args.yml' 16 | protocol = 'grpc' 17 | executor_name = 'executor' 18 | 19 | 20 | def test_update_executor_args(): 21 | with CloudDeployment( 22 | path=os.path.join(deployments_dir, deployment_file) 23 | ) as deployment: 24 | assert utils.eventually_reaches_phase(deployment, Phase.Serving) 25 | 26 | assert deployment.endpoints != {} 27 | assert executor_name in deployment.endpoints 28 | endpoint = deployment.endpoints[executor_name] 29 | assert endpoint.startswith(f'{protocol}s://') 30 | 31 | ltt = utils.get_last_transition_time(deployment, DeploymentAlive) 32 | assert ltt 33 | 34 | assert utils.eventually_serve_requests(endpoint) 35 | 36 | deployment.path = os.path.join(deployments_dir, new_exc_args_deployment_file) 37 | deployment._loop.run_until_complete(deployment.update()) 38 | assert utils.eventually_reaches_phase(deployment, Phase.Serving) 39 | assert utils.eventually_condition_gets_updated(deployment, DeploymentAlive, ltt) 40 | 41 | assert deployment.endpoints != {} 42 | assert executor_name in deployment.endpoints 43 | endpoint = deployment.endpoints[executor_name] 44 | assert endpoint.startswith(f'{protocol}s://') 45 | 46 | status = deployment._loop.run_until_complete(deployment.status) 47 | assert ( 48 | get_dict_list_key_path(status, ['spec', 'with', 'uses_with', 'punct_chars']) 49 | == '$PUNCT_CHARS' 50 | ) 51 | 52 | assert utils.eventually_serve_requests(endpoint) 53 | -------------------------------------------------------------------------------- /tests/integration/deployment/update/test_update_executor_image.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from jcloud.deployment import CloudDeployment 4 | from jcloud.constants import Phase 5 | 6 | from jcloud.helper import get_dict_list_key_path 7 | 8 | from tests.utils import utils 9 | from .. import DeploymentAlive 10 | 11 | 12 | deployments_dir = os.path.join( 13 | os.path.dirname(os.path.abspath(__file__)), 'deployments' 14 | ) 15 | deployment_file = 'base_deployment.yml' 16 | new_exc_image_deployment_file = 'update_image.yml' 17 | protocol = 'grpc' 18 | executor_name = 'executor' 19 | 20 | 21 | def test_update_executor_image(): 22 | with CloudDeployment( 23 | path=os.path.join(deployments_dir, deployment_file) 24 | ) as deployment: 25 | assert utils.eventually_reaches_phase(deployment, Phase.Serving) 26 | 27 | assert deployment.endpoints != {} 28 | assert executor_name in deployment.endpoints 29 | endpoint = deployment.endpoints[executor_name] 30 | assert endpoint.startswith(f'{protocol}s://') 31 | 32 | ltt = utils.get_last_transition_time(deployment, DeploymentAlive) 33 | assert ltt 34 | 35 | assert utils.eventually_serve_requests(endpoint) 36 | 37 | deployment.path = os.path.join(deployments_dir, new_exc_image_deployment_file) 38 | deployment._loop.run_until_complete(deployment.update()) 39 | assert utils.eventually_reaches_phase(deployment, Phase.Serving) 40 | assert utils.eventually_condition_gets_updated(deployment, DeploymentAlive, ltt) 41 | 42 | assert deployment.endpoints != {} 43 | assert executor_name in deployment.endpoints 44 | endpoint = deployment.endpoints[executor_name] 45 | assert endpoint.startswith(f'{protocol}s://') 46 | 47 | status = deployment._loop.run_until_complete(deployment.status) 48 | 49 | assert ( 50 | get_dict_list_key_path(status, ['spec', 'with', 'uses']) 51 | == 'jinahub+docker://SimpleIndexer' 52 | ) 53 | 54 | assert utils.eventually_serve_requests(endpoint) 55 | -------------------------------------------------------------------------------- /tests/integration/deployment/update/test_update_executor_resources.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from jcloud.deployment import CloudDeployment 4 | from jcloud.constants import Phase 5 | 6 | from jcloud.helper import get_dict_list_key_path 7 | 8 | from tests.utils import utils 9 | from .. import DeploymentAlive 10 | 11 | deployments_dir = os.path.join( 12 | os.path.dirname(os.path.abspath(__file__)), 'deployments' 13 | ) 14 | deployment_file = 'base_deployment.yml' 15 | resources_deployment_file = 'update_resources.yml' 16 | protocol = 'grpc' 17 | executor_name = 'executor' 18 | 19 | 20 | def test_update_executor_resources(): 21 | with CloudDeployment( 22 | path=os.path.join(deployments_dir, deployment_file) 23 | ) as deployment: 24 | assert utils.eventually_reaches_phase(deployment, Phase.Serving) 25 | 26 | assert deployment.endpoints != {} 27 | assert executor_name in deployment.endpoints 28 | endpoint = deployment.endpoints[executor_name] 29 | assert endpoint.startswith(f'{protocol}s://') 30 | 31 | ltt = utils.get_last_transition_time(deployment, DeploymentAlive) 32 | assert ltt 33 | 34 | assert utils.eventually_serve_requests(endpoint) 35 | 36 | deployment.path = os.path.join(deployments_dir, resources_deployment_file) 37 | deployment._loop.run_until_complete(deployment.update()) 38 | assert utils.eventually_reaches_phase(deployment, Phase.Serving) 39 | assert utils.eventually_condition_gets_updated(deployment, DeploymentAlive, ltt) 40 | 41 | assert deployment.endpoints != {} 42 | assert executor_name in deployment.endpoints 43 | endpoint = deployment.endpoints[executor_name] 44 | assert endpoint.startswith(f'{protocol}s://') 45 | 46 | status = deployment._loop.run_until_complete(deployment.status) 47 | 48 | res = get_dict_list_key_path(status, ['spec', 'jcloud', 'resources']) 49 | assert res is not None 50 | assert 'cpu' in res and res['cpu'] == '0.5' 51 | assert 'memory' in res and res['memory'] == '1G' 52 | 53 | assert utils.eventually_serve_requests(endpoint) 54 | -------------------------------------------------------------------------------- /.github/workflows/force-docs-build.yml: -------------------------------------------------------------------------------- 1 | name: Manual Docs Build 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | release_token: 7 | description: 'Your release token' 8 | required: true 9 | triggered_by: 10 | description: 'CD | TAG | MANUAL' 11 | required: false 12 | default: MANUAL 13 | 14 | jobs: 15 | token-check: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/github-script@v3 19 | with: 20 | script: | 21 | core.setFailed('token are not equivalent!') 22 | if: github.event.inputs.release_token != env.release_token 23 | env: 24 | release_token: ${{ secrets.JCLOUD_RELEASE_TOKEN }} 25 | 26 | release-docs: 27 | needs: token-check 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@v2 31 | with: 32 | fetch-depth: 0 33 | - uses: actions/setup-python@v2 34 | with: 35 | python-version: 3.7 36 | - name: Build doc and push to gh-pages 37 | run: | 38 | git config --local user.email "dev-bot@jina.ai" 39 | git config --local user.name "Jina Dev Bot" 40 | pip install . 41 | mkdir gen-html 42 | cd docs 43 | pip install -r requirements.txt 44 | pip install --pre -U furo 45 | bash makedoc.sh 46 | cd ./_build/dirhtml/ 47 | cp -r ./ ../../../gen-html 48 | cd - # back to ./docs 49 | cd .. 50 | git checkout -f gh-pages 51 | git rm -rf ./docs 52 | mkdir -p docs 53 | cd gen-html 54 | cp -r ./ ../docs 55 | cd ../docs 56 | ls -la 57 | touch .nojekyll 58 | cp 404/index.html 404.html 59 | sed -i 's/href="\.\./href="/' 404.html # fix asset urls that needs to be updated in 404.html 60 | echo jcloud.jina.ai > CNAME 61 | cd .. 62 | git add docs 63 | git status 64 | git commit -m "chore(docs): update docs due to ${{github.event_name}} on ${{github.repository}}" 65 | git push --force origin gh-pages -------------------------------------------------------------------------------- /tests/integration/flow/stateful/test_stateful_flow_grpc.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import numpy as np 4 | import pytest 5 | from jina import Client, Document 6 | 7 | from jcloud.flow import CloudFlow 8 | 9 | flows_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'flows') 10 | 11 | 12 | @pytest.mark.skip('unskip once sharing workspace is implemented') 13 | def test_crud_stateful_flow_grpc(): 14 | # This tests 15 | # Index Flow stores data on disk -> terminated 16 | # Query Flow accesses same data using Index Flows workspace to `/search` 17 | protocol = 'grpc' 18 | INDEX_FLOW_NAME = f'simpleindexer-{protocol}-index' 19 | SEARCH_FLOW_NAME = F'simpleindexer-{protocol}-search' 20 | FLOW_FILE_PATH = os.path.join(flows_dir, f'{protocol}-stateful.yml') 21 | 22 | index_docs = [ 23 | Document(text=f'text-{i}', embedding=np.array([i, i + 1, i + 2])) 24 | for i in range(5) 25 | ] 26 | query_doc = index_docs[0] 27 | 28 | with CloudFlow(path=FLOW_FILE_PATH, name=INDEX_FLOW_NAME) as index_flow: 29 | da_index = Client(host=index_flow.gateway).index(inputs=index_docs) 30 | assert da_index.texts == [f'text-{i}' for i in range(5)] 31 | for limit in [3, 5]: 32 | da_search = Client(host=index_flow.gateway).search( 33 | inputs=query_doc, parameters={'limit': limit} 34 | ) 35 | assert len(da_search[0].matches.texts) == limit 36 | assert da_search[0].matches.texts == [f'text-{i}' for i in range(limit)] 37 | 38 | with CloudFlow( 39 | path=FLOW_FILE_PATH, name=SEARCH_FLOW_NAME, workspace_id=index_flow.workspace_id 40 | ) as search_flow: 41 | da_search = Client(host=search_flow.gateway).search(inputs=query_doc) 42 | assert da_search[0].matches.texts == [f'text-{i}' for i in range(5)] 43 | for limit in [3, 5]: 44 | da_search = Client(host=search_flow.gateway).search( 45 | inputs=query_doc, parameters={'limit': limit} 46 | ) 47 | assert len(da_search[0].matches.texts) == limit 48 | assert da_search[0].matches.texts == [f'text-{i}' for i in range(limit)] 49 | -------------------------------------------------------------------------------- /tests/integration/flow/stateful/test_stateful_flow_http.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import numpy as np 4 | import pytest 5 | from jina import Client, Document 6 | 7 | from jcloud.flow import CloudFlow 8 | 9 | flows_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'flows') 10 | 11 | 12 | @pytest.mark.skip('unskip once sharing workspace is implemented') 13 | def test_crud_stateful_flow_http(): 14 | # This tests 15 | # Index Flow stores data on disk -> terminated 16 | # Query Flow accesses same data using Index Flows workspace to `/search` 17 | protocol = 'http' 18 | INDEX_FLOW_NAME = f'simpleindexer-{protocol}-index' 19 | SEARCH_FLOW_NAME = F'simpleindexer-{protocol}-search' 20 | FLOW_FILE_PATH = os.path.join(flows_dir, f'{protocol}-stateful.yml') 21 | 22 | index_docs = [ 23 | Document(text=f'text-{i}', embedding=np.array([i, i + 1, i + 2])) 24 | for i in range(5) 25 | ] 26 | query_doc = index_docs[0] 27 | 28 | with CloudFlow(path=FLOW_FILE_PATH, name=INDEX_FLOW_NAME) as index_flow: 29 | da_index = Client(host=index_flow.gateway).index(inputs=index_docs) 30 | assert da_index.texts == [f'text-{i}' for i in range(5)] 31 | for limit in [3, 5]: 32 | da_search = Client(host=index_flow.gateway).search( 33 | inputs=query_doc, parameters={'limit': limit} 34 | ) 35 | assert len(da_search[0].matches.texts) == limit 36 | assert da_search[0].matches.texts == [f'text-{i}' for i in range(limit)] 37 | 38 | with CloudFlow( 39 | path=FLOW_FILE_PATH, name=SEARCH_FLOW_NAME, workspace_id=index_flow.workspace_id 40 | ) as search_flow: 41 | da_search = Client(host=search_flow.gateway).search(inputs=query_doc) 42 | assert da_search[0].matches.texts == [f'text-{i}' for i in range(5)] 43 | for limit in [3, 5]: 44 | da_search = Client(host=search_flow.gateway).search( 45 | inputs=query_doc, parameters={'limit': limit} 46 | ) 47 | assert len(da_search[0].matches.texts) == limit 48 | assert da_search[0].matches.texts == [f'text-{i}' for i in range(limit)] 49 | -------------------------------------------------------------------------------- /tests/integration/flow/stateful/test_stateful_flow_websocket.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import numpy as np 4 | import pytest 5 | from jina import Client, Document 6 | 7 | from jcloud.flow import CloudFlow 8 | 9 | flows_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'flows') 10 | 11 | 12 | @pytest.mark.skip('unskip once sharing workspace is implemented') 13 | def test_crud_stateful_flow_websocket(): 14 | # This tests 15 | # Index Flow stores data on disk -> terminated 16 | # Query Flow accesses same data using Index Flows workspace to `/search` 17 | protocol = 'websocket' 18 | INDEX_FLOW_NAME = f'simpleindexer-{protocol}-index' 19 | SEARCH_FLOW_NAME = F'simpleindexer-{protocol}-search' 20 | FLOW_FILE_PATH = os.path.join(flows_dir, f'{protocol}-stateful.yml') 21 | 22 | index_docs = [ 23 | Document(text=f'text-{i}', embedding=np.array([i, i + 1, i + 2])) 24 | for i in range(5) 25 | ] 26 | query_doc = index_docs[0] 27 | 28 | with CloudFlow(path=FLOW_FILE_PATH, name=INDEX_FLOW_NAME) as index_flow: 29 | da_index = Client(host=index_flow.gateway).index(inputs=index_docs) 30 | assert da_index.texts == [f'text-{i}' for i in range(5)] 31 | for limit in [3, 5]: 32 | da_search = Client(host=index_flow.gateway).search( 33 | inputs=query_doc, parameters={'limit': limit} 34 | ) 35 | assert len(da_search[0].matches.texts) == limit 36 | assert da_search[0].matches.texts == [f'text-{i}' for i in range(limit)] 37 | 38 | with CloudFlow( 39 | path=FLOW_FILE_PATH, name=SEARCH_FLOW_NAME, workspace_id=index_flow.workspace_id 40 | ) as search_flow: 41 | da_search = Client(host=search_flow.gateway).search(inputs=query_doc) 42 | assert da_search[0].matches.texts == [f'text-{i}' for i in range(5)] 43 | for limit in [3, 5]: 44 | da_search = Client(host=search_flow.gateway).search( 45 | inputs=query_doc, parameters={'limit': limit} 46 | ) 47 | assert len(da_search[0].matches.texts) == limit 48 | assert da_search[0].matches.texts == [f'text-{i}' for i in range(limit)] 49 | -------------------------------------------------------------------------------- /tests/integration/flow/custom_actions/test_pause_resume.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | from jina import Client, Document, DocumentArray 6 | 7 | from jcloud.flow import CloudFlow 8 | from jcloud.constants import Phase 9 | 10 | from tests.utils import utils 11 | from .. import FlowAlive 12 | 13 | flows_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'flows') 14 | flow_file = 'base_flow.yml' 15 | protocol = 'http' 16 | 17 | 18 | def test_pause_resume_flow(): 19 | with CloudFlow(path=os.path.join(flows_dir, flow_file)) as flow: 20 | assert utils.eventually_reaches_phase(flow, Phase.Serving) 21 | 22 | assert flow.endpoints != {} 23 | assert 'gateway' in flow.endpoints 24 | gateway = flow.endpoints['gateway'] 25 | assert gateway.startswith(f'{protocol}s://') 26 | 27 | ltt = utils.get_last_transition_time(flow, FlowAlive) 28 | assert ltt 29 | 30 | assert utils.eventually_serve_requests(gateway) 31 | 32 | # pause the flow 33 | flow._loop.run_until_complete(flow.pause()) 34 | assert utils.eventually_reaches_phase(flow, Phase.Paused) 35 | assert utils.eventually_condition_gets_updated(flow, FlowAlive, ltt) 36 | 37 | assert flow.endpoints != {} 38 | assert 'gateway' in flow.endpoints 39 | gateway = flow.endpoints['gateway'] 40 | assert gateway.startswith(f'{protocol}s://') 41 | 42 | with pytest.raises(ValueError): 43 | da = Client(host=gateway).post( 44 | on='/', 45 | inputs=DocumentArray(Document(text=f'text-{i}') for i in range(50)), 46 | ) 47 | 48 | # resume the flow 49 | ltt = utils.get_last_transition_time(flow, FlowAlive) 50 | assert ltt 51 | 52 | flow._loop.run_until_complete(flow.resume()) 53 | assert utils.eventually_reaches_phase(flow, Phase.Serving) 54 | assert utils.eventually_condition_gets_updated(flow, FlowAlive, ltt) 55 | 56 | assert flow.endpoints != {} 57 | assert 'gateway' in flow.endpoints 58 | gateway = flow.endpoints['gateway'] 59 | assert gateway.startswith(f'{protocol}s://') 60 | 61 | assert utils.eventually_serve_requests(gateway) 62 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | prep-testbed: 10 | if: | 11 | !startsWith(github.event.head_commit.message, 'chore') && 12 | !startsWith(github.event.head_commit.message, 'build: hotfix') && 13 | !endsWith(github.event.head_commit.message, 'reformatted by jina-dev-bot') 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - id: set-matrix 18 | run: | 19 | sudo apt-get install jq 20 | echo "::set-output name=matrix::$(bash scripts/get-all-test-paths.sh unit)" 21 | outputs: 22 | matrix: ${{ steps.set-matrix.outputs.matrix }} 23 | 24 | core-test: 25 | needs: prep-testbed 26 | runs-on: ubuntu-latest 27 | strategy: 28 | fail-fast: false 29 | matrix: 30 | python-version: [3.7] 31 | test-path: ${{fromJson(needs.prep-testbed.outputs.matrix)}} 32 | steps: 33 | - uses: actions/checkout@v2 34 | - name: Set up Python ${{ matrix.python-version }} 35 | uses: actions/setup-python@v2 36 | with: 37 | python-version: ${{ matrix.python-version }} 38 | - name: Prepare environment 39 | run: | 40 | python -m pip install --upgrade pip 41 | python -m pip install wheel 42 | pip install --no-cache-dir ".[full,test]" 43 | sudo apt-get install libsndfile1 44 | - name: Test 45 | id: test 46 | env: 47 | JINA_AUTH_TOKEN: ${{ secrets.CI_TOKEN }} 48 | run: | 49 | pytest --suppress-no-test-exit-code --cov=jcloud --cov-report=xml \ 50 | -v -s --log-cli-level=DEBUG -m "not gpu" ${{ matrix.test-path }} 51 | echo "::set-output name=codecov_flag::jcloud" 52 | timeout-minutes: 30 53 | 54 | prerelease: 55 | needs: core-test 56 | runs-on: ubuntu-latest 57 | steps: 58 | - uses: actions/checkout@v2 59 | with: 60 | fetch-depth: 100 61 | - name: Pre-release (.devN) 62 | run: | 63 | git fetch --depth=1 origin +refs/tags/*:refs/tags/* 64 | pip install twine wheel 65 | ./scripts/release.sh 66 | env: 67 | TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }} 68 | TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} 69 | JINA_SLACK_WEBHOOK: ${{ secrets.JINA_SLACK_WEBHOOK }} 70 | -------------------------------------------------------------------------------- /jcloud/parsers/create.py: -------------------------------------------------------------------------------- 1 | import ast 2 | 3 | from pathlib import Path 4 | 5 | from .helper import _chf 6 | from ..constants import Resources 7 | 8 | 9 | def set_create_resource_parser(subparser, resource): 10 | 11 | create_parser = subparser.add_parser( 12 | 'create', 13 | help=f'Create a {resource.title()}.', 14 | formatter_class=_chf, 15 | ) 16 | 17 | create_parser.add_argument( 18 | 'name', 19 | type=str, 20 | help=f'The name of the {resource.title()}.', 21 | ) 22 | 23 | create_parser.add_argument( 24 | 'flow', 25 | type=str, 26 | help='The string ID of the Flow.', 27 | ) 28 | if resource == Resources.Job: 29 | _set_job_create_parser(create_parser) 30 | else: 31 | _set_secret_create_parser(create_parser) 32 | 33 | 34 | def _set_job_create_parser(create_parser): 35 | create_parser.add_argument( 36 | 'image', 37 | type=str, 38 | help='The image the Job will use.', 39 | ) 40 | 41 | create_parser.add_argument( 42 | 'entrypoint', 43 | type=ast.literal_eval, 44 | help='The command to be added to the image\'s entrypoint.', 45 | ) 46 | 47 | create_parser.add_argument( 48 | '--timeout', 49 | type=int, 50 | default=600, 51 | help='Duration the Job will be active before termination in seconds.', 52 | ) 53 | 54 | create_parser.add_argument( 55 | '--backofflimit', 56 | type=int, 57 | help='Number of retries before Job is marked as failed.', 58 | ) 59 | 60 | create_parser.add_argument( 61 | '--secrets', 62 | type=ast.literal_eval, 63 | help='Literal value following the format "{\'ENV_VAR\': {\'name\': \'secret-name\', \'key\': \'secret-key\'}}"', 64 | ) 65 | 66 | 67 | def _set_secret_create_parser(create_parser): 68 | create_parser.add_argument( 69 | '--from-literal', 70 | type=ast.literal_eval, 71 | help='Literal Secret value. Should follow the format "{\'env1\':\'value\'},\'env2\':\'value2\'}".', 72 | ) 73 | 74 | create_parser.add_argument( 75 | '--update', 76 | action='store_true', 77 | help='Whether to update the flow spec after create the Secret', 78 | ) 79 | 80 | create_parser.add_argument( 81 | '--path', 82 | type=Path, 83 | help='The path of flow yaml spec file', 84 | ) 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 |
4 |
5 | JCloud logo: the command line interface that simplifies deploying and managing Jina projects on Jina Cloud 6 |
7 |
8 |
9 | Simplify deploying and hosting Jina projects on Jina Cloud 10 |

11 | 12 |

13 | PyPI 14 | 15 |

16 | 17 | ☁️ **To the cloud!** - Smoothly deploy a local project as a cloud service. Radically easy, no nasty surprises. 18 | 19 | 🎯 **Cut to the chase** - One CLI with five commands to manage the lifecycle of your Jina projects. 20 | 21 | 🎟️ **Early free access** - Sneak peek at our stealthy cloud hosting platform. Built on latest cloud-native tech stack, we now host your Jina project and offer computational and storage resources, for free! 22 | 23 | ## Install 24 | 25 | ```bash 26 | pip install jcloud 27 | jc -h 28 | ``` 29 | 30 | In case `jc` is already occupied by another tool, please use `jcloud` instead. If your pip install doesn't register bash commands for you, you can run `python -m jcloud -h`. 31 | 32 | ## [Documentation](https://docs.jina.ai/concepts/jcloud/) 33 | 34 | ## Support 35 | 36 | - Join our [Slack community](https://slack.jina.ai) and chat with other community members about ideas. 37 | - Join our [Engineering All Hands](https://youtube.com/playlist?list=PL3UBBWOUVhFYRUa_gpYYKBqEAkO4sxmne) meet-up to discuss your use case and learn Jina's new features. 38 | - **When?** The second Tuesday of every month 39 | - **Where?** 40 | Zoom ([see our public events calendar](https://calendar.google.com/calendar/embed?src=c_1t5ogfp2d45v8fit981j08mcm4%40group.calendar.google.com&ctz=Europe%2FBerlin)/[.ical](https://calendar.google.com/calendar/ical/c_1t5ogfp2d45v8fit981j08mcm4%40group.calendar.google.com/public/basic.ics)) 41 | and [live stream on YouTube](https://youtube.com/c/jina-ai) 42 | - Subscribe to the latest video tutorials on our [YouTube channel](https://youtube.com/c/jina-ai) 43 | 44 | ## Join Us 45 | 46 | JCloud is backed by [Jina AI](https://jina.ai) and licensed under [Apache-2.0](./LICENSE). [We are actively hiring](https://jobs.jina.ai) AI engineers, solution engineers to build the next neural search ecosystem in open-source. 47 | -------------------------------------------------------------------------------- /tests/utils/utils.py: -------------------------------------------------------------------------------- 1 | from jina import Client, Document, DocumentArray 2 | 3 | from jcloud.flow import CloudFlow 4 | from jcloud.deployment import CloudDeployment 5 | from jcloud.constants import Phase 6 | from ..integration.flow import FlowAlive 7 | 8 | import time 9 | 10 | 11 | def get_condition_from_status(status, cond_type=FlowAlive): 12 | try: 13 | sts = status["status"] 14 | conds = sts["conditions"] 15 | for c in conds: 16 | if c["type"] == cond_type: 17 | return c 18 | except KeyError: 19 | return None 20 | 21 | 22 | def get_last_transition_time( 23 | res: CloudFlow | CloudDeployment, 24 | condition_type, 25 | ) -> str: 26 | try: 27 | status = res._loop.run_until_complete(res.status) 28 | cnd = get_condition_from_status(status, condition_type) 29 | if cnd: 30 | return cnd['lastTransitionTime'] 31 | except KeyError: 32 | pass 33 | 34 | return '' 35 | 36 | 37 | def eventually_reaches_phase( 38 | res: CloudFlow | CloudDeployment, 39 | phase=Phase.Serving, 40 | num_of_retries=12, 41 | interval=30, 42 | ) -> bool: 43 | 44 | for i in range(num_of_retries): 45 | time.sleep(interval) 46 | status = res._loop.run_until_complete(res.status) 47 | try: 48 | sts = status['status'] 49 | p = sts['phase'] 50 | if p == phase: 51 | return True 52 | except KeyError: 53 | pass 54 | 55 | return False 56 | 57 | 58 | def eventually_condition_gets_updated( 59 | res: CloudFlow | CloudDeployment, 60 | condition_type, 61 | condition_timestamp, 62 | num_of_retries=6, 63 | interval=4, 64 | ) -> bool: 65 | for i in range(num_of_retries): 66 | nltt = get_last_transition_time(res, condition_type) 67 | if nltt > condition_timestamp: 68 | return True 69 | 70 | time.sleep(interval) 71 | 72 | return False 73 | 74 | 75 | def eventually_serve_requests( 76 | endpoint: str, 77 | num_of_retries=12, 78 | interval=3, 79 | ) -> bool: 80 | for _ in range(num_of_retries): 81 | time.sleep(interval) 82 | try: 83 | da = Client(host=endpoint).post( 84 | on='/', 85 | inputs=DocumentArray(Document(text=f'text-{i}') for i in range(50)), 86 | ) 87 | if len(da.texts) == 50: 88 | return True 89 | except (ValueError, ConnectionError) as e: 90 | print(e, ". retrying") 91 | 92 | return False 93 | -------------------------------------------------------------------------------- /tests/integration/deployment/custom_action/test_pause_resume.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | from jina import Client, Document, DocumentArray 6 | 7 | 8 | from jcloud.constants import Phase 9 | from jcloud.deployment import CloudDeployment 10 | 11 | from tests.utils import utils 12 | from .. import DeploymentAlive 13 | 14 | deployments_dir = os.path.join( 15 | os.path.dirname(os.path.abspath(__file__)), 'deployments' 16 | ) 17 | deployment_file = 'base_deployment.yml' 18 | protocol = 'grpc' 19 | executor_name = 'executor' 20 | 21 | 22 | def test_pause_resume_deployment(): 23 | with CloudDeployment( 24 | path=os.path.join(deployments_dir, deployment_file) 25 | ) as deployment: 26 | assert utils.eventually_reaches_phase(deployment, Phase.Serving) 27 | 28 | assert deployment.endpoints != {} 29 | assert executor_name in deployment.endpoints 30 | endpoint = deployment.endpoints[executor_name] 31 | assert endpoint.startswith(f'{protocol}s://') 32 | 33 | ltt = utils.get_last_transition_time(deployment, DeploymentAlive) 34 | assert ltt 35 | 36 | assert utils.eventually_serve_requests(endpoint) 37 | 38 | # pause the deployment 39 | deployment._loop.run_until_complete(deployment.pause()) 40 | assert utils.eventually_reaches_phase(deployment, Phase.Paused) 41 | assert utils.eventually_condition_gets_updated(deployment, DeploymentAlive, ltt) 42 | 43 | assert deployment.endpoints != {} 44 | assert executor_name in deployment.endpoints 45 | endpoint = deployment.endpoints[executor_name] 46 | assert endpoint.startswith(f'{protocol}s://') 47 | 48 | with pytest.raises(ConnectionError): 49 | da = Client(host=endpoint).post( 50 | on='/', 51 | inputs=DocumentArray(Document(text=f'text-{i}') for i in range(50)), 52 | ) 53 | 54 | # resume the deployment 55 | ltt = utils.get_last_transition_time(deployment, DeploymentAlive) 56 | assert ltt 57 | 58 | deployment._loop.run_until_complete(deployment.resume()) 59 | assert utils.eventually_reaches_phase(deployment, Phase.Serving) 60 | assert utils.eventually_condition_gets_updated(deployment, DeploymentAlive, ltt) 61 | 62 | assert deployment.endpoints != {} 63 | assert executor_name in deployment.endpoints 64 | endpoint = deployment.endpoints[executor_name] 65 | assert endpoint.startswith(f'{protocol}s://') 66 | 67 | assert utils.eventually_serve_requests(endpoint) 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Initially taken from Github's Python gitignore file 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # IPython 79 | profile_default/ 80 | ipython_config.py 81 | 82 | # pyenv 83 | docs/.python-version 84 | 85 | # celery beat schedule file 86 | celerybeat-schedule 87 | 88 | # SageMath parsed files 89 | *.sage.py 90 | 91 | # Environments 92 | .venv 93 | env/ 94 | venv/ 95 | ENV/ 96 | env.bak/ 97 | venv.bak/ 98 | 99 | # Spyder project settings 100 | .spyderproject 101 | .spyproject 102 | 103 | # Rope project settings 104 | .ropeproject 105 | 106 | # mkdocs documentation 107 | /site 108 | 109 | # mypy 110 | .mypy_cache/ 111 | .dmypy.json 112 | dmypy.json 113 | 114 | # Pyre type checker 115 | .pyre/ 116 | .idea/ 117 | toy*.py 118 | .DS_Store 119 | post/ 120 | toy*.ipynb 121 | data/ 122 | *.c 123 | .nes_cache 124 | toy*.yml 125 | *.tmp 126 | 127 | shell/jina-wizard.sh 128 | /junit/ 129 | /tests/junit/ 130 | /docs/chapters/proto/docs.md 131 | /tests/.pytest-kind 132 | 133 | # IntelliJ IDEA 134 | *.iml 135 | .idea 136 | 137 | # VSCode 138 | .vscode 139 | 140 | # test with config in resources 141 | tests/integration/crud/simple/simple_indexer/ 142 | 143 | # latency tracking 144 | latency 145 | MyIndexer/ 146 | MyMemMap/ 147 | original/ 148 | output/ 149 | 150 | # kubernetes testing 151 | .pytest-kind 152 | .kube -------------------------------------------------------------------------------- /tests/integration/flow/remove_multiple/test_multi_flows_removal.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | 4 | import pytest 5 | 6 | from jcloud.api import _remove_multi 7 | from jcloud.constants import Phase 8 | from jcloud.flow import CloudFlow 9 | from jcloud.helper import get_logger 10 | 11 | logger = get_logger("Test Logger") 12 | 13 | flows_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'flows') 14 | flow_one = 'flow1.yml' 15 | flow_two = 'flow2.yml' 16 | 17 | 18 | async def _simplified_deploy(flow: CloudFlow): 19 | """Simplified deployment coroutine without using progress bar just for testing purpose. 20 | 21 | Since progress bar doesn't support displaying many at once if flows are running concurrently, 22 | so we have to use this workaround. 23 | """ 24 | 25 | json_result = await flow._deploy() 26 | flow.endpoints, flow.dashboard = await flow._fetch_until( 27 | intermediate=[ 28 | Phase.Empty, 29 | Phase.Pending, 30 | Phase.Starting, 31 | ], 32 | desired=Phase.Serving, 33 | ) 34 | return json_result['id'] 35 | 36 | 37 | async def get_serving_flows(): 38 | fl = await CloudFlow().list_all(phase=Phase.Serving.value) 39 | return {flow['id'] for flow in fl['flows']} 40 | 41 | 42 | @pytest.mark.asyncio 43 | async def test_remove_selected_flows(): 44 | initial_owned_flows = await get_serving_flows() 45 | 46 | logger.info(f'Initial owned flows: {len(initial_owned_flows)}') 47 | flow_1 = _simplified_deploy(CloudFlow(path=os.path.join(flows_dir, flow_one))) 48 | flow_2 = _simplified_deploy(CloudFlow(path=os.path.join(flows_dir, flow_two))) 49 | 50 | logger.info(f'Deploying two new flows...') 51 | added_flows = set() 52 | for coro in asyncio.as_completed([flow_1, flow_2]): 53 | r = await coro 54 | added_flows.add(r) 55 | 56 | owned_flows_after_add = await get_serving_flows() 57 | logger.info(f'New Flow added: {added_flows}') 58 | logger.info(f'Owned flows after new deployments: {len(owned_flows_after_add)}') 59 | # assert len(initial_owned_flows) + 2 == len(owned_flows_after_add) 60 | assert added_flows.issubset(owned_flows_after_add) 61 | 62 | logger.info(f'Removing two new flows...') 63 | await _remove_multi(list(added_flows), None) 64 | 65 | owned_flows_after_delete = await get_serving_flows() 66 | logger.info(f'Owned flows after removal: {len(owned_flows_after_delete)}') 67 | 68 | # assert len(initial_owned_flows) == len(owned_flows_after_delete) 69 | assert ( 70 | any([flow_id in owned_flows_after_delete for flow_id in added_flows]) == False 71 | ) 72 | -------------------------------------------------------------------------------- /tests/integration/flow/custom_actions/test_restart.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from jcloud.flow import CloudFlow 4 | from jcloud.constants import Phase 5 | 6 | from tests.utils import utils 7 | from .. import FlowAlive 8 | 9 | flows_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'flows') 10 | flow_file = 'base_flow.yml' 11 | protocol = 'http' 12 | 13 | 14 | def test_restart_flow(): 15 | with CloudFlow(path=os.path.join(flows_dir, flow_file)) as flow: 16 | assert utils.eventually_reaches_phase(flow, Phase.Serving) 17 | 18 | assert flow.endpoints != {} 19 | assert 'gateway' in flow.endpoints 20 | gateway = flow.endpoints['gateway'] 21 | assert gateway.startswith(f'{protocol}s://') 22 | 23 | ltt = utils.get_last_transition_time(flow, FlowAlive) 24 | assert ltt 25 | 26 | assert utils.eventually_serve_requests(gateway) 27 | 28 | # restart the flow 29 | flow._loop.run_until_complete(flow.restart()) 30 | assert utils.eventually_reaches_phase(flow, Phase.Serving) 31 | assert utils.eventually_condition_gets_updated(flow, FlowAlive, ltt) 32 | 33 | assert flow.endpoints != {} 34 | assert 'gateway' in flow.endpoints 35 | gateway = flow.endpoints['gateway'] 36 | assert gateway.startswith(f'{protocol}s://') 37 | 38 | assert utils.eventually_serve_requests(gateway) 39 | 40 | # restart the gateway of the flow 41 | ltt = utils.get_last_transition_time(flow, FlowAlive) 42 | assert ltt 43 | 44 | flow._loop.run_until_complete(flow.restart(gateway=True)) 45 | assert utils.eventually_reaches_phase(flow, Phase.Serving) 46 | assert utils.eventually_condition_gets_updated(flow, FlowAlive, ltt) 47 | 48 | assert flow.endpoints != {} 49 | assert 'gateway' in flow.endpoints 50 | gateway = flow.endpoints['gateway'] 51 | assert gateway.startswith(f'{protocol}s://') 52 | 53 | assert utils.eventually_serve_requests(gateway) 54 | 55 | # restart one of the executors of the flow 56 | ltt = utils.get_last_transition_time(flow, FlowAlive) 57 | assert ltt 58 | 59 | flow._loop.run_until_complete(flow.restart(executor='executor0')) 60 | assert utils.eventually_reaches_phase(flow, Phase.Serving) 61 | assert utils.eventually_condition_gets_updated(flow, FlowAlive, ltt) 62 | 63 | assert flow.endpoints != {} 64 | assert 'gateway' in flow.endpoints 65 | gateway = flow.endpoints['gateway'] 66 | assert gateway.startswith(f'{protocol}s://') 67 | 68 | assert utils.eventually_serve_requests(gateway) 69 | -------------------------------------------------------------------------------- /jcloud/parsers/list.py: -------------------------------------------------------------------------------- 1 | from .helper import _chf 2 | from ..constants import Phase, Resources 3 | 4 | 5 | def set_list_resource_parser(subparser, parser_prog): 6 | 7 | if Resources.Flow in parser_prog: 8 | list_parser = subparser.add_parser( 9 | 'list', 10 | help='List all Flows that are in `Serving` or `Failed` phase if no phase is passed.', 11 | formatter_class=_chf, 12 | ) 13 | _set_list_flow_parser(list_parser) 14 | elif Resources.Deployment in parser_prog: 15 | list_parser = subparser.add_parser( 16 | 'list', 17 | help='List all Deployments that are in `Serving` or `Failed` phase if no phase is passed.', 18 | formatter_class=_chf, 19 | ) 20 | _set_list_deployment_parser(list_parser) 21 | else: 22 | resource = Resources.Job if Resources.Job in parser_prog else Resources.Secret 23 | list_parser = subparser.add_parser( 24 | 'list', 25 | help=f'List {resource.title()}s in a Flow.', 26 | formatter_class=_chf, 27 | ) 28 | _set_list_resource_parser(list_parser) 29 | 30 | 31 | def _set_list_flow_parser(list_parser): 32 | list_parser.add_argument( 33 | '--phase', 34 | type=str.title, 35 | choices=[s.value for s in Phase] + ['All'], 36 | help='Pass the phase of Flows to be listed.', 37 | ) 38 | 39 | list_parser.add_argument( 40 | '--name', 41 | type=str, 42 | default=None, 43 | help='Pass the name of Flows to be listed.', 44 | ) 45 | 46 | list_parser.add_argument( 47 | '--labels', 48 | type=str, 49 | default=None, 50 | help='Pass the labels with which to filter flows. Format is comma separated list of `key=value`.', 51 | ) 52 | 53 | 54 | def _set_list_deployment_parser(list_parser): 55 | list_parser.add_argument( 56 | '--phase', 57 | type=str.title, 58 | choices=[s.value for s in Phase] + ['All'], 59 | help='Pass the phase of deployments to be listed.', 60 | ) 61 | 62 | list_parser.add_argument( 63 | '--name', 64 | type=str, 65 | default=None, 66 | help='Pass the name of deployment to be listed.', 67 | ) 68 | 69 | list_parser.add_argument( 70 | '--labels', 71 | type=str, 72 | default=None, 73 | help='Pass the labels with which to filter deployments. Format is comma separated list of `key=value`.', 74 | ) 75 | 76 | 77 | def _set_list_resource_parser(list_parser): 78 | list_parser.add_argument( 79 | 'flow', 80 | type=str, 81 | help='The string ID of the Flow.', 82 | ) 83 | -------------------------------------------------------------------------------- /jcloud/parsers/remove.py: -------------------------------------------------------------------------------- 1 | from .helper import _chf 2 | from ..constants import Phase, Resources 3 | 4 | 5 | def set_remove_resource_parser(subparser, parser_prog): 6 | 7 | if Resources.Flow in parser_prog: 8 | remove_parser = subparser.add_parser( 9 | 'remove', 10 | help='Remove Flow(s). If `all` is passed it removes Flows in `Serving` or `Failed` phase.', 11 | formatter_class=_chf, 12 | ) 13 | _set_remove_flow_parser(remove_parser) 14 | elif Resources.Deployment in parser_prog: 15 | remove_parser = subparser.add_parser( 16 | 'remove', 17 | help='Remove Deployment(s). If `all` is passed it removes Deployments in `Serving` or `Failed` phase.', 18 | formatter_class=_chf, 19 | ) 20 | _set_remove_deployment_parser(remove_parser) 21 | else: 22 | resource = Resources.Job if Resources.Job in parser_prog else Resources.Secret 23 | remove_parser = subparser.add_parser( 24 | 'remove', 25 | help=f'Remove a {resource.title()} from a Flow.', 26 | formatter_class=_chf, 27 | ) 28 | _set_remove_resource_parser(remove_parser, resource) 29 | 30 | 31 | def _set_remove_flow_parser(remove_parser): 32 | remove_parser.add_argument( 33 | '--phase', 34 | help='The phase to filter flows on for removal', 35 | type=str, 36 | choices=[s.value for s in Phase if s.value != ''] + ['All'], 37 | ) 38 | 39 | remove_parser.add_argument( 40 | 'flows', 41 | nargs="*", 42 | help='The string ID of a flow for single removal, ' 43 | 'or a list of space separated string IDs for multiple removal, ' 44 | 'or string \'all\' for deleting ALL SERVING flows.', 45 | ) 46 | 47 | 48 | def _set_remove_deployment_parser(remove_parser): 49 | remove_parser.add_argument( 50 | '--phase', 51 | help='The phase to filter deployments on for removal', 52 | type=str, 53 | choices=[s.value for s in Phase if s.value != ''] + ['All'], 54 | ) 55 | 56 | remove_parser.add_argument( 57 | 'deployments', 58 | nargs="*", 59 | help='The string ID of a deployment for single removal, ' 60 | 'or a list of space separated string IDs for multiple removal, ' 61 | 'or string \'all\' for deleting ALL SERVING deployments.', 62 | ) 63 | 64 | 65 | def _set_remove_resource_parser(remove_parser, resource): 66 | remove_parser.add_argument( 67 | 'name', 68 | type=str, 69 | help=f'The name of the {resource.title()} to remove.', 70 | ) 71 | 72 | remove_parser.add_argument( 73 | 'flow', 74 | type=str, 75 | help='The string ID of the Flow.', 76 | ) 77 | -------------------------------------------------------------------------------- /tests/integration/flow/update/test_update_env.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from jcloud.flow import CloudFlow 4 | from jcloud.constants import Phase 5 | 6 | from jcloud.helper import get_dict_list_key_path 7 | 8 | from tests.utils import utils 9 | from .. import FlowAlive 10 | 11 | flows_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'flows') 12 | flow_file = 'base_flow.yml' 13 | add_env_flow_file = 'add_env.yml' 14 | modify_env_flow_file = 'modify_env.yml' 15 | protocol = 'http' 16 | 17 | 18 | def test_update_executor_env(): 19 | with CloudFlow(path=os.path.join(flows_dir, flow_file)) as flow: 20 | assert utils.eventually_reaches_phase(flow, Phase.Serving) 21 | 22 | assert flow.endpoints != {} 23 | assert 'gateway' in flow.endpoints 24 | gateway = flow.endpoints['gateway'] 25 | assert gateway.startswith(f'{protocol}s://') 26 | 27 | ltt = utils.get_last_transition_time(flow, FlowAlive) 28 | assert ltt 29 | 30 | assert utils.eventually_serve_requests(gateway) 31 | 32 | flow.path = os.path.join(flows_dir, add_env_flow_file) 33 | flow._loop.run_until_complete(flow.update()) 34 | assert utils.eventually_reaches_phase(flow, Phase.Serving) 35 | assert utils.eventually_condition_gets_updated(flow, FlowAlive, ltt) 36 | 37 | assert flow.endpoints != {} 38 | assert 'gateway' in flow.endpoints 39 | gateway = flow.endpoints['gateway'] 40 | assert gateway.startswith(f'{protocol}s://') 41 | 42 | status = flow._loop.run_until_complete(flow.status) 43 | 44 | env = get_dict_list_key_path(status, ['spec', 'executors', 0, 'env']) 45 | 46 | assert 'JINA_LOG_LEVEL' in env and env['JINA_LOG_LEVEL'] == 'DEBUG' 47 | assert 'PUNCT_CHARS' in env and env['PUNCT_CHARS'] == '(!,)' 48 | 49 | assert utils.eventually_serve_requests(gateway) 50 | 51 | ltt = utils.get_last_transition_time(flow, FlowAlive) 52 | assert ltt 53 | flow.path = os.path.join(flows_dir, modify_env_flow_file) 54 | flow._loop.run_until_complete(flow.update()) 55 | assert utils.eventually_reaches_phase(flow, Phase.Serving) 56 | assert utils.eventually_condition_gets_updated(flow, FlowAlive, ltt) 57 | 58 | assert flow.endpoints != {} 59 | assert 'gateway' in flow.endpoints 60 | gateway = flow.endpoints['gateway'] 61 | assert gateway.startswith(f'{protocol}s://') 62 | 63 | status = flow._loop.run_until_complete(flow.status) 64 | 65 | env = get_dict_list_key_path(status, ['spec', 'executors', 0, 'env']) 66 | 67 | assert 'JINA_LOG_LEVEL' in env and env['JINA_LOG_LEVEL'] == 'INFO' 68 | assert 'PUNCT_CHARS' not in env 69 | 70 | assert utils.eventually_serve_requests(gateway) 71 | -------------------------------------------------------------------------------- /tests/unit/test_flow.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | 4 | import pytest 5 | 6 | import jcloud 7 | from jcloud import flow # import flow_normalize 8 | from jcloud.flow import CloudFlow 9 | from jcloud.normalize import flow_normalize 10 | 11 | cur_dir = os.path.dirname(os.path.abspath(__file__)) 12 | 13 | 14 | def func(*args, **kwargs): 15 | return tempfile.mkstemp()[1] 16 | 17 | 18 | @pytest.mark.asyncio 19 | @pytest.mark.parametrize('filename', ('grpc-flow.yml', 'http-flow.yml')) 20 | async def test_post_params_normalized_flow(monkeypatch, filename): 21 | flow = CloudFlow( 22 | path=os.path.join( 23 | cur_dir, '..', 'integration', 'flow', 'basic', 'flows', filename 24 | ) 25 | ) 26 | monkeypatch.setattr('jcloud.normalize.flow_normalize', func) 27 | _post_params = await flow._get_post_params() 28 | assert 'data' in _post_params 29 | assert len(_post_params['data']._fields) == 1 30 | assert _post_params['data']._fields[0][0]['name'] == 'spec' 31 | assert 'params' in _post_params 32 | assert _post_params['params'] == {} 33 | 34 | 35 | @pytest.mark.asyncio 36 | async def test_post_params_normalized_flow_with_env(monkeypatch): 37 | flow = CloudFlow( 38 | path=os.path.join( 39 | cur_dir, '..', 'integration', 'flow', 'basic', 'flows', 'http-flow.yml' 40 | ) 41 | ) 42 | monkeypatch.setattr('jcloud.normalize.flow_normalize', func) 43 | _post_params = await flow._get_post_params() 44 | assert 'data' in _post_params 45 | assert len(_post_params['data']._fields) == 1 46 | assert _post_params['data']._fields[0][0]['name'] == 'spec' 47 | assert 'params' in _post_params 48 | assert _post_params['params'] == {} 49 | 50 | 51 | @pytest.mark.asyncio 52 | @pytest.mark.parametrize( 53 | 'dirname', 54 | ( 55 | 'simple', 56 | # 'multi_executors', 57 | # 'envvars_default_file', 58 | ), 59 | ) 60 | async def test_post_params_local_project_file(monkeypatch, dirname): 61 | flow = CloudFlow( 62 | path=os.path.join( 63 | cur_dir, '..', 'integration', 'flow', 'projects', dirname, 'flow.yml' 64 | ) 65 | ) 66 | monkeypatch.setattr('jcloud.normalize.flow_normalize', func) 67 | _post_params = await flow._get_post_params() 68 | assert 'data' in _post_params 69 | assert 'params' in _post_params 70 | 71 | 72 | @pytest.mark.asyncio 73 | @pytest.mark.parametrize( 74 | 'dirname', 75 | ( 76 | 'simple', 77 | 'multi_executors', 78 | 'envvars_default_file', 79 | ), 80 | ) 81 | async def test_post_params_local_project_dir(monkeypatch, dirname): 82 | flow = CloudFlow( 83 | path=os.path.join(cur_dir, '..', 'integration', 'flow', 'projects', dirname) 84 | ) 85 | monkeypatch.setattr('jcloud.normalize.flow_normalize', func) 86 | _post_params = await flow._get_post_params() 87 | assert 'data' in _post_params 88 | assert 'params' in _post_params 89 | -------------------------------------------------------------------------------- /tests/integration/flow/update/test_update_labels.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from jcloud.flow import CloudFlow 4 | from jcloud.constants import Phase 5 | 6 | from jcloud.helper import get_dict_list_key_path 7 | 8 | from tests.utils import utils 9 | from .. import FlowAlive 10 | 11 | flows_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'flows') 12 | flow_file = 'base_flow.yml' 13 | add_labels_flow_file = 'add_labels.yml' 14 | modify_delete_labels_flow_file = "modify_delete_labels.yml" 15 | protocol = 'http' 16 | 17 | 18 | def test_update_labels_of_flow(): 19 | with CloudFlow(path=os.path.join(flows_dir, flow_file)) as flow: 20 | assert utils.eventually_reaches_phase(flow, Phase.Serving) 21 | assert flow.endpoints != {} 22 | assert 'gateway' in flow.endpoints 23 | gateway = flow.endpoints['gateway'] 24 | assert gateway.startswith(f'{protocol}s://') 25 | 26 | ltt = utils.get_last_transition_time(flow, FlowAlive) 27 | assert ltt 28 | 29 | assert utils.eventually_serve_requests(gateway) 30 | 31 | flow.path = os.path.join(flows_dir, add_labels_flow_file) 32 | flow._loop.run_until_complete(flow.update()) 33 | assert utils.eventually_reaches_phase(flow, Phase.Serving) 34 | assert utils.eventually_condition_gets_updated(flow, FlowAlive, ltt) 35 | 36 | assert flow.endpoints != {} 37 | assert 'gateway' in flow.endpoints 38 | gateway = flow.endpoints['gateway'] 39 | assert gateway.startswith(f'{protocol}s://') 40 | 41 | status = flow._loop.run_until_complete(flow.status) 42 | 43 | labels = get_dict_list_key_path(status, ['spec', 'jcloud', 'labels']) 44 | assert "jina.ai/username" in labels and labels["jina.ai/username"] == "johndoe" 45 | assert ( 46 | "jina.ai/application" in labels 47 | and labels["jina.ai/application"] == "fashion-search" 48 | ) 49 | 50 | assert utils.eventually_serve_requests(gateway) 51 | 52 | ltt = utils.get_last_transition_time(flow, FlowAlive) 53 | assert ltt 54 | flow.path = os.path.join(flows_dir, modify_delete_labels_flow_file) 55 | flow._loop.run_until_complete(flow.update()) 56 | assert utils.eventually_reaches_phase(flow, Phase.Serving) 57 | assert utils.eventually_condition_gets_updated(flow, FlowAlive, ltt) 58 | 59 | assert flow.endpoints != {} 60 | assert 'gateway' in flow.endpoints 61 | gateway = flow.endpoints['gateway'] 62 | assert gateway.startswith(f'{protocol}s://') 63 | 64 | status = flow._loop.run_until_complete(flow.status) 65 | 66 | labels = get_dict_list_key_path(status, ['spec', 'jcloud', 'labels']) 67 | assert "jina.ai/username" not in labels 68 | assert ( 69 | "jina.ai/application" in labels 70 | and labels["jina.ai/application"] == "retail-search" 71 | ) 72 | 73 | assert utils.eventually_serve_requests(gateway) 74 | -------------------------------------------------------------------------------- /tests/integration/deployment/update/test_update_env.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from jcloud.deployment import CloudDeployment 4 | from jcloud.constants import Phase 5 | 6 | from jcloud.helper import get_dict_list_key_path 7 | 8 | from tests.utils import utils 9 | from .. import DeploymentAlive 10 | 11 | deployments_dir = os.path.join( 12 | os.path.dirname(os.path.abspath(__file__)), 'deployments' 13 | ) 14 | deployment_file = 'base_deployment.yml' 15 | add_env_deployment_file = 'add_env.yml' 16 | modify_env_deployment_file = 'modify_env.yml' 17 | protocol = 'grpc' 18 | executor_name = 'executor' 19 | 20 | 21 | def test_update_executor_env(): 22 | with CloudDeployment( 23 | path=os.path.join(deployments_dir, deployment_file) 24 | ) as deployment: 25 | assert utils.eventually_reaches_phase(deployment, Phase.Serving) 26 | 27 | assert deployment.endpoints != {} 28 | assert executor_name in deployment.endpoints 29 | endpoint = deployment.endpoints[executor_name] 30 | assert endpoint.startswith(f'{protocol}s://') 31 | 32 | ltt = utils.get_last_transition_time(deployment, DeploymentAlive) 33 | assert ltt 34 | 35 | assert utils.eventually_serve_requests(endpoint) 36 | 37 | deployment.path = os.path.join(deployments_dir, add_env_deployment_file) 38 | deployment._loop.run_until_complete(deployment.update()) 39 | assert utils.eventually_reaches_phase(deployment, Phase.Serving) 40 | assert utils.eventually_condition_gets_updated(deployment, DeploymentAlive, ltt) 41 | 42 | assert deployment.endpoints != {} 43 | assert executor_name in deployment.endpoints 44 | endpoint = deployment.endpoints[executor_name] 45 | assert endpoint.startswith(f'{protocol}s://') 46 | 47 | status = deployment._loop.run_until_complete(deployment.status) 48 | 49 | env = get_dict_list_key_path(status, ['spec', 'with', 'env']) 50 | 51 | assert 'JINA_LOG_LEVEL' in env and env['JINA_LOG_LEVEL'] == 'DEBUG' 52 | assert 'PUNCT_CHARS' in env and env['PUNCT_CHARS'] == '(!,)' 53 | 54 | assert utils.eventually_serve_requests(endpoint) 55 | 56 | ltt = utils.get_last_transition_time(deployment, DeploymentAlive) 57 | assert ltt 58 | deployment.path = os.path.join(deployments_dir, modify_env_deployment_file) 59 | deployment._loop.run_until_complete(deployment.update()) 60 | assert utils.eventually_reaches_phase(deployment, Phase.Serving) 61 | assert utils.eventually_condition_gets_updated(deployment, DeploymentAlive, ltt) 62 | 63 | assert deployment.endpoints != {} 64 | assert executor_name in deployment.endpoints 65 | endpoint = deployment.endpoints[executor_name] 66 | assert endpoint.startswith(f'{protocol}s://') 67 | 68 | status = deployment._loop.run_until_complete(deployment.status) 69 | 70 | env = get_dict_list_key_path(status, ['spec', 'with', 'env']) 71 | 72 | assert 'JINA_LOG_LEVEL' in env and env['JINA_LOG_LEVEL'] == 'INFO' 73 | assert 'PUNCT_CHARS' not in env 74 | 75 | assert utils.eventually_serve_requests(endpoint) 76 | -------------------------------------------------------------------------------- /jcloud/parsers/update.py: -------------------------------------------------------------------------------- 1 | import ast 2 | from pathlib import Path 3 | 4 | from .helper import _chf 5 | from ..constants import Resources 6 | 7 | 8 | def set_update_resource_parser(subparser, parser_prog): 9 | 10 | if Resources.Flow in parser_prog: 11 | update_parser = subparser.add_parser( 12 | 'update', 13 | help='Update a Flow.', 14 | formatter_class=_chf, 15 | ) 16 | _set_update_flow_parser(update_parser) 17 | elif Resources.Deployment in parser_prog: 18 | update_parser = subparser.add_parser( 19 | 'update', 20 | help='Update a Deployment.', 21 | formatter_class=_chf, 22 | ) 23 | _set_update_deployment_parser(update_parser) 24 | else: 25 | update_parser = subparser.add_parser( 26 | 'update', 27 | help='Update a Secret.', 28 | formatter_class=_chf, 29 | ) 30 | _set_update_secret_parser(update_parser) 31 | 32 | 33 | def _set_update_flow_parser(update_parser): 34 | update_parser.add_argument( 35 | 'flow', 36 | help='The string ID of the flow to be updated', 37 | ) 38 | 39 | update_parser.add_argument( 40 | 'path', 41 | type=Path, 42 | help='The local path to a Jina flow project directory or yml file.', 43 | ) 44 | 45 | 46 | def _set_update_deployment_parser(update_parser): 47 | update_parser.add_argument( 48 | 'deployment', 49 | help='The string ID of the deployment to be updated', 50 | ) 51 | 52 | update_parser.add_argument( 53 | 'path', 54 | type=Path, 55 | help='The local path to a Jina deployment project directory or yml file.', 56 | ) 57 | 58 | 59 | def _set_update_secret_parser(update_parser): 60 | update_parser.add_argument( 61 | 'name', 62 | help='The name of the Secret.', 63 | ) 64 | 65 | update_parser.add_argument( 66 | 'flow', 67 | type=str, 68 | help='The string ID of the Flow.', 69 | ) 70 | 71 | # TODO (subbu) need make flow to be optional as deployment can be used instead of flow 72 | # This is a breaking change for the cli options 73 | # update_parser.add_argument( 74 | # '--flow', 75 | # type=str, 76 | # help='The string ID of the Flow.', 77 | # ) 78 | # update_parser.add_argument( 79 | # '--deployment', 80 | # type=str, 81 | # help='The string ID of the Deployment.', 82 | # ) 83 | 84 | update_parser.add_argument( 85 | '--from-literal', 86 | type=ast.literal_eval, 87 | help='Literal Secret value. Should follow the format "{\'env1\':\'value\'},\'env2\':\'value2\'}}".', 88 | ) 89 | 90 | update_parser.add_argument( 91 | '--update', 92 | action='store_true', 93 | help='Whether to update the flow spec after create the Secret', 94 | ) 95 | 96 | update_parser.add_argument( 97 | '--path', 98 | type=Path, 99 | help='The path of flow yaml spec file', 100 | ) 101 | -------------------------------------------------------------------------------- /.github/workflows/label-pr.yml: -------------------------------------------------------------------------------- 1 | name: PR 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | assign-label-to-pr: 8 | runs-on: ubuntu-latest 9 | if: ${{ !github.event.pull_request.head.repo.fork }} 10 | steps: 11 | - uses: codelytv/pr-size-labeler@v1 12 | with: 13 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 14 | xs_max_size: '10' 15 | s_max_size: '100' 16 | m_max_size: '500' 17 | l_max_size: '1000' 18 | fail_if_xl: 'false' 19 | - uses: actions/labeler@v3 20 | with: 21 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 22 | - id: docs_updated 23 | if: contains( github.event.pull_request.labels.*.name, 'area/docs') 24 | run: echo '::set-output name=docs::true' 25 | outputs: 26 | docs: ${{ steps.docs_updated.outputs.docs }} 27 | 28 | deploy-to-netlify: 29 | runs-on: ubuntu-latest 30 | needs: [assign-label-to-pr] 31 | if: ${{ needs.assign-label-to-pr.outputs.docs == 'true' }} 32 | steps: 33 | - run: | 34 | echo "BRANCH_NAME=${{ github.head_ref }}" >> $GITHUB_ENV 35 | - uses: actions/checkout@v2 36 | with: 37 | repository: jina-ai/jcloud 38 | ref: ${{ env.BRANCH_NAME }} 39 | - uses: actions/setup-python@v2 40 | with: 41 | python-version: 3.7 42 | - uses: actions/setup-node@v2 43 | with: 44 | node-version: '14' 45 | - name: Build and Deploy 46 | run: | 47 | npm i -g netlify-cli 48 | python -m pip install --upgrade pip 49 | pip install -r requirements.txt 50 | git fetch origin 51 | export NUM_RELEASES=2 # only 2 last tags to save build time 52 | bash makedoc.sh development 53 | netlify deploy --dir=_build/dirhtml --alias="ft-${{ env.BRANCH_NAME }}" --message="Deploying docs to ${{ env.BRANCH_NAME }} branch" 54 | env: 55 | NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN1 }} 56 | NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} 57 | working-directory: docs 58 | - name: Find the prev comment if exists 59 | uses: peter-evans/find-comment@v1 60 | id: fc 61 | with: 62 | issue-number: ${{ github.event.pull_request.number }} 63 | comment-author: 'github-actions[bot]' 64 | body-includes: 'Docs are deployed' 65 | - name: Delete comment if exists 66 | if: ${{ steps.fc.outputs.comment-id != 0 && !github.event.pull_request.head.repo.fork }} 67 | uses: actions/github-script@v3 68 | with: 69 | github-token: ${{ secrets.GITHUB_TOKEN }} 70 | script: | 71 | github.issues.deleteComment({ 72 | owner: context.repo.owner, 73 | repo: context.repo.repo, 74 | comment_id: ${{ steps.fc.outputs.comment-id }}, 75 | }) 76 | - name: Add or update comment 77 | uses: peter-evans/create-or-update-comment@v1 78 | with: 79 | issue-number: ${{ github.event.pull_request.number }} 80 | body: | 81 | :memo: Docs are deployed on https://ft-${{ env.BRANCH_NAME }}--jina-docs.netlify.app :tada: 82 | -------------------------------------------------------------------------------- /tests/integration/flow/update/test_update_expose_executor.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from jina import Client, Document, DocumentArray, Flow 4 | 5 | from jcloud.flow import CloudFlow 6 | from jcloud.constants import Phase 7 | from jcloud.helper import remove_prefix 8 | 9 | from tests.utils import utils 10 | from .. import FlowAlive 11 | 12 | flows_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'flows') 13 | flow_file = 'base_flow.yml' 14 | exposed_executor_flow_file = 'expose_executor.yml' 15 | protocol = 'http' 16 | 17 | 18 | def test_update_executor_expose(): 19 | with CloudFlow(path=os.path.join(flows_dir, flow_file)) as flow: 20 | assert utils.eventually_reaches_phase(flow, Phase.Serving) 21 | 22 | assert flow.endpoints != {} 23 | assert 'gateway' in flow.endpoints 24 | gateway = flow.endpoints['gateway'] 25 | assert gateway.startswith(f'{protocol}s://') 26 | 27 | ltt = utils.get_last_transition_time(flow, FlowAlive) 28 | assert ltt 29 | 30 | assert utils.eventually_serve_requests(gateway) 31 | 32 | flow.path = os.path.join(flows_dir, exposed_executor_flow_file) 33 | flow._loop.run_until_complete(flow.update()) 34 | assert utils.eventually_reaches_phase(flow, Phase.Serving) 35 | assert utils.eventually_condition_gets_updated(flow, FlowAlive, ltt) 36 | 37 | assert flow.endpoints != {} 38 | assert 'gateway' in flow.endpoints 39 | gateway = flow.endpoints['gateway'] 40 | assert gateway.startswith(f'{protocol}s://') 41 | 42 | assert 'executor0' in flow.endpoints 43 | exc_host = flow.endpoints['executor0'] 44 | 45 | with Flow(protocol='HTTP').add( 46 | host=remove_prefix(exc_host, 'grpcs://'), 47 | external=True, 48 | port=443, 49 | tls=True, 50 | ) as f: 51 | da = f.post( 52 | on='/', 53 | inputs=DocumentArray(Document(text=f'text-{i}') for i in range(50)), 54 | ) 55 | assert len(da.texts) == 50 56 | 57 | 58 | def test_update_executor_unexpose(): 59 | with CloudFlow(path=os.path.join(flows_dir, exposed_executor_flow_file)) as flow: 60 | assert utils.eventually_reaches_phase(flow, Phase.Serving) 61 | 62 | assert flow.endpoints != {} 63 | assert 'gateway' in flow.endpoints 64 | gateway = flow.endpoints['gateway'] 65 | assert gateway.startswith(f'{protocol}s://') 66 | assert 'executor0' in flow.endpoints 67 | 68 | ltt = utils.get_last_transition_time(flow, FlowAlive) 69 | assert ltt 70 | 71 | assert utils.eventually_serve_requests(gateway) 72 | 73 | flow.path = os.path.join(flows_dir, flow_file) 74 | flow._loop.run_until_complete(flow.update()) 75 | assert utils.eventually_reaches_phase(flow, Phase.Serving) 76 | assert utils.eventually_condition_gets_updated(flow, FlowAlive, ltt) 77 | 78 | assert flow.endpoints != {} 79 | assert 'gateway' in flow.endpoints 80 | gateway = flow.endpoints['gateway'] 81 | assert gateway.startswith(f'{protocol}s://') 82 | 83 | assert 'executor0' not in flow.endpoints 84 | 85 | assert utils.eventually_serve_requests(gateway) 86 | -------------------------------------------------------------------------------- /tests/integration/deployment/update/test_update_labels.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from jcloud.deployment import CloudDeployment 4 | from jcloud.constants import Phase 5 | 6 | from jcloud.helper import get_dict_list_key_path 7 | 8 | from tests.utils import utils 9 | from .. import DeploymentAlive 10 | 11 | deployments_dir = os.path.join( 12 | os.path.dirname(os.path.abspath(__file__)), 'deployments' 13 | ) 14 | deployment_file = 'base_deployment.yml' 15 | add_labels_deployment_file = 'add_labels.yml' 16 | modify_delete_labels_deployment_file = "modify_delete_labels.yml" 17 | protocol = 'grpc' 18 | executor_name = 'executor' 19 | 20 | 21 | def test_update_labels_of_deployment(): 22 | with CloudDeployment( 23 | path=os.path.join(deployments_dir, deployment_file) 24 | ) as deployment: 25 | assert utils.eventually_reaches_phase(deployment, Phase.Serving) 26 | assert deployment.endpoints != {} 27 | assert executor_name in deployment.endpoints 28 | endpoint = deployment.endpoints[executor_name] 29 | assert endpoint.startswith(f'{protocol}s://') 30 | 31 | ltt = utils.get_last_transition_time(deployment, DeploymentAlive) 32 | assert ltt 33 | 34 | assert utils.eventually_serve_requests(endpoint) 35 | 36 | deployment.path = os.path.join(deployments_dir, add_labels_deployment_file) 37 | deployment._loop.run_until_complete(deployment.update()) 38 | assert utils.eventually_reaches_phase(deployment, Phase.Serving) 39 | assert utils.eventually_condition_gets_updated(deployment, DeploymentAlive, ltt) 40 | 41 | assert deployment.endpoints != {} 42 | assert executor_name in deployment.endpoints 43 | endpoint = deployment.endpoints[executor_name] 44 | assert endpoint.startswith(f'{protocol}s://') 45 | 46 | status = deployment._loop.run_until_complete(deployment.status) 47 | 48 | labels = get_dict_list_key_path(status, ['spec', 'jcloud', 'labels']) 49 | assert "jina.ai/username" in labels and labels["jina.ai/username"] == "johndoe" 50 | assert ( 51 | "jina.ai/application" in labels 52 | and labels["jina.ai/application"] == "fashion-search" 53 | ) 54 | 55 | assert utils.eventually_serve_requests(endpoint) 56 | 57 | ltt = utils.get_last_transition_time(deployment, DeploymentAlive) 58 | assert ltt 59 | deployment.path = os.path.join( 60 | deployments_dir, modify_delete_labels_deployment_file 61 | ) 62 | deployment._loop.run_until_complete(deployment.update()) 63 | assert utils.eventually_reaches_phase(deployment, Phase.Serving) 64 | assert utils.eventually_condition_gets_updated(deployment, DeploymentAlive, ltt) 65 | 66 | assert deployment.endpoints != {} 67 | assert executor_name in deployment.endpoints 68 | endpoint = deployment.endpoints[executor_name] 69 | assert endpoint.startswith(f'{protocol}s://') 70 | 71 | status = deployment._loop.run_until_complete(deployment.status) 72 | 73 | labels = get_dict_list_key_path(status, ['spec', 'jcloud', 'labels']) 74 | assert "jina.ai/username" not in labels 75 | assert ( 76 | "jina.ai/application" in labels 77 | and labels["jina.ai/application"] == "retail-search" 78 | ) 79 | 80 | assert utils.eventually_serve_requests(endpoint) 81 | -------------------------------------------------------------------------------- /.github/workflows/integration-tests.yml: -------------------------------------------------------------------------------- 1 | name: Integration tests 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | branch: 7 | description: Pass the JCloud branch 8 | required: false 9 | default: main 10 | # schedule: 11 | # - cron: "0 4,16 * * 1-5" 12 | 13 | jobs: 14 | prep-testbed: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v2 18 | with: 19 | ref: ${{ github.event.inputs.branch }} 20 | - id: set-matrix 21 | run: | 22 | sudo apt-get install jq 23 | echo "::set-output name=matrix::$(bash scripts/get-all-test-paths.sh integration 1)" 24 | outputs: 25 | matrix: ${{ steps.set-matrix.outputs.matrix }} 26 | 27 | integration-tests: 28 | needs: prep-testbed 29 | runs-on: ubuntu-latest 30 | env: 31 | JINA_AUTH_TOKEN: ${{ secrets.JCLOUD_INTEGRATION_TESTS_TOKEN }} 32 | strategy: 33 | max-parallel: 5 34 | fail-fast: false 35 | matrix: 36 | python-version: ['3.10'] 37 | test-path: ${{fromJson(needs.prep-testbed.outputs.matrix)}} 38 | steps: 39 | - uses: actions/checkout@v2 40 | with: 41 | ref: ${{ github.event.inputs.branch }} 42 | - name: Set up Python ${{ matrix.python-version }} 43 | uses: actions/setup-python@v2 44 | with: 45 | python-version: ${{ matrix.python-version }} 46 | - name: Prepare environment 47 | run: | 48 | python -m pip install --upgrade pip 49 | python -m pip install wheel 50 | pip install --no-cache-dir ".[test]" 51 | pip install docarray==0.21.0 52 | sudo apt-get install libsndfile1 53 | - name: Test 54 | id: test 55 | run: | 56 | export JCLOUD_LOGLEVEL=DEBUG 57 | pytest --suppress-no-test-exit-code --cov=jcloud --cov-report=xml \ 58 | -v -s --log-cli-level=DEBUG -m "not gpu" ${{ matrix.test-path }} 59 | echo "::set-output name=codecov_flag::jcloud" 60 | timeout-minutes: 30 61 | - name: Check codecov file 62 | id: check_files 63 | uses: andstor/file-existence-action@v1 64 | with: 65 | files: "coverage.xml" 66 | - name: Upload coverage from test to Codecov 67 | uses: codecov/codecov-action@v2 68 | if: steps.check_files.outputs.files_exists == 'true' && ${{ matrix.python-version }} == '3.7' 69 | with: 70 | file: coverage.xml 71 | flags: ${{ steps.test.outputs.codecov_flag }} 72 | fail_ci_if_error: false 73 | token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos 74 | - if: failure() 75 | uses: 8398a7/action-slack@v3 76 | with: 77 | status: ${{ job.status }} 78 | fields: eventName,job 79 | text: | 80 | :no_entry: `${{ matrix.test-path }}` failed for branch `${{ github.event.inputs.branch }}` 81 | author_name: ":jcloud: Integration Test" 82 | env: 83 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_NIGHTLY_TESTS_WEBHOOK }} 84 | MATRIX_CONTEXT: ${{ toJson(matrix) }} 85 | 86 | # just for blocking the merge until all parallel integration-tests are successful 87 | success-all-test: 88 | needs: integration-tests 89 | if: always() 90 | runs-on: ubuntu-latest 91 | steps: 92 | - uses: technote-space/workflow-conclusion-action@v2 93 | - name: Check Failure 94 | if: env.WORKFLOW_CONCLUSION == 'failure' 95 | run: exit 1 96 | - name: Success 97 | if: ${{ success() }} 98 | run: echo "All Done" 99 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from os import path 3 | 4 | from setuptools import find_packages, setup 5 | 6 | if sys.version_info < (3, 7, 0): 7 | raise OSError(f'Requires Python >=3.7, but yours is {sys.version}') 8 | 9 | try: 10 | pkg_name = 'jcloud' 11 | libinfo_py = path.join(pkg_name, '__init__.py') 12 | libinfo_content = open(libinfo_py, 'r', encoding='utf8').readlines() 13 | version_line = [l.strip() for l in libinfo_content if l.startswith('__version__')][ 14 | 0 15 | ] 16 | exec(version_line) # gives __version__ 17 | except FileNotFoundError: 18 | __version__ = '0.0.0' 19 | 20 | try: 21 | with open('README.md', encoding='utf8') as fp: 22 | _long_description = fp.read() 23 | except FileNotFoundError: 24 | _long_description = '' 25 | 26 | setup( 27 | name=pkg_name, 28 | packages=find_packages(), 29 | version=__version__, 30 | include_package_data=True, 31 | description='Simplify deploying and managing Jina projects on Jina Cloud', 32 | author='Jina AI', 33 | author_email='hello@jina.ai', 34 | license='Apache 2.0', 35 | url='https://github.com/jina-ai/jcloud', 36 | download_url='https://github.com/jina-ai/jcloud/tags', 37 | long_description=_long_description, 38 | long_description_content_type='text/markdown', 39 | zip_safe=False, 40 | setup_requires=['setuptools>=18.0', 'wheel'], 41 | install_requires=[ 42 | 'rich>=12.0.0', 43 | 'aiohttp>=3.8.0', 44 | 'jina-hubble-sdk>=0.26.10', 45 | 'packaging', 46 | 'pyyaml', 47 | 'python-dotenv', 48 | 'python-dateutil', 49 | ], 50 | extras_require={ 51 | 'test': [ 52 | 'pytest', 53 | 'pytest-asyncio', 54 | 'pytest-timeout', 55 | 'pytest-mock', 56 | 'pytest-cov', 57 | 'pytest-repeat', 58 | 'pytest-reraise', 59 | 'pytest-env', 60 | 'mock', 61 | 'pytest-custom_exit_code', 62 | 'black==22.3.0', 63 | 'jina>=3.7.0', 64 | ], 65 | }, 66 | entry_points={ 67 | 'console_scripts': [ 68 | 'jcloud=jcloud.__main__:main', 69 | 'jc=jcloud.__main__:main', 70 | ], 71 | }, 72 | classifiers=[ 73 | 'Development Status :: 5 - Production/Stable', 74 | 'Intended Audience :: Developers', 75 | 'Intended Audience :: Education', 76 | 'Intended Audience :: Science/Research', 77 | 'Programming Language :: Python :: 3.7', 78 | 'Programming Language :: Python :: 3.8', 79 | 'Programming Language :: Python :: 3.9', 80 | 'Programming Language :: Python :: 3.10', 81 | 'Programming Language :: Unix Shell', 82 | 'Environment :: Console', 83 | 'License :: OSI Approved :: Apache Software License', 84 | 'Operating System :: OS Independent', 85 | 'Topic :: Database :: Database Engines/Servers', 86 | 'Topic :: Scientific/Engineering :: Artificial Intelligence', 87 | 'Topic :: Internet :: WWW/HTTP :: Indexing/Search', 88 | 'Topic :: Scientific/Engineering :: Image Recognition', 89 | 'Topic :: Multimedia :: Video', 90 | 'Topic :: Scientific/Engineering', 91 | 'Topic :: Scientific/Engineering :: Mathematics', 92 | 'Topic :: Software Development', 93 | 'Topic :: Software Development :: Libraries', 94 | 'Topic :: Software Development :: Libraries :: Python Modules', 95 | ], 96 | project_urls={ 97 | 'Documentation': 'https://jcloud.jina.ai', 98 | 'Source': 'https://github.com/jina-ai/jcloud/', 99 | 'Tracker': 'https://github.com/jina-ai/jcloud/issues', 100 | }, 101 | keywords='jcloud neural-search serverless deployment devops mlops', 102 | ) 103 | -------------------------------------------------------------------------------- /.github/README-img/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/flow/update/test_rename_executor.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from jina import Client, Document, DocumentArray, Flow 4 | 5 | from jcloud.flow import CloudFlow 6 | from jcloud.constants import Phase 7 | 8 | from jcloud.helper import get_dict_list_key_path, remove_prefix 9 | 10 | from tests.utils import utils 11 | from .. import FlowAlive 12 | 13 | flows_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'flows') 14 | flow_file = 'base_flow.yml' 15 | rename_executor_flow_file = 'custom_name_executor.yml' 16 | expsoed_executor_flow_file = 'expose_executor.yml' 17 | rename_expsoed_executor_flow_file = 'custom_name_exposed_executor.yml' 18 | protocol = 'http' 19 | 20 | 21 | def test_rename_executor(): 22 | with CloudFlow(path=os.path.join(flows_dir, flow_file)) as flow: 23 | assert utils.eventually_reaches_phase(flow, Phase.Serving) 24 | 25 | assert flow.endpoints != {} 26 | assert 'gateway' in flow.endpoints 27 | gateway = flow.endpoints['gateway'] 28 | assert gateway.startswith(f'{protocol}s://') 29 | 30 | ltt = utils.get_last_transition_time(flow, FlowAlive) 31 | assert ltt 32 | 33 | assert utils.eventually_serve_requests(gateway) 34 | 35 | flow.path = os.path.join(flows_dir, rename_executor_flow_file) 36 | flow._loop.run_until_complete(flow.update()) 37 | assert utils.eventually_reaches_phase(flow, Phase.Serving) 38 | assert utils.eventually_condition_gets_updated(flow, FlowAlive, ltt) 39 | 40 | assert flow.endpoints != {} 41 | assert 'gateway' in flow.endpoints 42 | gateway = flow.endpoints['gateway'] 43 | assert gateway.startswith(f'{protocol}s://') 44 | 45 | status = flow._loop.run_until_complete(flow.status) 46 | 47 | assert ( 48 | get_dict_list_key_path(status, ['spec', 'executors', 0, 'name']) 49 | == 'newsentencizer' 50 | ) 51 | 52 | assert utils.eventually_serve_requests(gateway) 53 | 54 | 55 | def test_rename_exposed_executor(): 56 | with CloudFlow(path=os.path.join(flows_dir, expsoed_executor_flow_file)) as flow: 57 | assert utils.eventually_reaches_phase(flow, Phase.Serving) 58 | 59 | assert flow.endpoints != {} 60 | assert 'gateway' in flow.endpoints 61 | gateway = flow.endpoints['gateway'] 62 | assert gateway.startswith(f'{protocol}s://') 63 | assert 'executor0' in flow.endpoints 64 | assert flow.endpoints['executor0'].startswith('grpcs://executor0') 65 | 66 | ltt = utils.get_last_transition_time(flow, FlowAlive) 67 | assert ltt 68 | 69 | assert utils.eventually_serve_requests(gateway) 70 | 71 | flow.path = os.path.join(flows_dir, rename_expsoed_executor_flow_file) 72 | flow._loop.run_until_complete(flow.update()) 73 | assert utils.eventually_reaches_phase(flow, Phase.Serving) 74 | assert utils.eventually_condition_gets_updated(flow, FlowAlive, ltt) 75 | 76 | assert flow.endpoints != {} 77 | assert 'gateway' in flow.endpoints 78 | gateway = flow.endpoints['gateway'] 79 | assert gateway.startswith(f'{protocol}s://') 80 | assert 'newsentencizer' in flow.endpoints 81 | assert flow.endpoints['newsentencizer'].startswith('grpcs://newsentencizer') 82 | exc_host = flow.endpoints['newsentencizer'] 83 | 84 | status = flow._loop.run_until_complete(flow.status) 85 | 86 | assert ( 87 | get_dict_list_key_path(status, ['spec', 'executors', 0, 'name']) 88 | == 'newsentencizer' 89 | ) 90 | 91 | with Flow(protocol='HTTP').add( 92 | host=remove_prefix(exc_host, 'grpcs://'), 93 | external=True, 94 | port=443, 95 | tls=True, 96 | ) as f: 97 | da = f.post( 98 | on='/', 99 | inputs=DocumentArray(Document(text=f'text-{i}') for i in range(50)), 100 | ) 101 | assert len(da.texts) == 50 102 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Requirements 4 | # brew install hub 5 | # npm install -g git-release-notes 6 | # pip install twine wheel 7 | 8 | set -ex 9 | 10 | INIT_FILE='jcloud/__init__.py' 11 | VER_TAG='__version__ = ' 12 | RELEASENOTE='./node_modules/.bin/git-release-notes' 13 | 14 | function escape_slashes { 15 | sed 's/\//\\\//g' 16 | } 17 | 18 | function update_ver_line { 19 | local OLD_LINE_PATTERN=$1 20 | local NEW_LINE=$2 21 | local FILE=$3 22 | 23 | local NEW=$(echo "${NEW_LINE}" | escape_slashes) 24 | sed -i '/'"${OLD_LINE_PATTERN}"'/s/.*/'"${NEW}"'/' "${FILE}" 25 | head -n10 ${FILE} 26 | } 27 | 28 | 29 | function clean_build { 30 | rm -rf dist 31 | rm -rf *.egg-info 32 | rm -rf build 33 | } 34 | 35 | function pub_pypi { 36 | # publish to pypi 37 | clean_build 38 | python setup.py sdist 39 | twine upload dist/* 40 | clean_build 41 | } 42 | 43 | function git_commit { 44 | git config --local user.email "dev-bot@jina.ai" 45 | git config --local user.name "Jina Dev Bot" 46 | git tag "v$RELEASE_VER" -m "$(cat ./CHANGELOG.tmp)" 47 | git add $INIT_FILE ./CHANGELOG.md 48 | git commit -m "chore(version): the next version will be $NEXT_VER" -m "build($RELEASE_ACTOR): $RELEASE_REASON" 49 | } 50 | 51 | 52 | 53 | function make_release_note { 54 | ${RELEASENOTE} ${LAST_VER}..HEAD .github/release-template.ejs > ./CHANGELOG.tmp 55 | head -n10 ./CHANGELOG.tmp 56 | printf '\n%s\n\n%s\n%s\n\n%s\n\n%s\n\n' "$(cat ./CHANGELOG.md)" "" "## Release Note (\`${RELEASE_VER}\`)" "> Release time: $(date +'%Y-%m-%d %H:%M:%S')" "$(cat ./CHANGELOG.tmp)" > ./CHANGELOG.md 57 | } 58 | 59 | BRANCH=$(git rev-parse --abbrev-ref HEAD) 60 | 61 | if [[ "$BRANCH" != "main" ]]; then 62 | printf "You are not at main branch, exit\n"; 63 | exit 1; 64 | fi 65 | 66 | LAST_UPDATE=`git show --no-notes --format=format:"%H" $BRANCH | head -n 1` 67 | LAST_COMMIT=`git show --no-notes --format=format:"%H" origin/$BRANCH | head -n 1` 68 | 69 | if [ $LAST_COMMIT != $LAST_UPDATE ]; then 70 | printf "Your local $BRANCH is behind the remote master, exit\n" 71 | exit 1; 72 | fi 73 | 74 | # release the current version 75 | export RELEASE_VER=$(sed -n '/^__version__/p' $INIT_FILE | cut -d \' -f2) 76 | LAST_VER=$(git tag -l | sort -V | tail -n1) 77 | printf "last version: \e[1;32m$LAST_VER\e[0m\n" 78 | 79 | if [[ $1 == "final" ]]; then 80 | printf "this will be a final release: \e[1;33m$RELEASE_VER\e[0m\n" 81 | 82 | NEXT_VER=$(echo $RELEASE_VER | awk -F. -v OFS=. 'NF==1{print ++$NF}; NF>1{$NF=sprintf("%0*d", length($NF), ($NF+1)); print}') 83 | printf "bump master version to: \e[1;32m$NEXT_VER\e[0m\n" 84 | 85 | make_release_note 86 | 87 | pub_pypi 88 | 89 | VER_TAG_NEXT=$VER_TAG\'${NEXT_VER}\' 90 | update_ver_line "$VER_TAG" "$VER_TAG_NEXT" "$INIT_FILE" 91 | RELEASE_REASON="$2" 92 | RELEASE_ACTOR="$3" 93 | git_commit 94 | elif [[ $1 == 'rc' ]]; then 95 | printf "this will be a release candidate: \e[1;33m$RELEASE_VER\e[0m\n" 96 | DOT_RELEASE_VER=$(echo $RELEASE_VER | sed "s/rc/\./") 97 | NEXT_VER=$(echo $DOT_RELEASE_VER | awk -F. -v OFS=. 'NF==1{print ++$NF}; NF>1{$NF=sprintf("%0*d", length($NF), ($NF+1)); print}') 98 | NEXT_VER=$(echo $NEXT_VER | sed "s/\.\([^.]*\)$/rc\1/") 99 | printf "bump master version to: \e[1;32m$NEXT_VER\e[0m, this will be the next version\n" 100 | 101 | make_release_note 102 | 103 | pub_pypi 104 | 105 | VER_TAG_NEXT=$VER_TAG\'${NEXT_VER}\' 106 | update_ver_line "$VER_TAG" "$VER_TAG_NEXT" "$INIT_FILE" 107 | RELEASE_REASON="$2" 108 | RELEASE_ACTOR="$3" 109 | git_commit 110 | else 111 | # as a prerelease, pypi update only, no back commit etc. 112 | COMMITS_SINCE_LAST_VER=$(git rev-list $LAST_VER..HEAD --count) 113 | NEXT_VER=$RELEASE_VER".dev"$COMMITS_SINCE_LAST_VER 114 | printf "this will be a developmental release: \e[1;33m$NEXT_VER\e[0m\n" 115 | 116 | VER_TAG_NEXT=$VER_TAG\'${NEXT_VER}\' 117 | update_ver_line "$VER_TAG" "$VER_TAG_NEXT" "$INIT_FILE" 118 | 119 | pub_pypi 120 | fi 121 | -------------------------------------------------------------------------------- /tests/unit/test_normalize.py: -------------------------------------------------------------------------------- 1 | import os 2 | import jina 3 | import pytest 4 | 5 | from unittest.mock import patch 6 | from jcloud.helper import load_flow_data 7 | from jcloud.normalize import * 8 | from jcloud.helper import JCloudLabelsError 9 | 10 | 11 | @pytest.fixture 12 | def cur_dir(): 13 | return Path(os.path.dirname(os.path.abspath(__file__))) 14 | 15 | 16 | @pytest.fixture 17 | def workspace(cur_dir): 18 | return cur_dir / 'flows' 19 | 20 | 21 | @pytest.fixture 22 | def mixed_flow_path(workspace): 23 | return workspace / 'mixed_flow' 24 | 25 | 26 | flow_data_params = ('normalized_flows', 'local_flow') 27 | 28 | 29 | def test_create_manifest(): 30 | executor = ExecutorData(name='test', src_dir='/path/to/folder', tag='last') 31 | project_id = 'normalized_flow_test' 32 | generate_manifest(executor, project_id) 33 | 34 | 35 | def test_get_hubble_url_with_executor_name(): 36 | executor = ExecutorData(name='test', src_dir='/path/to/folder', tag='last') 37 | 38 | assert ( 39 | get_hubble_uses(executor) == f'jinahub+docker://{executor.name}/{executor.tag}' 40 | ) 41 | 42 | 43 | def test_get_hubble_url_with_executor_id(): 44 | executor = ExecutorData(id='1y0jd3ac', src_dir='/path/to/folder', tag='last') 45 | 46 | assert get_hubble_uses(executor) == f'jinahub+docker://{executor.id}/{executor.tag}' 47 | 48 | 49 | @pytest.fixture(name='flow_data', params=flow_data_params) 50 | def flow_data(request, workspace): 51 | flow_path = workspace / request.param / 'flow.yml' 52 | flow_data = load_flow_data(flow_path) 53 | assert flow_data['jtype'] == 'Flow' 54 | assert len(flow_data['executors']) == 2 55 | return flow_data, request.param 56 | 57 | 58 | @pytest.fixture() 59 | def executors(flow_data, workspace): 60 | result = inspect_executors(flow_data[0], workspace=workspace / flow_data[1]) 61 | assert len(result) == 2 62 | if flow_data[1] == flow_data_params[0]: 63 | assert result[0].name == 'Executor1' 64 | assert result[0].hubble_url == 'jinahub+docker://Executor1' 65 | assert result[1].name == 'Executor2' 66 | assert result[1].hubble_url == 'jinahub+docker://Executor2' 67 | else: 68 | assert result[0].name == 'executor1-MyExecutor' 69 | assert result[0].src_dir == workspace / flow_data[1] / 'executor1' 70 | assert result[1].name == 'executor2-MyExecutor' 71 | assert result[1].src_dir == workspace / flow_data[1] / 'executor2' 72 | return result 73 | 74 | 75 | def test_normalize_flow(flow_data, executors): 76 | flow = update_flow_data(flow_data[0], executors) 77 | 78 | if flow_data[1] == flow_data_params[0]: 79 | assert flow['executors'][0]['uses'] == 'jinahub+docker://Executor1' 80 | assert flow['executors'][1]['uses'] == 'jinahub+docker://Executor2' 81 | else: 82 | assert flow['executors'][0]['uses'] == 'jinahub+docker://executor1-MyExecutor' 83 | assert flow['executors'][1]['uses'] == 'jinahub+docker://executor2-MyExecutor' 84 | 85 | 86 | @pytest.mark.parametrize('filename', ('flow1.yml', 'flow2.yml')) 87 | def test_inspect_executors_without_uses(filename, cur_dir): 88 | flow_dir = os.path.join(cur_dir, 'flows') 89 | flow_dict = load_flow_data(Path(os.path.join(flow_dir, filename))) 90 | executors = inspect_executors( 91 | flow_dict=flow_dict, workspace=flow_dir, tag='abc', secret='abc' 92 | ) 93 | assert executors[0].hubble_url == 'jinahub+docker://Sentencizer' 94 | assert executors[1].hubble_url == f'jinaai/jina:{jina.__version__}-py38-standard' 95 | assert executors[2].hubble_url == f'jinaai/jina:{jina.__version__}-py38-standard' 96 | 97 | 98 | def test_mixed_update_flow_data(mixed_flow_path): 99 | flow_data = load_flow_data(mixed_flow_path / 'flow.yml') 100 | executors = inspect_executors(flow_data, mixed_flow_path, '', '') 101 | executors[0].id = '14mqmnk1' 102 | flow_data = update_flow_data(flow_data, executors) 103 | assert flow_data['executors'][0]['uses'] == f'jinahub+docker://{executors[0].id}' 104 | assert flow_data['executors'][1]['uses'] == 'jinahub+docker://Sentencizer' 105 | 106 | 107 | @patch('jcloud.normalize.push_executors_to_hubble') 108 | def test_flow_normalize_with_output_path( 109 | push_to_hubble_mock, mixed_flow_path, tmp_path 110 | ): 111 | for output_path in [None, tmp_path, tmp_path / 'hello.yml']: 112 | fn = flow_normalize(mixed_flow_path / 'flow.yml', output_path=output_path) 113 | assert os.path.isfile(fn) 114 | if output_path is not None and output_path.suffix == '.yml': 115 | assert os.path.isfile(output_path) 116 | -------------------------------------------------------------------------------- /jcloud/parsers/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from argparse import ArgumentParser 3 | 4 | from .helper import _chf 5 | from ..constants import Resources 6 | 7 | 8 | def get_main_parser(parser=None): 9 | """The main parser for Jina 10 | 11 | :return: the parser 12 | """ 13 | from .base import set_base_parser, set_new_project_parser 14 | from .deploy import set_deploy_parser 15 | from .list import set_list_resource_parser 16 | from .get import set_get_resource_parser 17 | from .create import set_create_resource_parser 18 | from .remove import set_remove_resource_parser 19 | from .status import set_status_parser 20 | from .normalize import set_normalize_parser 21 | from .update import set_update_resource_parser 22 | from .logs import set_logs_resource_parser 23 | from .custom_actions import ( 24 | set_restart_parser, 25 | set_pause_parser, 26 | set_resume_parser, 27 | set_scale_parser, 28 | set_recreate_parser, 29 | ) 30 | 31 | # create the top-level parser 32 | parser = set_base_parser(parser=parser) 33 | 34 | sp = parser.add_subparsers( 35 | dest='jc_cli', 36 | required=True, 37 | ) 38 | 39 | sp.add_parser( 40 | 'login', 41 | help='Login to Jina AI Cloud / Ecosystem.', 42 | formatter_class=_chf, 43 | ) 44 | 45 | sp.add_parser( 46 | 'logout', 47 | help='Logout from Jina AI Cloud / Ecosystem.', 48 | formatter_class=_chf, 49 | ) 50 | 51 | resource_parsers = _add_resource_parsers(sp) 52 | 53 | for resource_parser in resource_parsers: 54 | subparser = resource_parser.add_subparsers( 55 | dest='subcommand', 56 | required=True, 57 | ) 58 | set_list_resource_parser(subparser, resource_parser.prog) 59 | set_remove_resource_parser(subparser, resource_parser.prog) 60 | if Resources.Job not in resource_parser.prog: 61 | set_update_resource_parser(subparser, resource_parser.prog) 62 | if ( 63 | Resources.Flow in resource_parser.prog 64 | or Resources.Deployment in resource_parser.prog 65 | ): 66 | set_restart_parser(subparser, resource_parser.prog) 67 | set_pause_parser(subparser, resource_parser.prog) 68 | set_resume_parser(subparser, resource_parser.prog) 69 | set_scale_parser(subparser, resource_parser.prog) 70 | set_recreate_parser(subparser, resource_parser.prog) 71 | set_status_parser(subparser, resource_parser.prog) 72 | set_deploy_parser(subparser, resource_parser.prog) 73 | set_normalize_parser(subparser, resource_parser.prog) 74 | if ( 75 | Resources.Flow in resource_parser.prog 76 | or Resources.Job in resource_parser.prog 77 | or Resources.Deployment in resource_parser.prog 78 | ): 79 | set_logs_resource_parser(subparser, resource_parser.prog) 80 | if ( 81 | Resources.Job in resource_parser.prog 82 | or Resources.Secret in resource_parser.prog 83 | ): 84 | resource = ( 85 | Resources.Job 86 | if Resources.Job in resource_parser.prog 87 | else Resources.Secret 88 | ) 89 | set_create_resource_parser(subparser, resource) 90 | set_get_resource_parser(subparser, resource) 91 | 92 | set_new_project_parser( 93 | sp.add_parser( 94 | 'new', 95 | help='Create a new project.', 96 | description='Create a new Jina project via template.', 97 | formatter_class=_chf, 98 | ) 99 | ) 100 | 101 | return parser 102 | 103 | 104 | def _add_resource_parsers(subparser) -> List[ArgumentParser]: 105 | flow_parser = subparser.add_parser( 106 | 'flow', 107 | help='Manage Flow(s).', 108 | formatter_class=_chf, 109 | aliases=['flows'], 110 | ) 111 | 112 | deployment_parser = subparser.add_parser( 113 | 'deployment', 114 | help='Manage Deployment(s).', 115 | formatter_class=_chf, 116 | aliases=['jds'], 117 | ) 118 | 119 | job_parser = subparser.add_parser( 120 | 'job', 121 | help='Manage Job(s).', 122 | formatter_class=_chf, 123 | aliases=['jobs'], 124 | ) 125 | 126 | secret_parser = subparser.add_parser( 127 | 'secret', 128 | help='Manage Secret(s).', 129 | formatter_class=_chf, 130 | aliases=['secrets'], 131 | ) 132 | 133 | return [flow_parser, deployment_parser, job_parser, secret_parser] 134 | --------------------------------------------------------------------------------