diff --git a/cli/cmd/get.go b/cli/cmd/get.go index 691d849b5b..2149a39635 100644 --- a/cli/cmd/get.go +++ b/cli/cmd/get.go @@ -47,6 +47,7 @@ func init() { addWatchFlag(getCmd) addSummaryFlag(getCmd) addVerboseFlag(getCmd) + addAllDeploymentsFlag(getCmd) // addResourceTypesToHelp(getCmd) } @@ -63,6 +64,10 @@ var getCmd = &cobra.Command{ } func runGet(cmd *cobra.Command, args []string) (string, error) { + if flagAllDeployments || !IsAppNameSpecified() { + return getDeploymentsResponse() + } + resourcesRes, err := getResourcesResponse() if err != nil { return "", err @@ -109,6 +114,42 @@ func runGet(cmd *cobra.Command, args []string) (string, error) { return "", errors.New("too many args") // unexpected } +func getDeploymentsResponse() (string, error) { + httpResponse, err := HTTPGet("/deployments", map[string]string{}) + if err != nil { + return "", err + } + + var resourcesRes schema.GetDeploymentsResponse + if err = json.Unmarshal(httpResponse, &resourcesRes); err != nil { + return "", err + } + + if len(resourcesRes.Deployments) == 0 { + return "No deployments found", nil + } + + rows := make([][]interface{}, len(resourcesRes.Deployments)) + for idx, deployment := range resourcesRes.Deployments { + rows[idx] = []interface{}{ + deployment.Name, + deployment.Status.String(), + libtime.Since(&deployment.LastUpdated), + } + } + + t := table.Table{ + Headers: []table.Header{ + {Title: "NAME", MaxWidth: 32}, + {Title: "STATUS", MaxWidth: 21}, + {Title: "LAST UPDATED"}, + }, + Rows: rows, + } + + return table.MustFormat(t), nil +} + func getResourcesResponse() (*schema.GetResourcesResponse, error) { appName, err := AppNameFromFlagOrConfig() if err != nil { diff --git a/cli/cmd/lib_config_reader.go b/cli/cmd/lib_config_reader.go index 1eba1df173..efa8a08457 100644 --- a/cli/cmd/lib_config_reader.go +++ b/cli/cmd/lib_config_reader.go @@ -106,3 +106,7 @@ func AppNameFromFlagOrConfig() (string, error) { return appName, nil } + +func IsAppNameSpecified() bool { + return flagAppName != "" || appRootOrBlank() != "" +} diff --git a/cli/cmd/root.go b/cli/cmd/root.go index 2946f0f7e0..996878eeaa 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -40,6 +40,7 @@ var flagWatch bool var flagAppName string var flagVerbose bool var flagSummary bool +var flagAllDeployments bool var configFileExts = []string{"yaml", "yml"} @@ -106,6 +107,10 @@ func addSummaryFlag(cmd *cobra.Command) { cmd.PersistentFlags().BoolVarP(&flagSummary, "summary", "s", false, "show summarized output") } +func addAllDeploymentsFlag(cmd *cobra.Command) { + getCmd.PersistentFlags().BoolVarP(&flagAllDeployments, "all-deployments", "a", false, "list all deployments") +} + var resourceTypesHelp = fmt.Sprintf("\nResource Types:\n %s\n", strings.Join(resource.VisibleTypes.StringList(), "\n ")) func addResourceTypesToHelp(cmd *cobra.Command) { diff --git a/docs/cluster/cli.md b/docs/cluster/cli.md index 469a083207..c7d945e009 100644 --- a/docs/cluster/cli.md +++ b/docs/cluster/cli.md @@ -25,6 +25,7 @@ Usage: cortex get [RESOURCE_TYPE] [RESOURCE_NAME] [flags] Flags: + -a, --all-deployments list all deployments -d, --deployment string deployment name -e, --env string environment (default "dev") -h, --help help for get @@ -33,7 +34,7 @@ Flags: -w, --watch re-run the command every second ``` -The `get` command displays the current state of all resources on the cluster. Specifying a resource name provides the state of the particular resource. A detailed view of the configuration and additional metdata of a specific resource can be retrieved by adding the `-v` or `--verbose` flag. Using the `-s` or `--summary` flag will show a summarized view of all resource statuses. +The `get` command displays the current state of all resources on the cluster. Specifying a resource name provides the state of the particular resource. A detailed view of the configuration and additional metdata of a specific resource can be retrieved by adding the `-v` or `--verbose` flag. Using the `-s` or `--summary` flag will show a summarized view of all resource statuses. A list of deployments can be displayed by specifying the `-a` or `--all-deployments` flag. ## logs diff --git a/pkg/operator/api/context/context.go b/pkg/operator/api/context/context.go index 0aef70158b..76f76873eb 100644 --- a/pkg/operator/api/context/context.go +++ b/pkg/operator/api/context/context.go @@ -29,6 +29,7 @@ import ( type Context struct { ID string `json:"id"` Key string `json:"key"` + CreatedEpoch int64 `json:"created_epoch"` CortexConfig *config.CortexConfig `json:"cortex_config"` DatasetVersion string `json:"dataset_version"` Root string `json:"root"` diff --git a/pkg/operator/api/resource/deployment_status.go b/pkg/operator/api/resource/deployment_status.go new file mode 100644 index 0000000000..f3a156c954 --- /dev/null +++ b/pkg/operator/api/resource/deployment_status.go @@ -0,0 +1,80 @@ +/* +Copyright 2019 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 resource + +type DeploymentStatus int + +const ( + UnknownDeploymentStatus DeploymentStatus = iota + UpdatedDeploymentStatus + UpdatingDeploymentStatus + ErrorDeploymentStatus +) + +var deploymentStatuses = []string{ + "unknown", + "updated", + "updating", + "error", +} + +func DeploymentStatusFromString(s string) DeploymentStatus { + for i := 0; i < len(deploymentStatuses); i++ { + if s == deploymentStatuses[i] { + return DeploymentStatus(i) + } + } + return UnknownDeploymentStatus +} + +func DeploymentStatusStrings() []string { + return deploymentStatuses[1:] +} + +func (t DeploymentStatus) String() string { + return deploymentStatuses[t] +} + +// MarshalText satisfies TextMarshaler +func (t DeploymentStatus) MarshalText() ([]byte, error) { + return []byte(t.String()), nil +} + +// UnmarshalText satisfies TextUnmarshaler +func (t *DeploymentStatus) UnmarshalText(text []byte) error { + enum := string(text) + for i := 0; i < len(deploymentStatuses); i++ { + if enum == deploymentStatuses[i] { + *t = DeploymentStatus(i) + return nil + } + } + + *t = UnknownDeploymentStatus + return nil +} + +// UnmarshalBinary satisfies BinaryUnmarshaler +// Needed for msgpack +func (t *DeploymentStatus) UnmarshalBinary(data []byte) error { + return t.UnmarshalText(data) +} + +// MarshalBinary satisfies BinaryMarshaler +func (t DeploymentStatus) MarshalBinary() ([]byte, error) { + return []byte(t.String()), nil +} diff --git a/pkg/operator/api/schema/schema.go b/pkg/operator/api/schema/schema.go index 7823363cc5..2ced0d9362 100644 --- a/pkg/operator/api/schema/schema.go +++ b/pkg/operator/api/schema/schema.go @@ -17,6 +17,8 @@ limitations under the License. package schema import ( + "time" + "github.com/cortexlabs/cortex/pkg/operator/api/context" "github.com/cortexlabs/cortex/pkg/operator/api/resource" ) @@ -41,6 +43,16 @@ type GetResourcesResponse struct { APIsBaseURL string `json:"apis_base_url"` } +type Deployment struct { + Name string `json:"name"` + Status resource.DeploymentStatus `json:"status"` + LastUpdated time.Time `json:"last_updated"` +} + +type GetDeploymentsResponse struct { + Deployments []Deployment `json:"deployments"` +} + type GetAggregateResponse struct { Value []byte `json:"value"` } diff --git a/pkg/operator/context/context.go b/pkg/operator/context/context.go index 58794f401d..c867b24e7c 100644 --- a/pkg/operator/context/context.go +++ b/pkg/operator/context/context.go @@ -20,6 +20,7 @@ import ( "path/filepath" "sort" "strings" + "time" "github.com/cortexlabs/cortex/pkg/consts" "github.com/cortexlabs/cortex/pkg/lib/configreader" @@ -122,6 +123,7 @@ func New( ignoreCache bool, ) (*context.Context, error) { ctx := &context.Context{} + ctx.CreatedEpoch = time.Now().Unix() ctx.CortexConfig = config.Cortex diff --git a/pkg/operator/endpoints/deploy.go b/pkg/operator/endpoints/deploy.go index 197782793a..26426a272d 100644 --- a/pkg/operator/endpoints/deploy.go +++ b/pkg/operator/endpoints/deploy.go @@ -23,6 +23,7 @@ import ( "github.com/cortexlabs/cortex/pkg/lib/files" "github.com/cortexlabs/cortex/pkg/lib/zip" "github.com/cortexlabs/cortex/pkg/operator/api/context" + "github.com/cortexlabs/cortex/pkg/operator/api/resource" "github.com/cortexlabs/cortex/pkg/operator/api/schema" "github.com/cortexlabs/cortex/pkg/operator/api/userconfig" "github.com/cortexlabs/cortex/pkg/operator/config" @@ -61,12 +62,14 @@ func Deploy(w http.ResponseWriter, r *http.Request) { fullCtxMatch = true } - isUpdating, err := workloads.IsDeploymentUpdating(ctx.App.Name) + deploymentStatus, err := workloads.GetDeploymentStatus(ctx.App.Name) if err != nil { RespondError(w, err) return } + isUpdating := deploymentStatus == resource.UpdatingDeploymentStatus + if isUpdating { if fullCtxMatch { respondDeploy(w, ResDeploymentUpToDateUpdating) diff --git a/pkg/operator/endpoints/deployments.go b/pkg/operator/endpoints/deployments.go new file mode 100644 index 0000000000..0cd7914234 --- /dev/null +++ b/pkg/operator/endpoints/deployments.go @@ -0,0 +1,42 @@ +/* +Copyright 2019 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 ( + "net/http" + "time" + + "github.com/cortexlabs/cortex/pkg/operator/api/schema" + "github.com/cortexlabs/cortex/pkg/operator/workloads" +) + +func GetDeployments(w http.ResponseWriter, r *http.Request) { + currentContexts := workloads.CurrentContexts() + deployments := make([]schema.Deployment, len(currentContexts)) + for i, ctx := range currentContexts { + deployments[i].Name = ctx.App.Name + status, _ := workloads.GetDeploymentStatus(ctx.App.Name) + deployments[i].Status = status + deployments[i].LastUpdated = time.Unix(ctx.CreatedEpoch, 0) + } + + response := schema.GetDeploymentsResponse{ + Deployments: deployments, + } + + Respond(w, response) +} diff --git a/pkg/operator/operator.go b/pkg/operator/operator.go index be56591c7e..ae99110baa 100644 --- a/pkg/operator/operator.go +++ b/pkg/operator/operator.go @@ -58,6 +58,7 @@ func main() { router.HandleFunc("/deploy", endpoints.Deploy).Methods("POST") router.HandleFunc("/delete", endpoints.Delete).Methods("POST") + router.HandleFunc("/deployments", endpoints.GetDeployments).Methods("GET") router.HandleFunc("/resources", endpoints.GetResources).Methods("GET") router.HandleFunc("/aggregate/{id}", endpoints.GetAggregate).Methods("GET") router.HandleFunc("/logs/read", endpoints.ReadLogs) diff --git a/pkg/operator/workloads/workflow.go b/pkg/operator/workloads/workflow.go index bc855f0411..ab174bb7fd 100644 --- a/pkg/operator/workloads/workflow.go +++ b/pkg/operator/workloads/workflow.go @@ -24,6 +24,7 @@ import ( "github.com/cortexlabs/cortex/pkg/lib/errors" "github.com/cortexlabs/cortex/pkg/lib/sets/strset" "github.com/cortexlabs/cortex/pkg/operator/api/context" + "github.com/cortexlabs/cortex/pkg/operator/api/resource" "github.com/cortexlabs/cortex/pkg/operator/api/userconfig" "github.com/cortexlabs/cortex/pkg/operator/config" ) @@ -280,22 +281,23 @@ func IsWorkloadEnded(appName string, workloadID string) (bool, error) { return false, errors.New("workload not found in the current context") } -func IsDeploymentUpdating(appName string) (bool, error) { +func GetDeploymentStatus(appName string) (resource.DeploymentStatus, error) { ctx := CurrentContext(appName) if ctx == nil { - return false, nil + return resource.UnknownDeploymentStatus, nil } + isUpdating := false for _, workload := range extractWorkloads(ctx) { - // Pending HPA workloads shouldn't block new deployments + // HPA workloads don't really count if workload.GetWorkloadType() == workloadTypeHPA { continue } isSucceeded, err := workload.IsSucceeded(ctx) if err != nil { - return false, err + return resource.UnknownDeploymentStatus, err } if isSucceeded { continue @@ -303,23 +305,24 @@ func IsDeploymentUpdating(appName string) (bool, error) { isFailed, err := workload.IsFailed(ctx) if err != nil { - return false, err + return resource.UnknownDeploymentStatus, err } if isFailed { - continue + return resource.ErrorDeploymentStatus, nil } canRun, err := workload.CanRun(ctx) if err != nil { - return false, err + return resource.UnknownDeploymentStatus, err } if !canRun { continue } - - // It's either running or can run - return true, nil + isUpdating = true } - return false, nil + if isUpdating { + return resource.UpdatingDeploymentStatus, nil + } + return resource.UpdatedDeploymentStatus, nil }