Skip to content

Show API history in get command, add export API ID command #1544

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Nov 23, 2020
14 changes: 14 additions & 0 deletions cli/cluster/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,20 @@ func GetAPI(operatorConfig OperatorConfig, apiName string) ([]schema.APIResponse
return apiRes, nil
}

func GetAPIByID(operatorConfig OperatorConfig, apiName string, apiID string) ([]schema.APIResponse, error) {
httpRes, err := HTTPGet(operatorConfig, "/get/"+apiName+"/"+apiID)
if err != nil {
return nil, err
}

var apiRes []schema.APIResponse
if err = json.Unmarshal(httpRes, &apiRes); err != nil {
return nil, errors.Wrap(err, "/get/"+apiName+"/"+apiID, string(httpRes))
}

return apiRes, nil
}

func GetJob(operatorConfig OperatorConfig, apiName string, jobID string) (schema.JobResponse, error) {
endpoint := path.Join("/batch", apiName)
httpRes, err := HTTPGet(operatorConfig, endpoint, map[string]string{"jobID": jobID})
Expand Down
36 changes: 24 additions & 12 deletions cli/cmd/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -591,9 +591,9 @@ var _downCmd = &cobra.Command{
}

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

Expand Down Expand Up @@ -649,14 +649,26 @@ var _exportCmd = &cobra.Command{
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 cluster named %s in %s", *accessConfig.ClusterName, *accessConfig.Region))
exit.Ok()
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)
}
}

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

