Skip to content

Commit f9e6dc9

Browse files
authored
Debug API containers locally (#2112)
1 parent 21e5d34 commit f9e6dc9

File tree

19 files changed

+578
-539
lines changed

19 files changed

+578
-539
lines changed

cli/cmd/errors.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
s "github.com/cortexlabs/cortex/pkg/lib/strings"
2727
"github.com/cortexlabs/cortex/pkg/lib/urls"
2828
"github.com/cortexlabs/cortex/pkg/types/clusterconfig"
29+
"github.com/cortexlabs/cortex/pkg/types/userconfig"
2930
)
3031

3132
const (
@@ -67,6 +68,9 @@ const (
6768
ErrShellCompletionNotSupported = "cli.shell_completion_not_supported"
6869
ErrNoTerminalWidth = "cli.no_terminal_width"
6970
ErrDeployFromTopLevelDir = "cli.deploy_from_top_level_dir"
71+
ErrAPINameMustBeProvided = "cli.api_name_must_be_provided"
72+
ErrAPINotFoundInConfig = "cli.api_not_found_in_config"
73+
ErrNotSupportedForKindAndType = "cli.not_supported_for_kind_and_type"
7074
)
7175

7276
func ErrorInvalidProvider(providerStr, cliConfigPath string) error {
@@ -267,3 +271,28 @@ func ErrorDeployFromTopLevelDir(genericDirName string) error {
267271
Message: fmt.Sprintf("cannot deploy from your %s directory - when deploying your API, cortex sends all files in your project directory (i.e. the directory which contains cortex.yaml) to your cluster (see https://docs.cortex.dev/v/%s/); therefore it is recommended to create a subdirectory for your project files", genericDirName, consts.CortexVersionMinor),
268272
})
269273
}
274+
275+
func ErrorAPINameMustBeProvided() error {
276+
return errors.WithStack(&errors.Error{
277+
Kind: ErrAPINameMustBeProvided,
278+
Message: fmt.Sprintf("multiple apis listed; please specify the name of an api"),
279+
})
280+
}
281+
282+
func ErrorAPINotFoundInConfig(apiName string) error {
283+
return errors.WithStack(&errors.Error{
284+
Kind: ErrAPINotFoundInConfig,
285+
Message: fmt.Sprintf("api '%s' not found in config", apiName),
286+
})
287+
}
288+
289+
func ErrorNotSupportedForKindAndType(kind userconfig.Kind, predictorType userconfig.PredictorType) error {
290+
return errors.WithStack(&errors.Error{
291+
Kind: ErrNotSupportedForKindAndType,
292+
Message: fmt.Sprintf("this command is still in beta and currently only supports %s with type %s", userconfig.RealtimeAPIKind.String(), userconfig.PythonPredictorType.String()),
293+
Metadata: map[string]interface{}{
294+
"apiKind": kind.String(),
295+
"predictorType": predictorType.String(),
296+
},
297+
})
298+
}

