diff --git a/cli/cluster/patch.go b/cli/cluster/patch.go deleted file mode 100644 index 464a4cd6ad..0000000000 --- a/cli/cluster/patch.go +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright 2021 Cortex Labs, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package cluster - -import ( - "path/filepath" - - "github.com/cortexlabs/cortex/pkg/lib/errors" - "github.com/cortexlabs/cortex/pkg/lib/files" - "github.com/cortexlabs/cortex/pkg/lib/json" - s "github.com/cortexlabs/cortex/pkg/lib/strings" - "github.com/cortexlabs/cortex/pkg/operator/schema" -) - -func Patch(operatorConfig OperatorConfig, configPath string, force bool) ([]schema.DeployResult, error) { - params := map[string]string{ - "force": s.Bool(force), - "configFileName": filepath.Base(configPath), - } - - configBytes, err := files.ReadFileBytes(configPath) - if err != nil { - return nil, err - } - - response, err := HTTPPostJSON(operatorConfig, "/patch", configBytes, params) - if err != nil { - return nil, err - } - - var deployResults []schema.DeployResult - if err := json.Unmarshal(response, &deployResults); err != nil { - return nil, errors.Wrap(err, "/patch", string(response)) - } - - return deployResults, nil -} diff --git a/cli/cmd/cluster.go b/cli/cmd/cluster.go index 1f2348bf11..7bb7f38387 100644 --- a/cli/cmd/cluster.go +++ b/cli/cmd/cluster.go @@ -19,7 +19,6 @@ package cmd import ( "fmt" "os" - "path" "path/filepath" "regexp" "strings" @@ -675,9 +674,9 @@ var _clusterDownCmd = &cobra.Command{ } var _clusterExportCmd = &cobra.Command{ - Use: "export [API_NAME] [API_ID]", - Short: "download the code and configuration for APIs", - Args: cobra.RangeArgs(0, 2), + Use: "export", + Short: "download the configurations for all APIs", + Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { telemetry.Event("cli.cluster.export") @@ -715,25 +714,13 @@ var _clusterExportCmd = &cobra.Command{ } var apisResponse []schema.APIResponse - if len(args) == 0 { - apisResponse, err = cluster.GetAPIs(operatorConfig) - if err != nil { - exit.Error(err) - } - if len(apisResponse) == 0 { - fmt.Println(fmt.Sprintf("no apis found in your cluster named %s in %s", accessConfig.ClusterName, accessConfig.Region)) - exit.Ok() - } - } else if len(args) == 1 { - apisResponse, err = cluster.GetAPI(operatorConfig, args[0]) - if err != nil { - exit.Error(err) - } - } else if len(args) == 2 { - apisResponse, err = cluster.GetAPIByID(operatorConfig, args[0], args[1]) - if err != nil { - exit.Error(err) - } + apisResponse, err = cluster.GetAPIs(operatorConfig) + if err != nil { + exit.Error(err) + } + if len(apisResponse) == 0 { + fmt.Println(fmt.Sprintf("no apis found in your cluster named %s in %s", accessConfig.ClusterName, accessConfig.Region)) + exit.Ok() } exportPath := fmt.Sprintf("export-%s-%s", accessConfig.Region, accessConfig.ClusterName) @@ -744,21 +731,16 @@ var _clusterExportCmd = &cobra.Command{ } for _, apiResponse := range apisResponse { - baseDir := filepath.Join(exportPath, apiResponse.Spec.Name, apiResponse.Spec.ID) + specFilePath := filepath.Join(exportPath, apiResponse.Spec.Name+".yaml") - fmt.Println(fmt.Sprintf("exporting %s to %s", apiResponse.Spec.Name, baseDir)) - - err = files.CreateDir(baseDir) - if err != nil { - exit.Error(err) - } + fmt.Println(fmt.Sprintf("exporting %s to %s", apiResponse.Spec.Name, specFilePath)) yamlBytes, err := yaml.Marshal(apiResponse.Spec.API.SubmittedAPISpec) if err != nil { exit.Error(err) } - err = files.WriteFile(yamlBytes, path.Join(baseDir, apiResponse.Spec.FileName)) + err = files.WriteFile(yamlBytes, specFilePath) if err != nil { exit.Error(err) } diff --git a/cli/cmd/patch.go b/cli/cmd/patch.go deleted file mode 100644 index 398ebe2d13..0000000000 --- a/cli/cmd/patch.go +++ /dev/null @@ -1,93 +0,0 @@ -/* -Copyright 2021 Cortex Labs, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package cmd - -import ( - "fmt" - "strings" - - "github.com/cortexlabs/cortex/cli/cluster" - "github.com/cortexlabs/cortex/cli/types/flags" - "github.com/cortexlabs/cortex/pkg/lib/exit" - libjson "github.com/cortexlabs/cortex/pkg/lib/json" - "github.com/cortexlabs/cortex/pkg/lib/print" - "github.com/cortexlabs/cortex/pkg/lib/telemetry" - "github.com/spf13/cobra" -) - -var ( - _flagPatchEnv string - _flagPatchForce bool -) - -func patchInit() { - _patchCmd.Flags().SortFlags = false - _patchCmd.Flags().StringVarP(&_flagPatchEnv, "env", "e", "", "environment to use") - _patchCmd.Flags().BoolVarP(&_flagPatchForce, "force", "f", false, "override the in-progress api update") - _patchCmd.Flags().VarP(&_flagOutput, "output", "o", fmt.Sprintf("output format: one of %s", strings.Join(flags.UserOutputTypeStrings(), "|"))) -} - -var _patchCmd = &cobra.Command{ - Use: "patch [CONFIG_FILE]", - Short: "update API configuration for a deployed API", - Args: cobra.RangeArgs(0, 1), - Run: func(cmd *cobra.Command, args []string) { - envName, err := getEnvFromFlag(_flagPatchEnv) - if err != nil { - telemetry.Event("cli.patch") - exit.Error(err) - } - - env, err := ReadOrConfigureEnv(envName) - if err != nil { - telemetry.Event("cli.patch") - exit.Error(err) - } - telemetry.Event("cli.patch", map[string]interface{}{"env_name": env.Name}) - - err = printEnvIfNotSpecified(env.Name, cmd) - if err != nil { - exit.Error(err) - } - - configPath := getConfigPath(args) - - deployResults, err := cluster.Patch(MustGetOperatorConfig(env.Name), configPath, _flagPatchForce) - if err != nil { - exit.Error(err) - } - - switch _flagOutput { - case flags.JSONOutputType: - bytes, err := libjson.Marshal(deployResults) - if err != nil { - exit.Error(err) - } - fmt.Print(string(bytes)) - case flags.PrettyOutputType: - message, err := deployMessage(deployResults, env.Name) - if err != nil { - exit.Error(err) - } - if didAnyResultsError(deployResults) { - print.StderrBoldFirstBlock(message) - } else { - print.BoldFirstBlock(message) - } - } - }, -} diff --git a/cli/cmd/root.go b/cli/cmd/root.go index bee317fabf..2cd18267db 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -116,7 +116,6 @@ func init() { envInit() getInit() logsInit() - patchInit() refreshInit() versionInit() } @@ -155,7 +154,6 @@ func Execute() { _rootCmd.AddCommand(_deployCmd) _rootCmd.AddCommand(_getCmd) - _rootCmd.AddCommand(_patchCmd) _rootCmd.AddCommand(_logsCmd) _rootCmd.AddCommand(_refreshCmd) _rootCmd.AddCommand(_deleteCmd) diff --git a/cmd/operator/main.go b/cmd/operator/main.go index 50a7b65110..0b40de677c 100644 --- a/cmd/operator/main.go +++ b/cmd/operator/main.go @@ -117,7 +117,6 @@ func main() { routerWithAuth.HandleFunc("/info", endpoints.Info).Methods("GET") routerWithAuth.HandleFunc("/deploy", endpoints.Deploy).Methods("POST") - routerWithAuth.HandleFunc("/patch", endpoints.Patch).Methods("POST") routerWithAuth.HandleFunc("/refresh/{apiName}", endpoints.Refresh).Methods("POST") routerWithAuth.HandleFunc("/delete/{apiName}", endpoints.Delete).Methods("DELETE") routerWithAuth.HandleFunc("/get", endpoints.GetAPIs).Methods("GET") diff --git a/dev/generate_cli_md.sh b/dev/generate_cli_md.sh index 2d49524e9d..63f9f40d0d 100755 --- a/dev/generate_cli_md.sh +++ b/dev/generate_cli_md.sh @@ -34,10 +34,8 @@ commands=( "deploy" "get" "logs" - "patch" "refresh" "delete" - "prepare-debug" "cluster up" "cluster info" "cluster scale" diff --git a/dev/generate_python_client_md.sh b/dev/generate_python_client_md.sh index 0b0215e76f..d6ec879de2 100755 --- a/dev/generate_python_client_md.sh +++ b/dev/generate_python_client_md.sh @@ -65,11 +65,7 @@ truncate -s -1 $docs_path # Cortex version comment sed -i "s/^## deploy$/## deploy\n\n/g" $docs_path -sed -i "s/^## deploy\\\_realtime\\\_api$/## deploy\\\_realtime\\\_api\n\n/g" $docs_path -sed -i "s/^## deploy\\\_async\\\_api$/## deploy\\\_async\\\_api\n\n/g" $docs_path -sed -i "s/^## deploy\\\_batch\\\_api$/## deploy\\\_batch\\\_api\n\n/g" $docs_path -sed -i "s/^## deploy\\\_task\\\_api$/## deploy\\\_task\\\_api\n\n/g" $docs_path -sed -i "s/^## deploy\\\_traffic\\\_splitter$/## deploy\\\_traffic\\\_splitter\n\n/g" $docs_path +sed -i "s/^## deploy\\\_from\\\_file$/## deploy\\\_from\\\_file\n\n/g" $docs_path pip3 uninstall -y cortex rm -rf $ROOT/python/client/cortex.egg-info diff --git a/docs/clients/cli.md b/docs/clients/cli.md index 7179115a3b..faae5c0be9 100644 --- a/docs/clients/cli.md +++ b/docs/clients/cli.md @@ -46,21 +46,6 @@ Flags: -h, --help help for logs ``` -## patch - -```text -update API configuration for a deployed API - -Usage: - cortex patch [CONFIG_FILE] [flags] - -Flags: - -e, --env string environment to use - -f, --force override the in-progress api update - -o, --output string output format: one of pretty|json (default "pretty") - -h, --help help for patch -``` - ## refresh ```text @@ -92,18 +77,6 @@ Flags: -h, --help help for delete ``` -## prepare-debug - -```text -prepare artifacts to debug containers - -Usage: - cortex prepare-debug CONFIG_FILE [API_NAME] [flags] - -Flags: - -h, --help help for prepare-debug -``` - ## cluster up ```text @@ -175,10 +148,10 @@ Flags: ## cluster export ```text -download the code and configuration for APIs +download the configurations for all APIs Usage: - cortex cluster export [API_NAME] [API_ID] [flags] + cortex cluster export [flags] Flags: -c, --config string path to a cluster configuration file diff --git a/docs/clients/python.md b/docs/clients/python.md index a12f1d4ba8..2e7cb704ad 100644 --- a/docs/clients/python.md +++ b/docs/clients/python.md @@ -7,16 +7,11 @@ * [env\_delete](#env_delete) * [cortex.client.Client](#cortex-client-client) * [deploy](#deploy) - * [deploy\_realtime\_api](#deploy_realtime_api) - * [deploy\_async\_api](#deploy_async_api) - * [deploy\_batch\_api](#deploy_batch_api) - * [deploy\_task\_api](#deploy_task_api) - * [deploy\_traffic\_splitter](#deploy_traffic_splitter) + * [deploy\_from\_file](#deploy_from_file) * [get\_api](#get_api) * [list\_apis](#list_apis) * [get\_job](#get_job) * [refresh](#refresh) - * [patch](#patch) * [delete](#delete) * [stop\_job](#stop_job) * [stream\_api\_logs](#stream_api_logs) @@ -86,129 +81,39 @@ Delete an environment configured on this machine. ```python - | deploy(api_spec: Dict[str, Any], project_dir: str, force: bool = True, wait: bool = False) + | deploy(api_spec: Dict[str, Any], force: bool = True, wait: bool = False) ``` -Deploy API(s) from a project directory. +Deploy or update an API. **Arguments**: - `api_spec` - A dictionary defining a single Cortex API. See https://docs.cortex.dev/v/master/ for schema. -- `project_dir` - Path to a python project. - `force` - Override any in-progress api updates. -- `wait` - Streams logs until the APIs are ready. +- `wait` - Streams logs until the API is ready. **Returns**: Deployment status, API specification, and endpoint for each API. -## deploy\_realtime\_api +## deploy\_from\_file ```python - | deploy_realtime_api(api_spec: Dict[str, Any], handler, requirements: Optional[List] = None, conda_packages: Optional[List] = None, force: bool = True, wait: bool = False) -> Dict + | deploy_from_file(config_file: str, force: bool = False, wait: bool = False) -> Dict ``` -Deploy a Realtime API. +Deploy or update APIs specified in a configuration file. **Arguments**: -- `api_spec` - A dictionary defining a single Cortex API. See https://docs.cortex.dev/v/master/workloads/realtime-apis/configuration for schema. -- `handler` - A Cortex Handler class implementation. -- `requirements` - A list of PyPI dependencies that will be installed before the handler class implementation is invoked. -- `conda_packages` - A list of Conda dependencies that will be installed before the handler class implementation is invoked. +- `config_file` - Local path to a yaml file defining Cortex API(s). See https://docs.cortex.dev/v/master/ for schema. - `force` - Override any in-progress api updates. - `wait` - Streams logs until the APIs are ready. -**Returns**: - - Deployment status, API specification, and endpoint for each API. - -## deploy\_async\_api - - - -```python - | deploy_async_api(api_spec: Dict[str, Any], handler, requirements: Optional[List] = None, conda_packages: Optional[List] = None, force: bool = True) -> Dict -``` - -Deploy an Async API. - -**Arguments**: - -- `api_spec` - A dictionary defining a single Cortex API. See https://docs.cortex.dev/v/master/workloads/async-apis/configuration for schema. -- `handler` - A Cortex Handler class implementation. -- `requirements` - A list of PyPI dependencies that will be installed before the handler class implementation is invoked. -- `conda_packages` - A list of Conda dependencies that will be installed before the handler class implementation is invoked. -- `force` - Override any in-progress api updates. - - -**Returns**: - - Deployment status, API specification, and endpoint for each API. - -## deploy\_batch\_api - - - -```python - | deploy_batch_api(api_spec: Dict[str, Any], handler, requirements: Optional[List] = None, conda_packages: Optional[List] = None) -> Dict -``` - -Deploy a Batch API. - -**Arguments**: - -- `api_spec` - A dictionary defining a single Cortex API. See https://docs.cortex.dev/v/master/workloads/batch-apis/configuration for schema. -- `handler` - A Cortex Handler class implementation. -- `requirements` - A list of PyPI dependencies that will be installed before the handler class implementation is invoked. -- `conda_packages` - A list of Conda dependencies that will be installed before the handler class implementation is invoked. - - -**Returns**: - - Deployment status, API specification, and endpoint for each API. - -## deploy\_task\_api - - - -```python - | deploy_task_api(api_spec: Dict[str, Any], task, requirements: Optional[List] = None, conda_packages: Optional[List] = None) -> Dict -``` - -Deploy a Task API. - -**Arguments**: - -- `api_spec` - A dictionary defining a single Cortex API. See https://docs.cortex.dev/v/master/workloads/task-apis/configuration for schema. -- `task` - A callable class implementation. -- `requirements` - A list of PyPI dependencies that will be installed before the handler class implementation is invoked. -- `conda_packages` - A list of Conda dependencies that will be installed before the handler class implementation is invoked. - - -**Returns**: - - Deployment status, API specification, and endpoint for each API. - -## deploy\_traffic\_splitter - - - -```python - | deploy_traffic_splitter(api_spec: Dict[str, Any]) -> Dict -``` - -Deploy a Task API. - -**Arguments**: - -- `api_spec` - A dictionary defining a single Cortex API. See https://docs.cortex.dev/v/master/workloads/realtime-apis/traffic-splitter/configuration for schema. - - **Returns**: Deployment status, API specification, and endpoint for each API. @@ -273,19 +178,6 @@ Restart all of the replicas for a Realtime API without downtime. - `api_name` - Name of the API to refresh. - `force` - Override an already in-progress API update. -## patch - -```python - | patch(api_spec: Dict, force: bool = False) -> Dict -``` - -Update the api specification for an API that has already been deployed. - -**Arguments**: - -- `api_spec` - The new api specification to apply -- `force` - Override an already in-progress API update. - ## delete ```python diff --git a/docs/clusters/management/update.md b/docs/clusters/management/update.md index 6b1f72afba..f881ae3ff5 100644 --- a/docs/clusters/management/update.md +++ b/docs/clusters/management/update.md @@ -27,7 +27,7 @@ cortex cluster up cluster.yaml In production environments, you can upgrade your cluster without downtime if you have a backend service or DNS in front of your Cortex cluster: 1. Spin up a new cluster. For example: `cortex cluster up new-cluster.yaml --configure-env cortex2` (this will create a CLI environment named `cortex2` for accessing the new cluster). -1. Re-deploy your APIs in your new cluster. For example, if the name of your CLI environment for your existing cluster is `cortex`, you can use `cortex get --env cortex` to list all running APIs in your cluster, and re-deploy them in the new cluster by changing directories to each API's project folder and running `cortex deploy --env cortex2`. Alternatively, you can run `cortex cluster export --name --region ` to export all of your APIs (including configuration and application code), change directories into each API/ID subfolder that was exported, and run `cortex deploy --env cortex2`. +1. Re-deploy your APIs in your new cluster. For example, if the name of your CLI environment for your existing cluster is `cortex`, you can use `cortex get --env cortex` to list all running APIs in your cluster, and re-deploy them in the new cluster by changing directories to each API's project folder and running `cortex deploy --env cortex2`. Alternatively, you can run `cortex cluster export --name --region ` to export all of your API specifications, change directories the folder that was exported, and run `cortex deploy --env cortex2 ` for each API that you want to deploy in the new cluster. 1. Route requests to your new cluster. * If you are using a custom domain: update the A record in your Route 53 hosted zone to point to your new cluster's API load balancer. * If you have a backend service which makes requests to Cortex: update your backend service to make requests to the new cluster's endpoints. diff --git a/docs/summary.md b/docs/summary.md index 0f58863d0c..f6f5d1db0c 100644 --- a/docs/summary.md +++ b/docs/summary.md @@ -75,7 +75,6 @@ * [Python packages](workloads/dependencies/python-packages.md) * [System packages](workloads/dependencies/system-packages.md) * [Custom images](workloads/dependencies/images.md) -* [Debugging](workloads/debugging.md) ## Clients diff --git a/docs/workloads/debugging.md b/docs/workloads/debugging.md deleted file mode 100644 index b03af8f971..0000000000 --- a/docs/workloads/debugging.md +++ /dev/null @@ -1,25 +0,0 @@ -# Debugging - -You can test and debug your handler implementation and image by running your API container locally. - -The `cortex prepare-debug` command will generate a debugging configuration file named `.debug.json` based on your api spec, and it will print out a corresponding `docker run` command that can be used to run the container locally. - -For example: - - - -```bash -cortex prepare-debug cortex.yaml iris-classifier - -> docker run -p 9000:8888 \ -> -e "CORTEX_VERSION=0.35.0" \ -> -e "CORTEX_API_SPEC=/mnt/project/iris-classifier.debug.json" \ -> -v /home/ubuntu/iris-classifier:/mnt/project \ -> quay.io/cortexlabs/python-handler-cpu:0.35.0 -``` - -Make a request to the api container: - -```bash -curl localhost:9000 -X POST -H "Content-Type: application/json" -d @sample.json -``` diff --git a/docs/workloads/realtime/traffic-splitter/example.md b/docs/workloads/realtime/traffic-splitter/example.md index baa9d72130..bb4fa6d28b 100644 --- a/docs/workloads/realtime/traffic-splitter/example.md +++ b/docs/workloads/realtime/traffic-splitter/example.md @@ -50,7 +50,7 @@ traffic_splitter_spec = { ], } -cx.deploy_traffic_splitter(traffic_splitter_spec) +cx.deploy(traffic_splitter_spec) ``` ## Update the weights of the traffic splitter @@ -62,5 +62,5 @@ traffic_splitter_spec = cx.get_api("text-generator")["spec"]["submitted_api_spec traffic_splitter_spec["apis"][0]["weight"] = 1 traffic_splitter_spec["apis"][1]["weight"] = 99 -cx.patch(traffic_splitter_spec) +cx.deploy(traffic_splitter_spec) ``` diff --git a/pkg/operator/endpoints/patch.go b/pkg/operator/endpoints/patch.go deleted file mode 100644 index 7f0dd32e0f..0000000000 --- a/pkg/operator/endpoints/patch.go +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright 2021 Cortex Labs, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package endpoints - -import ( - "io/ioutil" - "net/http" - - "github.com/cortexlabs/cortex/pkg/lib/errors" - "github.com/cortexlabs/cortex/pkg/operator/resources" -) - -func Patch(w http.ResponseWriter, r *http.Request) { - force := getOptionalBoolQParam("force", false, r) - - configFileName, err := getRequiredQueryParam("configFileName", r) - if err != nil { - respondError(w, r, errors.WithStack(err)) - return - } - - rw := http.MaxBytesReader(w, r.Body, 10<<20) - - bodyBytes, err := ioutil.ReadAll(rw) - if err != nil { - respondError(w, r, err) - return - } - - response, err := resources.Patch(bodyBytes, configFileName, force) - if err != nil { - respondError(w, r, err) - return - } - - respond(w, response) -} diff --git a/pkg/operator/resources/resources.go b/pkg/operator/resources/resources.go index ef6eb5482d..918174a1d6 100644 --- a/pkg/operator/resources/resources.go +++ b/pkg/operator/resources/resources.go @@ -169,76 +169,6 @@ func UpdateAPI(apiConfig *userconfig.API, projectID string, force bool) (*schema return nil, msg, err } -func Patch(configBytes []byte, configFileName string, force bool) ([]schema.DeployResult, error) { - apiConfigs, err := spec.ExtractAPIConfigs(configBytes, configFileName) - if err != nil { - return nil, err - } - - results := make([]schema.DeployResult, 0, len(apiConfigs)) - for i := range apiConfigs { - apiConfig := &apiConfigs[i] - result := schema.DeployResult{} - - apiSpec, msg, err := patchAPI(apiConfig, force) - if err == nil && apiSpec != nil { - apiEndpoint, _ := operator.APIEndpoint(apiSpec) - - result.API = &schema.APIResponse{ - Spec: *apiSpec, - Endpoint: apiEndpoint, - } - } - - result.Message = msg - if err != nil { - result.Error = errors.ErrorStr(err) - } - - results = append(results, result) - } - return results, nil -} - -func patchAPI(apiConfig *userconfig.API, force bool) (*spec.API, string, error) { - deployedResource, err := GetDeployedResourceByName(apiConfig.Name) - if err != nil { - return nil, "", err - } - - if deployedResource.Kind == userconfig.UnknownKind { - return nil, "", ErrorOperationIsOnlySupportedForKind(*deployedResource, userconfig.RealtimeAPIKind, userconfig.AsyncAPIKind, userconfig.BatchAPIKind, userconfig.TaskAPIKind, userconfig.TrafficSplitterKind) // unexpected - } - - if deployedResource.Kind != apiConfig.Kind { - return nil, "", ErrorCannotChangeKindOfDeployedAPI(apiConfig.Name, apiConfig.Kind, deployedResource.Kind) - } - - prevAPISpec, err := operator.DownloadAPISpec(deployedResource.Name, deployedResource.ID()) - if err != nil { - return nil, "", err - } - - err = ValidateClusterAPIs([]userconfig.API{*apiConfig}) - if err != nil { - err = errors.Append(err, fmt.Sprintf("\n\napi configuration schema can be found at https://docs.cortex.dev/v/%s/", consts.CortexVersionMinor)) - return nil, "", err - } - - switch deployedResource.Kind { - case userconfig.RealtimeAPIKind: - return realtimeapi.UpdateAPI(apiConfig, prevAPISpec.ProjectID, force) - case userconfig.BatchAPIKind: - return batchapi.UpdateAPI(apiConfig, prevAPISpec.ProjectID) - case userconfig.TaskAPIKind: - return taskapi.UpdateAPI(apiConfig, prevAPISpec.ProjectID) - case userconfig.AsyncAPIKind: - return asyncapi.UpdateAPI(*apiConfig, prevAPISpec.ProjectID, force) - default: - return trafficsplitter.UpdateAPI(apiConfig) - } -} - func RefreshAPI(apiName string, force bool) (string, error) { deployedResource, err := GetDeployedResourceByName(apiName) if err != nil { diff --git a/python/client/cortex/client.py b/python/client/cortex/client.py index 22e5bcf333..17c3f4366b 100644 --- a/python/client/cortex/client.py +++ b/python/client/cortex/client.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import inspect import json import os import shutil @@ -20,18 +19,13 @@ import sys import threading import time -import uuid +import yaml from pathlib import Path from typing import Optional, List, Dict, Any -import dill -import yaml - from cortex import util from cortex.binary import run_cli, get_cli_path -from cortex.consts import EXPECTED_PYTHON_VERSION from cortex.telemetry import sentry_wrapper -from cortex.exceptions import InvalidKindForMethod class Client: @@ -48,368 +42,49 @@ def __init__(self, env: Dict): self.env_name = env["name"] # CORTEX_VERSION_MINOR + @sentry_wrapper def deploy( self, api_spec: Dict[str, Any], - project_dir: str, force: bool = True, wait: bool = False, ): """ - Deploy API(s) from a project directory. + Deploy or update an API. Args: api_spec: A dictionary defining a single Cortex API. See https://docs.cortex.dev/v/master/ for schema. - project_dir: Path to a python project. - force: Override any in-progress api updates. - wait: Streams logs until the APIs are ready. - - Returns: - Deployment status, API specification, and endpoint for each API. - """ - return self._create_api( - api_spec=api_spec, - project_dir=project_dir, - force=force, - wait=wait, - ) - - # CORTEX_VERSION_MINOR - def deploy_realtime_api( - self, - api_spec: Dict[str, Any], - handler, - requirements: Optional[List] = None, - conda_packages: Optional[List] = None, - force: bool = True, - wait: bool = False, - ) -> Dict: - """ - Deploy a Realtime API. - - Args: - api_spec: A dictionary defining a single Cortex API. See https://docs.cortex.dev/v/master/workloads/realtime-apis/configuration for schema. - handler: A Cortex Handler class implementation. - requirements: A list of PyPI dependencies that will be installed before the handler class implementation is invoked. - conda_packages: A list of Conda dependencies that will be installed before the handler class implementation is invoked. - force: Override any in-progress api updates. - wait: Streams logs until the APIs are ready. - - Returns: - Deployment status, API specification, and endpoint for each API. - """ - kind = api_spec.get("kind") - if kind != "RealtimeAPI": - raise InvalidKindForMethod( - f"expected an api_spec with kind 'RealtimeAPI', got kind '{kind}' instead" - ) - - return self._create_api( - api_spec=api_spec, - handler=handler, - requirements=requirements, - conda_packages=conda_packages, - force=force, - wait=wait, - ) - - # CORTEX_VERSION_MINOR - def deploy_async_api( - self, - api_spec: Dict[str, Any], - handler, - requirements: Optional[List] = None, - conda_packages: Optional[List] = None, - force: bool = True, - ) -> Dict: - """ - Deploy an Async API. - - Args: - api_spec: A dictionary defining a single Cortex API. See https://docs.cortex.dev/v/master/workloads/async-apis/configuration for schema. - handler: A Cortex Handler class implementation. - requirements: A list of PyPI dependencies that will be installed before the handler class implementation is invoked. - conda_packages: A list of Conda dependencies that will be installed before the handler class implementation is invoked. force: Override any in-progress api updates. + wait: Streams logs until the API is ready. Returns: Deployment status, API specification, and endpoint for each API. """ - kind = api_spec.get("kind") - if kind != "AsyncAPI": - raise InvalidKindForMethod( - f"expected an api_spec with kind 'AsyncAPI', got kind '{kind}' instead" - ) - - return self._create_api( - api_spec=api_spec, - handler=handler, - requirements=requirements, - conda_packages=conda_packages, - force=force, - ) - - # CORTEX_VERSION_MINOR - def deploy_batch_api( - self, - api_spec: Dict[str, Any], - handler, - requirements: Optional[List] = None, - conda_packages: Optional[List] = None, - ) -> Dict: - """ - Deploy a Batch API. - - Args: - api_spec: A dictionary defining a single Cortex API. See https://docs.cortex.dev/v/master/workloads/batch-apis/configuration for schema. - handler: A Cortex Handler class implementation. - requirements: A list of PyPI dependencies that will be installed before the handler class implementation is invoked. - conda_packages: A list of Conda dependencies that will be installed before the handler class implementation is invoked. - - Returns: - Deployment status, API specification, and endpoint for each API. - """ - - kind = api_spec.get("kind") - if kind != "BatchAPI": - raise InvalidKindForMethod( - f"expected an api_spec with kind 'BatchAPI', got kind '{kind}' instead" - ) - - return self._create_api( - api_spec=api_spec, - handler=handler, - requirements=requirements, - conda_packages=conda_packages, - ) - - # CORTEX_VERSION_MINOR - def deploy_task_api( - self, - api_spec: Dict[str, Any], - task, - requirements: Optional[List] = None, - conda_packages: Optional[List] = None, - ) -> Dict: - """ - Deploy a Task API. - - Args: - api_spec: A dictionary defining a single Cortex API. See https://docs.cortex.dev/v/master/workloads/task-apis/configuration for schema. - task: A callable class implementation. - requirements: A list of PyPI dependencies that will be installed before the handler class implementation is invoked. - conda_packages: A list of Conda dependencies that will be installed before the handler class implementation is invoked. - - Returns: - Deployment status, API specification, and endpoint for each API. - """ - kind = api_spec.get("kind") - if kind != "TaskAPI": - raise InvalidKindForMethod( - f"expected an api_spec with kind 'TaskAPI', got kind '{kind}' instead" - ) - - return self._create_api( - api_spec=api_spec, - task=task, - requirements=requirements, - conda_packages=conda_packages, - ) - # CORTEX_VERSION_MINOR - def deploy_traffic_splitter( - self, - api_spec: Dict[str, Any], - ) -> Dict: - """ - Deploy a Task API. + temp_deploy_dir = util.cli_config_dir() / "deployments" / api_spec["name"] + if temp_deploy_dir.exists(): + shutil.rmtree(str(temp_deploy_dir)) + temp_deploy_dir.mkdir(parents=True) - Args: - api_spec: A dictionary defining a single Cortex API. See https://docs.cortex.dev/v/master/workloads/realtime-apis/traffic-splitter/configuration for schema. + cortex_yaml_path = os.path.join(temp_deploy_dir, "cortex.yaml") - Returns: - Deployment status, API specification, and endpoint for each API. - """ - kind = api_spec.get("kind") - if kind != "TrafficSplitter": - raise InvalidKindForMethod( - f"expected an api_spec with kind 'TrafficSplitter', got kind '{kind}' instead" - ) - - return self._create_api( - api_spec=api_spec, - ) + with util.open_temporarily(cortex_yaml_path, "w", delete_parent_if_empty=True) as f: + yaml.dump([api_spec], f) # write a list + return self.deploy_from_file(cortex_yaml_path, force=force, wait=wait) # CORTEX_VERSION_MINOR @sentry_wrapper - def _create_api( - self, - api_spec: Dict, - handler=None, - task=None, - requirements: Optional[List] = None, - conda_packages: Optional[List] = None, - project_dir: Optional[str] = None, - force: bool = True, - wait: bool = False, - ) -> Dict: - """ - Deploy an API. - - Args: - api_spec: A dictionary defining a single Cortex API. See https://docs.cortex.dev/v/master/ for schema. - handler: A Cortex Handler class implementation. Not required for TaskAPI/TrafficSplitter kinds. - task: A callable class/function implementation. Not required for RealtimeAPI/BatchAPI/TrafficSplitter kinds. - requirements: A list of PyPI dependencies that will be installed before the handler class implementation is invoked. - conda_packages: A list of Conda dependencies that will be installed before the handler class implementation is invoked. - project_dir: Path to a python project. - force: Override any in-progress api updates. - wait: Streams logs until the APIs are ready. - - Returns: - Deployment status, API specification, and endpoint for each API. - """ - - requirements = requirements if requirements is not None else [] - conda_packages = conda_packages if conda_packages is not None else [] - - if project_dir is not None: - if handler is not None: - raise ValueError( - "`handler` and `project_dir` parameters cannot be specified at the same time, please choose one" - ) - if task is not None: - raise ValueError( - "`task` and `project_dir` parameters cannot be specified at the same time, please choose one" - ) - - if project_dir is not None: - cortex_yaml_path = os.path.join(project_dir, f".cortex-{uuid.uuid4()}.yaml") - - with util.open_temporarily(cortex_yaml_path, "w") as f: - yaml.dump([api_spec], f) # write a list - return self._deploy(cortex_yaml_path, force, wait) - - api_kind = api_spec.get("kind") - if api_kind == "TrafficSplitter": - if handler: - raise ValueError(f"`handler` parameter cannot be specified for {api_kind} kind") - if task: - raise ValueError(f"`task` parameter cannot be specified for {api_kind} kind") - elif api_kind == "TaskAPI": - if handler: - raise ValueError(f"`handler` parameter cannnot be specified for {api_kind} kind") - if task is None: - raise ValueError(f"`task` parameter must be specified for {api_kind} kind") - elif api_kind in ["BatchAPI", "RealtimeAPI"]: - if not handler: - raise ValueError(f"`handler` parameter must be specified for {api_kind}") - if task: - raise ValueError(f"`task` parameter cannot be specified for {api_kind}") - else: - raise ValueError( - f"invalid {api_kind} kind, `api_spec` must have the `kind` field set to one of the following kinds: " - f"{['TrafficSplitter', 'TaskAPI', 'BatchAPI', 'RealtimeAPI']}" - ) - - if api_spec.get("name") is None: - raise ValueError("`api_spec` must have the `name` key set") - - project_dir = util.cli_config_dir() / "deployments" / api_spec["name"] - - if project_dir.exists(): - shutil.rmtree(str(project_dir)) - - project_dir.mkdir(parents=True) - - cortex_yaml_path = os.path.join(project_dir, "cortex.yaml") - - if api_kind == "TrafficSplitter": - # for deploying a traffic splitter - with open(cortex_yaml_path, "w") as f: - yaml.dump([api_spec], f) # write a list - return self._deploy(cortex_yaml_path, force=force, wait=wait) - - actual_version = ( - f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" - ) - - if actual_version != EXPECTED_PYTHON_VERSION: - is_python_set = any( - conda_dep.startswith("python=") or "::python=" in conda_dep - for conda_dep in conda_packages - ) - - if not is_python_set: - conda_packages = [f"python={actual_version}", "pip=19.*"] + conda_packages - - if len(requirements) > 0: - with open(project_dir / "requirements.txt", "w") as requirements_file: - requirements_file.write("\n".join(requirements)) - - if len(conda_packages) > 0: - with open(project_dir / "conda-packages.txt", "w") as conda_file: - conda_file.write("\n".join(conda_packages)) - - if api_kind in ["BatchAPI", "RealtimeAPI"]: - if not inspect.isclass(handler): - raise ValueError("`handler` parameter must be a class definition") - - if api_spec.get("handler") is None: - raise ValueError("`api_spec` must have the `handler` section defined") - - if api_spec["handler"].get("type") is None: - raise ValueError( - "the `type` field in the `handler` section of the `api_spec` must be set (tensorflow or python)" - ) - - impl_rel_path = self._save_impl(handler, project_dir, "handler") - api_spec["handler"]["path"] = impl_rel_path - - if api_kind == "TaskAPI": - if not callable(task): - raise ValueError( - "`task` parameter must be a callable (e.g. a function definition or a class definition called " - "`Task` with a `__call__` method implemented " - ) - - impl_rel_path = self._save_impl(task, project_dir, "task") - if api_spec.get("definition") is None: - api_spec["definition"] = {} - api_spec["definition"]["path"] = impl_rel_path - - with open(cortex_yaml_path, "w") as f: - yaml.dump([api_spec], f) # write a list - return self._deploy(cortex_yaml_path, force=force, wait=wait) - - def _save_impl(self, impl, project_dir: Path, filename: str) -> str: - import __main__ as main - - is_interactive = not hasattr(main, "__file__") - - if is_interactive and impl.__module__ == "__main__": - # class is defined in a REPL (e.g. jupyter) - filename += ".pickle" - with open(project_dir / filename, "wb") as pickle_file: - dill.dump(impl, pickle_file) - return filename - - filename += ".py" - with open(project_dir / filename, "w") as f: - f.write(dill.source.importable(impl, source=True)) - return filename - - def _deploy( + def deploy_from_file( self, config_file: str, force: bool = False, wait: bool = False, ) -> Dict: """ - Deploy or update APIs specified in the config_file. + Deploy or update APIs specified in a configuration file. Args: - config_file: Local path to a yaml file defining Cortex APIs. + config_file: Local path to a yaml file defining Cortex API(s). See https://docs.cortex.dev/v/master/ for schema. force: Override any in-progress api updates. wait: Streams logs until the APIs are ready. @@ -541,29 +216,6 @@ def refresh(self, api_name: str, force: bool = False): run_cli(args, hide_output=True) - @sentry_wrapper - def patch(self, api_spec: Dict, force: bool = False) -> Dict: - """ - Update the api specification for an API that has already been deployed. - - Args: - api_spec: The new api specification to apply - force: Override an already in-progress API update. - """ - - cortex_yaml_file = ( - util.cli_config_dir() / "deployments" / f"cortex-{str(uuid.uuid4())}.yaml" - ) - with util.open_temporarily(cortex_yaml_file, "w") as f: - yaml.dump([api_spec], f) - args = ["patch", cortex_yaml_file, "--env", self.env_name, "-o", "json"] - - if force: - args.append("--force") - - output = run_cli(args, hide_output=True) - return json.loads(output.strip()) - @sentry_wrapper def delete(self, api_name: str, keep_cache: bool = False): """ diff --git a/python/client/cortex/consts.py b/python/client/cortex/consts.py index 316ec3f5b3..42c5ec9546 100644 --- a/python/client/cortex/consts.py +++ b/python/client/cortex/consts.py @@ -12,8 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Change if PYTHONVERSION changes -EXPECTED_PYTHON_VERSION = "3.6.9" CORTEX_VERSION = "master" # CORTEX_VERSION CORTEX_TELEMETRY_SENTRY_DSN = "https://5cea3d2d67194d028f7191fcc6ebca14@sentry.io/1825326" diff --git a/python/client/cortex/exceptions.py b/python/client/cortex/exceptions.py index 16acded963..5505156a65 100644 --- a/python/client/cortex/exceptions.py +++ b/python/client/cortex/exceptions.py @@ -35,11 +35,3 @@ class NotFound(CortexException): """ pass - - -class InvalidKindForMethod(CortexException): - """ - Raise when the specified resource kind is not supported by the used python client method. - """ - - pass diff --git a/python/client/cortex/util.py b/python/client/cortex/util.py index 1db3993ad0..af344ac39b 100644 --- a/python/client/cortex/util.py +++ b/python/client/cortex/util.py @@ -26,8 +26,10 @@ def cli_config_dir() -> Path: @contextmanager -def open_temporarily(path, mode): - Path(path).parent.mkdir(parents=True, exist_ok=True) +def open_temporarily(path, mode, delete_parent_if_empty: bool = False): + parentDir = Path(path).parent + parentDir.mkdir(parents=True, exist_ok=True) + file = open(path, mode) try: @@ -35,6 +37,8 @@ def open_temporarily(path, mode): finally: file.close() os.remove(path) + if delete_parent_if_empty and len(os.listdir(str(parentDir))) == 0: + shutil.rmtree(str(parentDir)) @contextmanager diff --git a/test/apis/pytorch/text-generator/deploy_class.py b/test/apis/pytorch/text-generator/deploy_class.py deleted file mode 100644 index 312d55d21e..0000000000 --- a/test/apis/pytorch/text-generator/deploy_class.py +++ /dev/null @@ -1,40 +0,0 @@ -import cortex -import os -import requests - -dir_path = os.path.dirname(os.path.realpath(__file__)) - -cx = cortex.client() - -api_spec = { - "name": "text-generator", - "kind": "RealtimeAPI", -} - - -class Handler: - def __init__(self, config): - from transformers import pipeline - - self.model = pipeline(task="text-generation") - - def handle_post(self, payload): - return self.model(payload["text"])[0] - - -api = cx.deploy_realtime_api( - api_spec, - handler=Handler, - requirements=["torch", "transformers"], - wait=True, -) - -response = requests.post( - api["endpoint"], - json={"text": "machine learning is great because"}, -) - -print(response.status_code) -print(response.text) - -cx.delete(api_spec["name"])