Skip to content

Commit 5de83c7

Browse files
authored
Show API history in get command, add export API ID command (#1544)
1 parent 1139244 commit 5de83c7

File tree

18 files changed

+244
-22
lines changed

18 files changed

+244
-22
lines changed

cli/cluster/get.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,20 @@ func GetAPI(operatorConfig OperatorConfig, apiName string) ([]schema.APIResponse
5151
return apiRes, nil
5252
}
5353

54+
func GetAPIByID(operatorConfig OperatorConfig, apiName string, apiID string) ([]schema.APIResponse, error) {
55+
httpRes, err := HTTPGet(operatorConfig, "/get/"+apiName+"/"+apiID)
56+
if err != nil {
57+
return nil, err
58+
}
59+
60+
var apiRes []schema.APIResponse
61+
if err = json.Unmarshal(httpRes, &apiRes); err != nil {
62+
return nil, errors.Wrap(err, "/get/"+apiName+"/"+apiID, string(httpRes))
63+
}
64+
65+
return apiRes, nil
66+
}
67+
5468
func GetJob(operatorConfig OperatorConfig, apiName string, jobID string) (schema.JobResponse, error) {
5569
endpoint := path.Join("/batch", apiName)
5670
httpRes, err := HTTPGet(operatorConfig, endpoint, map[string]string{"jobID": jobID})

cli/cmd/cluster.go

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -591,9 +591,9 @@ var _downCmd = &cobra.Command{
591591
}
592592

593593
var _exportCmd = &cobra.Command{
594-
Use: "export",
595-
Short: "download the code and configuration for all APIs deployed in a cluster",
596-
Args: cobra.NoArgs,
594+
Use: "export [API_NAME] [API_ID]",
595+
Short: "download the code and configuration for APIs",
596+
Args: cobra.RangeArgs(0, 2),
597597
Run: func(cmd *cobra.Command, args []string) {
598598
telemetry.Event("cli.cluster.export")
599599

@@ -649,14 +649,26 @@ var _exportCmd = &cobra.Command{
649649
exit.Error(err)
650650
}
651651

652-
apisResponse, err := cluster.GetAPIs(operatorConfig)
653-
if err != nil {
654-
exit.Error(err)
655-
}
656-
657-
if len(apisResponse) == 0 {
658-
fmt.Println(fmt.Sprintf("no apis found in cluster named %s in %s", *accessConfig.ClusterName, *accessConfig.Region))
659-
exit.Ok()
652+
var apisResponse []schema.APIResponse
653+
if len(args) == 0 {
654+
apisResponse, err = cluster.GetAPIs(operatorConfig)
655+
if err != nil {
656+
exit.Error(err)
657+
}
658+
if len(apisResponse) == 0 {
659+
fmt.Println(fmt.Sprintf("no apis found in your cluster named %s in %s", *accessConfig.ClusterName, *accessConfig.Region))
660+
exit.Ok()
661+
}
662+
} else if len(args) == 1 {
663+
apisResponse, err = cluster.GetAPI(operatorConfig, args[0])
664+
if err != nil {
665+
exit.Error(err)
666+
}
667+
} else if len(args) == 2 {
668+
apisResponse, err = cluster.GetAPIByID(operatorConfig, args[0], args[1])
669+
if err != nil {
670+
exit.Error(err)
671+
}
660672
}
661673

662674
exportPath := fmt.Sprintf("export-%s-%s", *accessConfig.Region, *accessConfig.ClusterName)
@@ -667,7 +679,7 @@ var _exportCmd = &cobra.Command{
667679
}
668680

669681
for _, apiResponse := range apisResponse {
670-
baseDir := filepath.Join(exportPath, apiResponse.Spec.Name)
682+
baseDir := filepath.Join(exportPath, apiResponse.Spec.Name, apiResponse.Spec.ID)
671683

672684
fmt.Println(fmt.Sprintf("exporting %s to %s", apiResponse.Spec.Name, baseDir))
673685

cli/cmd/get.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package cmd
1919
import (
2020
"fmt"
2121
"strings"
22+
"time"
2223

2324
"github.com/cortexlabs/cortex/cli/cluster"
2425
"github.com/cortexlabs/cortex/cli/local"
@@ -28,10 +29,12 @@ import (
2829
"github.com/cortexlabs/cortex/pkg/lib/errors"
2930
"github.com/cortexlabs/cortex/pkg/lib/exit"
3031
libjson "github.com/cortexlabs/cortex/pkg/lib/json"
32+
"github.com/cortexlabs/cortex/pkg/lib/pointer"
3133
"github.com/cortexlabs/cortex/pkg/lib/sets/strset"
3234
s "github.com/cortexlabs/cortex/pkg/lib/strings"
3335
"github.com/cortexlabs/cortex/pkg/lib/table"
3436
"github.com/cortexlabs/cortex/pkg/lib/telemetry"
37+
libtime "github.com/cortexlabs/cortex/pkg/lib/time"
3538
"github.com/cortexlabs/cortex/pkg/operator/schema"
3639
"github.com/cortexlabs/cortex/pkg/types"
3740
"github.com/cortexlabs/cortex/pkg/types/userconfig"
@@ -63,6 +66,7 @@ func getInit() {
6366
_getCmd.Flags().StringVarP(&_flagGetEnv, "env", "e", getDefaultEnv(_generalCommandType), "environment to use")
6467
_getCmd.Flags().BoolVarP(&_flagWatch, "watch", "w", false, "re-run the command every 2 seconds")
6568
_getCmd.Flags().VarP(&_flagOutput, "output", "o", fmt.Sprintf("output format: one of %s", strings.Join(flags.UserOutputTypeStrings(), "|")))
69+
addVerboseFlag(_getCmd)
6670
}
6771

6872
var _getCmd = &cobra.Command{
@@ -498,6 +502,23 @@ func getAPI(env cliconfig.Environment, apiName string) (string, error) {
498502
return realtimeAPITable(apiRes, env)
499503
}
500504

505+
func apiHistoryTable(apiVersions []schema.APIVersion) string {
506+
t := table.Table{
507+
Headers: []table.Header{
508+
{Title: "api id"},
509+
{Title: "last deployed"},
510+
},
511+
}
512+
513+
t.Rows = make([][]interface{}, len(apiVersions))
514+
for i, apiVersion := range apiVersions {
515+
lastUpdated := time.Unix(apiVersion.LastUpdated, 0)
516+
t.Rows[i] = []interface{}{apiVersion.APIID, libtime.SinceStr(&lastUpdated)}
517+
}
518+
519+
return t.MustFormat(&table.Opts{Sort: pointer.Bool(false)})
520+
}
521+
501522
func titleStr(title string) string {
502523
return "\n" + console.Bold(title) + "\n"
503524
}

cli/cmd/lib_batch_apis.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,16 @@ func batchAPITable(batchAPI schema.APIResponse) string {
131131
out += t.MustFormat()
132132
}
133133

134-
out += "\n" + console.Bold("endpoint: ") + batchAPI.Endpoint
134+
out += "\n" + console.Bold("endpoint: ") + batchAPI.Endpoint + "\n"
135+
136+
out += "\n" + apiHistoryTable(batchAPI.APIVersions)
137+
138+
if !_flagVerbose {
139+
return out
140+
}
141+
142+
out += titleStr("batch api configuration") + batchAPI.Spec.UserStr(types.AWSProviderType)
135143

136-
out += "\n" + titleStr("batch api configuration") + batchAPI.Spec.UserStr(types.AWSProviderType)
137144
return out
138145
}
139146

cli/cmd/lib_realtime_apis.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ func realtimeAPITable(realtimeAPI schema.APIResponse, env cliconfig.Environment)
7474
out += "\n" + describeModelInput(realtimeAPI.Status, realtimeAPI.Spec.Predictor, realtimeAPI.Endpoint)
7575
}
7676

77+
out += "\n" + apiHistoryTable(realtimeAPI.APIVersions)
78+
79+
if !_flagVerbose {
80+
return out, nil
81+
}
82+
7783
out += titleStr("configuration") + strings.TrimSpace(realtimeAPI.Spec.UserStr(env.Provider))
7884

7985
return out, nil

cli/cmd/lib_traffic_splitters.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ func trafficSplitterTable(trafficSplitter schema.APIResponse, env cliconfig.Envi
5353
out += "\n" + console.Bold("endpoint: ") + trafficSplitter.Endpoint
5454
out += fmt.Sprintf("\n%s curl %s -X POST -H \"Content-Type: application/json\" -d @sample.json\n", console.Bold("example curl:"), trafficSplitter.Endpoint)
5555

56+
out += "\n" + apiHistoryTable(trafficSplitter.APIVersions)
57+
58+
if !_flagVerbose {
59+
return out, nil
60+
}
61+
5662
out += titleStr("configuration") + strings.TrimSpace(trafficSplitter.Spec.UserStr(env.Provider))
5763

5864
return out, nil

cli/cmd/root.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ var (
3939
_cmdStr string
4040

4141
_configFileExts = []string{"yaml", "yml"}
42+
_flagVerbose bool
4243
_flagOutput = flags.PrettyOutputType
4344

4445
_credentialsCacheDir string
@@ -203,6 +204,10 @@ func updateRootUsage() {
203204
})
204205
}
205206

207+
func addVerboseFlag(cmd *cobra.Command) {
208+
cmd.Flags().BoolVarP(&_flagVerbose, "verbose", "v", false, "show additional information (only applies to pretty output format)")
209+
}
210+
206211
func wasEnvFlagProvided(cmd *cobra.Command) bool {
207212
envFlagProvided := false
208213
cmd.Flags().VisitAll(func(flag *pflag.Flag) {

docs/miscellaneous/cli.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ Flags:
5858
-e, --env string environment to use (default "local")
5959
-w, --watch re-run the command every 2 seconds
6060
-o, --output string output format: one of pretty|json (default "pretty")
61+
-v, --verbose show additional information (only applies to pretty output format)
6162
-h, --help help for get
6263
```
6364

@@ -197,10 +198,10 @@ Flags:
197198
### cluster export
198199

199200
```text
200-
download the code and configuration for all APIs deployed in a cluster
201+
download the code and configuration for APIs
201202
202203
Usage:
203-
cortex cluster export [flags]
204+
cortex cluster export [API_NAME] [API_ID] [flags]
204205
205206
Flags:
206207
-c, --config string path to a cluster configuration file

pkg/lib/aws/s3.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ func IsValidS3aPath(s3aPath string) bool {
130130
// List all S3 objects that are "depth" levels or deeper than the given "s3Path".
131131
// Setting depth to 1 effectively translates to listing the objects one level or deeper than the given prefix (aka listing the directory contents).
132132
//
133-
// 1st returned value is the list of paths found at level <depth>.
133+
// 1st returned value is the list of paths found at level <depth> or deeper.
134134
// 2nd returned value is the list of paths found at all levels.
135135
func (c *Client) GetNLevelsDeepFromS3Path(s3Path string, depth int, includeDirObjects bool, maxResults *int64) ([]string, []string, error) {
136136
paths := strset.New()
@@ -637,6 +637,31 @@ func (c *Client) ListS3PathDir(s3DirPath string, includeDirObjects bool, maxResu
637637
return c.ListS3PathPrefix(s3Path, includeDirObjects, maxResults)
638638
}
639639

640+
// This behaves like you'd expect `ls` to behave on a local file system
641+
// "directory" names will be returned even if S3 directory objects don't exist
642+
func (c *Client) ListS3DirOneLevel(bucket string, s3Dir string, maxResults *int64) ([]string, error) {
643+
s3Dir = s.EnsureSuffix(s3Dir, "/")
644+
645+
allNames := strset.New()
646+
647+
err := c.S3Iterator(bucket, s3Dir, true, nil, func(object *s3.Object) (bool, error) {
648+
relativePath := strings.TrimPrefix(*object.Key, s3Dir)
649+
oneLevelPath := strings.Split(relativePath, "/")[0]
650+
allNames.Add(oneLevelPath)
651+
652+
if maxResults != nil && int64(len(allNames)) >= *maxResults {
653+
return false, nil
654+
}
655+
return true, nil
656+
})
657+
658+
if err != nil {
659+
return nil, errors.Wrap(err, S3Path(bucket, s3Dir))
660+
}
661+
662+
return allNames.SliceSorted(), nil
663+
}
664+
640665
func (c *Client) ListS3Prefix(bucket string, prefix string, includeDirObjects bool, maxResults *int64) ([]*s3.Object, error) {
641666
var allObjects []*s3.Object
642667

pkg/lib/sets/strset/strset.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ func (s Set) Slice() []string {
190190
return v
191191
}
192192

193-
// List returns a sorted slice of all items.
193+
// List returns a sorted slice of all items (a to z).
194194
func (s Set) SliceSorted() []string {
195195
v := s.Slice()
196196
sort.Strings(v)

0 commit comments

Comments
 (0)