cli/cmd/prepare_debug.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
Copyright 2021 Cortex Labs, Inc.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package cmd
18+
19+
import (
20+
"fmt"
21+
"path"
22+
23+
"github.com/cortexlabs/cortex/pkg/consts"
24+
"github.com/cortexlabs/cortex/pkg/lib/errors"
25+
"github.com/cortexlabs/cortex/pkg/lib/exit"
26+
"github.com/cortexlabs/cortex/pkg/lib/files"
27+
libjson "github.com/cortexlabs/cortex/pkg/lib/json"
28+
"github.com/cortexlabs/cortex/pkg/lib/telemetry"
29+
"github.com/cortexlabs/cortex/pkg/types/spec"
30+
"github.com/cortexlabs/cortex/pkg/types/userconfig"
31+
"github.com/spf13/cobra"
32+
)
33+
34+
func prepareDebugInit() {
35+
_prepareDebugCmd.Flags().SortFlags = false
36+
}
37+
38+
var _prepareDebugCmd = &cobra.Command{
39+
Use: "prepare-debug CONFIG_FILE [API_NAME]",
40+
Short: "prepare artifacts to debug containers",
41+
Args: cobra.RangeArgs(1, 2),
42+
Run: func(cmd *cobra.Command, args []string) {
43+
telemetry.Event("cli.prepare-debug")
44+
45+
configPath := args[0]
46+
47+
var apiName string
48+
if len(args) == 2 {
49+
apiName = args[1]
50+
}
51+
52+
configBytes, err := files.ReadFileBytes(configPath)
53+
if err != nil {
54+
exit.Error(err)
55+
}
56+
57+
projectRoot := files.Dir(files.UserRelToAbsPath(configPath))
58+
59+
apis, err := spec.ExtractAPIConfigs(configBytes, args[0])
60+
if err != nil {
61+
exit.Error(err)
62+
}
63+
64+
if apiName == "" && len(apis) > 1 {
65+
exit.Error(errors.Wrap(ErrorAPINameMustBeProvided(), configPath))
66+
}
67+
68+
var apiToPrepare userconfig.API
69+
if apiName == "" {
70+
apiToPrepare = apis[0]
71+
} else {
72+
found := false
73+
for i := range apis {
74+
api := apis[i]
75+
if api.Name == apiName {
76+
found = true
77+
apiToPrepare = api
78+
break
79+
}
80+
}
81+
if !found {
82+
exit.Error(errors.Wrap(ErrorAPINotFoundInConfig(apiName), configPath))
83+
}
84+
}
85+
86+
if apiToPrepare.Kind != userconfig.RealtimeAPIKind {
87+
exit.Error(ErrorNotSupportedForKindAndType(apiToPrepare.Kind, userconfig.UnknownPredictorType))
88+
}
89+
if apiToPrepare.Predictor.Type != userconfig.PythonPredictorType {
90+
exit.Error(ErrorNotSupportedForKindAndType(apiToPrepare.Kind, apiToPrepare.Predictor.Type))
91+
}
92+
93+
apiSpec := spec.API{
94+
API: &apiToPrepare,
95+
}
96+
97+
debugFileName := apiSpec.Name + ".debug.json"
98+
jsonStr, err := libjson.Pretty(apiSpec)
99+
if err != nil {
100+
exit.Error(err)
101+
}
102+
files.WriteFile([]byte(jsonStr), path.Join(projectRoot, debugFileName))
103+
104+
fmt.Println(fmt.Sprintf(
105+
`
106+
docker run -p 9000:8888 \
107+
-e "CORTEX_VERSION=%s" \
108+
-e "CORTEX_API_SPEC=/mnt/project/%s" \
109+
-v %s:/mnt/project \
110+
%s`, consts.CortexVersion, debugFileName, path.Clean(projectRoot), apiToPrepare.Predictor.Image))
111+
},
112+
}