for _, apiResponse := range apisResponse {
baseDir := filepath.Join(exportPath, apiResponse.Spec.Name)
baseDir := filepath.Join(exportPath, apiResponse.Spec.Name, apiResponse.Spec.ID)

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

Expand Down
21 changes: 21 additions & 0 deletions cli/cmd/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package cmd
import (
"fmt"
"strings"
"time"

"github.com/cortexlabs/cortex/cli/cluster"
"github.com/cortexlabs/cortex/cli/local"
Expand All @@ -28,10 +29,12 @@ import (
"github.com/cortexlabs/cortex/pkg/lib/errors"
"github.com/cortexlabs/cortex/pkg/lib/exit"
libjson "github.com/cortexlabs/cortex/pkg/lib/json"
"github.com/cortexlabs/cortex/pkg/lib/pointer"
"github.com/cortexlabs/cortex/pkg/lib/sets/strset"
s "github.com/cortexlabs/cortex/pkg/lib/strings"
"github.com/cortexlabs/cortex/pkg/lib/table"
"github.com/cortexlabs/cortex/pkg/lib/telemetry"
libtime "github.com/cortexlabs/cortex/pkg/lib/time"
"github.com/cortexlabs/cortex/pkg/operator/schema"
"github.com/cortexlabs/cortex/pkg/types"
"github.com/cortexlabs/cortex/pkg/types/userconfig"
Expand Down Expand Up @@ -63,6 +66,7 @@ func getInit() {
_getCmd.Flags().StringVarP(&_flagGetEnv, "env", "e", getDefaultEnv(_generalCommandType), "environment to use")
_getCmd.Flags().BoolVarP(&_flagWatch, "watch", "w", false, "re-run the command every 2 seconds")
_getCmd.Flags().VarP(&_flagOutput, "output", "o", fmt.Sprintf("output format: one of %s", strings.Join(flags.UserOutputTypeStrings(), "|")))
addVerboseFlag(_getCmd)
}

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

func apiHistoryTable(apiVersions []schema.APIVersion) string {
t := table.Table{
Headers: []table.Header{
{Title: "api id"},
{Title: "last deployed"},
},
}

t.Rows = make([][]interface{}, len(apiVersions))
for i, apiVersion := range apiVersions {
lastUpdated := time.Unix(apiVersion.LastUpdated, 0)
t.Rows[i] = []interface{}{apiVersion.APIID, libtime.SinceStr(&lastUpdated)}
}

return t.MustFormat(&table.Opts{Sort: pointer.Bool(false)})
}

func titleStr(title string) string {
return "\n" + console.Bold(title) + "\n"
}
11 changes: 9 additions & 2 deletions cli/cmd/lib_batch_apis.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,16 @@ func batchAPITable(batchAPI schema.APIResponse) string {
out += t.MustFormat()
}

out += "\n" + console.Bold("endpoint: ") + batchAPI.Endpoint
out += "\n" + console.Bold("endpoint: ") + batchAPI.Endpoint + "\n"

out += "\n" + apiHistoryTable(batchAPI.APIVersions)

if !_flagVerbose {
return out
}

out += titleStr("batch api configuration") + batchAPI.Spec.UserStr(types.AWSProviderType)

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

Expand Down
6 changes: 6 additions & 0 deletions cli/cmd/lib_realtime_apis.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ func realtimeAPITable(realtimeAPI schema.APIResponse, env cliconfig.Environment)
out += "\n" + describeModelInput(realtimeAPI.Status, realtimeAPI.Spec.Predictor, realtimeAPI.Endpoint)
}

out += "\n" + apiHistoryTable(realtimeAPI.APIVersions)

if !_flagVerbose {
return out, nil
}

out += titleStr("configuration") + strings.TrimSpace(realtimeAPI.Spec.UserStr(env.Provider))

return out, nil
Expand Down
6 changes: 6 additions & 0 deletions cli/cmd/lib_traffic_splitters.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ func trafficSplitterTable(trafficSplitter schema.APIResponse, env cliconfig.Envi
out += "\n" + console.Bold("endpoint: ") + trafficSplitter.Endpoint
out += fmt.Sprintf("\n%s curl %s -X POST -H \"Content-Type: application/json\" -d @sample.json\n", console.Bold("example curl:"), trafficSplitter.Endpoint)

out += "\n" + apiHistoryTable(trafficSplitter.APIVersions)

if !_flagVerbose {
return out, nil
}

out += titleStr("configuration") + strings.TrimSpace(trafficSplitter.Spec.UserStr(env.Provider))

return out, nil
Expand Down
5 changes: 5 additions & 0 deletions cli/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ var (
_cmdStr string

_configFileExts = []string{"yaml", "yml"}
_flagVerbose bool
_flagOutput = flags.PrettyOutputType

_credentialsCacheDir string
Expand Down Expand Up @@ -203,6 +204,10 @@ func updateRootUsage() {
})
}

func addVerboseFlag(cmd *cobra.Command) {
cmd.Flags().BoolVarP(&_flagVerbose, "verbose", "v", false, "show additional information (only applies to pretty output format)")
}

func wasEnvFlagProvided(cmd *cobra.Command) bool {
envFlagProvided := false
cmd.Flags().VisitAll(func(flag *pflag.Flag) {
Expand Down
5 changes: 3 additions & 2 deletions docs/miscellaneous/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ Flags:
-e, --env string environment to use (default "local")
-w, --watch re-run the command every 2 seconds
-o, --output string output format: one of pretty|json (default "pretty")
-v, --verbose show additional information (only applies to pretty output format)
-h, --help help for get
```

Expand Down Expand Up @@ -197,10 +198,10 @@ Flags:
### cluster export

```text
download the code and configuration for all APIs deployed in a cluster
download the code and configuration for APIs

Usage:
cortex cluster export [flags]
cortex cluster export [API_NAME] [API_ID] [flags]

Flags:
-c, --config string path to a cluster configuration file
Expand Down
27 changes: 26 additions & 1 deletion pkg/lib/aws/s3.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ func IsValidS3aPath(s3aPath string) bool {
// List all S3 objects that are "depth" levels or deeper than the given "s3Path".
// Setting depth to 1 effectively translates to listing the objects one level or deeper than the given prefix (aka listing the directory contents).
//
// 1st returned value is the list of paths found at level <depth>.
// 1st returned value is the list of paths found at level <depth> or deeper.
// 2nd returned value is the list of paths found at all levels.
func (c *Client) GetNLevelsDeepFromS3Path(s3Path string, depth int, includeDirObjects bool, maxResults *int64) ([]string, []string, error) {
paths := strset.New()
Expand Down Expand Up @@ -637,6 +637,31 @@ func (c *Client) ListS3PathDir(s3DirPath string, includeDirObjects bool, maxResu
return c.ListS3PathPrefix(s3Path, includeDirObjects, maxResults)
}

// This behaves like you'd expect `ls` to behave on a local file system
// "directory" names will be returned even if S3 directory objects don't exist
func (c *Client) ListS3DirOneLevel(bucket string, s3Dir string, maxResults *int64) ([]string, error) {
s3Dir = s.EnsureSuffix(s3Dir, "/")

allNames := strset.New()

err := c.S3Iterator(bucket, s3Dir, true, nil, func(object *s3.Object) (bool, error) {
relativePath := strings.TrimPrefix(*object.Key, s3Dir)
oneLevelPath := strings.Split(relativePath, "/")[0]
allNames.Add(oneLevelPath)

if maxResults != nil && int64(len(allNames)) >= *maxResults {
return false, nil
}
return true, nil
})

if err != nil {
return nil, errors.Wrap(err, S3Path(bucket, s3Dir))
}

return allNames.SliceSorted(), nil
}

func (c *Client) ListS3Prefix(bucket string, prefix string, includeDirObjects bool, maxResults *int64) ([]*s3.Object, error) {
var allObjects []*s3.Object

Expand Down
2 changes: 1 addition & 1 deletion pkg/lib/sets/strset/strset.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ func (s Set) Slice() []string {
return v
}

// List returns a sorted slice of all items.
// List returns a sorted slice of all items (a to z).
func (s Set) SliceSorted() []string {
v := s.Slice()
sort.Strings(v)
Expand Down
2 changes: 1 addition & 1 deletion pkg/lib/sets/strset/threadsafe/strset.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ func (s *Set) Slice() []string {
return s.s.Slice()
}

// List returns a sorted slice of all items.
// List returns a sorted slice of all items (a to z).
func (s *Set) SliceSorted() []string {
s.RLock()
defer s.RUnlock()
Expand Down
13 changes: 13 additions & 0 deletions pkg/operator/endpoints/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,16 @@ func GetAPI(w http.ResponseWriter, r *http.Request) {

respond(w, response)
}

func GetAPIByID(w http.ResponseWriter, r *http.Request) {
apiName := mux.Vars(r)["apiName"]
apiID := mux.Vars(r)["apiID"]

response, err := resources.GetAPIByID(apiName, apiID)
if err != nil {
respondError(w, r, err)
return
}

respond(w, response)
}
1 change: 1 addition & 0 deletions pkg/operator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ func main() {
routerWithAuth.HandleFunc("/delete/{apiName}", endpoints.Delete).Methods("DELETE")
routerWithAuth.HandleFunc("/get", endpoints.GetAPIs).Methods("GET")
routerWithAuth.HandleFunc("/get/{apiName}", endpoints.GetAPI).Methods("GET")
routerWithAuth.HandleFunc("/get/{apiName}/{apiID}", endpoints.GetAPIByID).Methods("GET")
routerWithAuth.HandleFunc("/logs/{apiName}", endpoints.ReadLogs)

log.Print("Running on port " + _operatorPortStr)
Expand Down
4 changes: 4 additions & 0 deletions pkg/operator/operator/deployed_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,7 @@ type DeployedResource struct {
userconfig.Resource
VirtualService *istioclientnetworking.VirtualService
}

func (deployedResourced *DeployedResource) ID() string {
return deployedResourced.VirtualService.Labels["apiID"]
}
8 changes: 8 additions & 0 deletions pkg/operator/resources/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
const (
ErrOperationIsOnlySupportedForKind = "resources.operation_is_only_supported_for_kind"
ErrAPINotDeployed = "resources.api_not_deployed"
ErrAPIIDNotFound = "resources.api_id_not_found"
ErrCannotChangeTypeOfDeployedAPI = "resources.cannot_change_kind_of_deployed_api"
ErrNoAvailableNodeComputeLimit = "resources.no_available_node_compute_limit"
ErrJobIDRequired = "resources.job_id_required"
Expand Down Expand Up @@ -58,6 +59,13 @@ func ErrorAPINotDeployed(apiName string) error {
})
}

func ErrorAPIIDNotFound(apiName string, apiID string) error {
return errors.WithStack(&errors.Error{
Kind: ErrAPIIDNotFound,
Message: fmt.Sprintf("%s with id %s has never been deployed", apiName, apiID),
})
}

func ErrorCannotChangeKindOfDeployedAPI(name string, newKind, prevKind userconfig.Kind) error {
return errors.WithStack(&errors.Error{
Kind: ErrCannotChangeTypeOfDeployedAPI,
Expand Down
Loading