cli/cmd/root.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ func init() {
112112
clusterInit()
113113
completionInit()
114114
deleteInit()
115+
prepareDebugInit()
115116
deployInit()
116117
envInit()
117118
getInit()
@@ -159,6 +160,7 @@ func Execute() {
159160
_rootCmd.AddCommand(_logsCmd)
160161
_rootCmd.AddCommand(_refreshCmd)
161162
_rootCmd.AddCommand(_deleteCmd)
163+
_rootCmd.AddCommand(_prepareDebugCmd)
162164

163165
_rootCmd.AddCommand(_clusterCmd)
164166

dev/generate_cli_md.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ commands=(
3737
"patch"
3838
"refresh"
3939
"delete"
40+
"prepare-debug"
4041
"cluster up"
4142
"cluster info"
4243
"cluster scale"

docs/clients/cli.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,18 @@ Flags:
9292
-h, --help help for delete
9393
```
9494

95+
## prepare-debug
96+
97+
```text
98+
prepare artifacts to debug containers
99+
100+
Usage:
101+
cortex prepare-debug CONFIG_FILE [API_NAME] [flags]
102+
103+
Flags:
104+
-h, --help help for prepare-debug
105+
```
106+
95107
## cluster up
96108

97109
```text

pkg/cortex/serve/cortex_internal/lib/api/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@
1313
# limitations under the License.
1414

1515
from cortex_internal.lib.api.predictor import Predictor
16-
from cortex_internal.lib.api.api import API, get_api, get_spec
16+
from cortex_internal.lib.api.api import API, get_api
1717
from cortex_internal.lib.api.task import TaskAPI

pkg/cortex/serve/cortex_internal/lib/api/api.py

Lines changed: 2 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -29,21 +29,17 @@
2929
class API:
3030
def __init__(
3131
self,
32-
storage: S3,
3332
api_spec: Dict[str, Any],
3433
model_dir: str,
3534
cache_dir: str = ".",
3635
):
37-
self.storage = storage
3836
self.api_spec = api_spec
3937
self.cache_dir = cache_dir
4038

4139
self.id = api_spec["id"]
4240
self.predictor_id = api_spec["predictor_id"]
4341
self.deployment_id = api_spec["deployment_id"]
4442

45-
self.key = api_spec["key"]
46-
self.metadata_root = api_spec["metadata_root"]
4743
self.name = api_spec["name"]
4844
self.predictor = Predictor(api_spec, model_dir)
4945

@@ -136,44 +132,14 @@ def get_api(
136132
spec_path: str,
137133
model_dir: str,
138134
cache_dir: str,
139-
region: str,
140135
) -> API:
141-
storage, raw_api_spec = get_spec(spec_path, cache_dir, region)
136+
with open(spec_path) as json_file:
137+
raw_api_spec = json.load(json_file)
142138

143139
api = API(
144-
storage=storage,
145140
api_spec=raw_api_spec,
146141
model_dir=model_dir,
147142
cache_dir=cache_dir,
148143
)
149144

150145
return api
151-
152-
153-
def get_spec(
154-
spec_path: str,
155-
cache_dir: str,
156-
region: str,
157-
spec_name: str = "api_spec.json",
158-
) -> Tuple[S3, dict]:
159-
"""
160-
Args:
161-
spec_path: Path to API spec (i.e. "s3://cortex-dev-0/apis/iris-classifier/api/69b93378fa5c0218-jy1fjtyihu-9fcc10739e7fc8050cefa8ca27ece1ee/master-spec.json").
162-
cache_dir: Local directory where the API spec gets saved to.
163-
region: Region of the bucket.
164-
spec_name: File name of the spec as it is saved on disk.
165-
"""
166-
167-
bucket, key = S3.deconstruct_s3_path(spec_path)
168-
storage = S3(bucket=bucket, region=region)
169-
170-
local_spec_path = os.path.join(cache_dir, spec_name)
171-
if not os.path.isfile(local_spec_path):
172-
storage.download_file(key, local_spec_path)
173-
174-
return storage, read_json(local_spec_path)
175-
176-
177-
def read_json(json_path: str):
178-
with open(json_path) as json_file:
179-
return json.load(json_file)

pkg/cortex/serve/cortex_internal/lib/telemetry.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,11 @@ def init_sentry(
5454
In addition to that, the user ID tag is added to every reported event.
5555
"""
5656

57-
if disabled is True or os.getenv("CORTEX_TELEMETRY_DISABLE", "").lower() == "true":
57+
if (
58+
disabled is True
59+
or os.getenv("CORTEX_TELEMETRY_DISABLE", "").lower() == "true"
60+
or os.getenv("CORTEX_DEBUGGING", "true").lower() == "true"
61+
):
5862
return
5963

6064
if dsn == "":

pkg/cortex/serve/cortex_internal/serve/serve.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ def start_fn():
284284
json.dump(used_ports, f)
285285
f.truncate()
286286

287-
api = get_api(spec_path, model_dir, cache_dir, region)
287+
api = get_api(spec_path, model_dir, cache_dir)
288288

289289
client = api.predictor.initialize_client(
290290
tf_serving_host=tf_serving_host, tf_serving_port=tf_serving_port

pkg/cortex/serve/init/bootloader.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ if [ "$CORTEX_VERSION" != "$EXPECTED_CORTEX_VERSION" ]; then
2424
exit 1
2525
fi
2626

27+
export CORTEX_DEBUGGING=${CORTEX_DEBUGGING:-"true"}
28+
29+
eval $(/opt/conda/envs/env/bin/python /src/cortex/serve/init/export_env_vars.py $CORTEX_API_SPEC)
30+
2731
function substitute_env_vars() {
2832
file_to_run_substitution=$1
2933
/opt/conda/envs/env/bin/python -c "from cortex_internal.lib import util; import os; util.expand_environment_vars_on_file('$file_to_run_substitution')"

0 commit comments

Comments
 (0)