diff --git a/README.md b/README.md index 640067b..e6c95c0 100644 --- a/README.md +++ b/README.md @@ -197,7 +197,8 @@ tail -f /tmp/e1s.log - [x] Auto refresh - [x] Describe clusters - [x] Describe services - - [x] Describe service deployments(new) + - [x] Describe service deployments + - [x] Describe service revisions - [x] Describe tasks(running, stopped) - [x] Describe containers - [x] Describe task definitions diff --git a/internal/api/service_deployment.go b/internal/api/service_deployment.go index b38b193..0345248 100644 --- a/internal/api/service_deployment.go +++ b/internal/api/service_deployment.go @@ -47,3 +47,16 @@ func (store *Store) ListServiceDeployments(cluster, service *string) ([]types.Se return describeOutput.ServiceDeployments, nil } + +// Equivalent to +// aws ecs describe-service-revisions --service-revision-arns ${arn1} +func (store *Store) GetServiceRevision(serviceRevisionArn *string) (*types.ServiceRevision, error) { + describeServiceRevisionOutput, err := store.ecs.DescribeServiceRevisions(context.Background(), &ecs.DescribeServiceRevisionsInput{ + ServiceRevisionArns: []string{*serviceRevisionArn}, + }) + if err != nil { + slog.Warn("failed to run aws api to describe service revision", "error", err) + return nil, err + } + return &describeServiceRevisionOutput.ServiceRevisions[0], nil +} diff --git a/internal/view/app.go b/internal/view/app.go index cbc84c9..3d45803 100644 --- a/internal/view/app.go +++ b/internal/view/app.go @@ -30,6 +30,7 @@ type Entity struct { metrics *api.MetricsData autoScaling *api.AutoScalingData serviceDeployment *types.ServiceDeployment + serviceRevision *types.ServiceRevision entityName string } @@ -282,7 +283,7 @@ func (app *App) showPrimaryKindPage(k kind, reload bool) error { err = app.showContainersPage(reload) case TaskDefinitionKind: err = app.showTaskDefinitionPage(reload) - case ServiceDeployment: + case ServiceDeploymentKind: err = app.showServiceDeploymentPage(reload) default: app.kind = ClusterKind diff --git a/internal/view/footer.go b/internal/view/footer.go index a071d5a..8a7fc32 100644 --- a/internal/view/footer.go +++ b/internal/view/footer.go @@ -30,7 +30,7 @@ func newFooter() *footer { task: tview.NewTextView().SetDynamicColors(true).SetText(fmt.Sprintf(color.FooterItemFmt, TaskKind)), container: tview.NewTextView().SetDynamicColors(true).SetText(fmt.Sprintf(color.FooterItemFmt, ContainerKind)), taskDefinition: tview.NewTextView().SetDynamicColors(true).SetText(fmt.Sprintf(color.FooterItemFmt, TaskDefinitionKind)).SetTextAlign(L), - serviceDeployment: tview.NewTextView().SetDynamicColors(true).SetText(fmt.Sprintf(color.FooterItemFmt, ServiceDeployment)).SetTextAlign(L), + serviceDeployment: tview.NewTextView().SetDynamicColors(true).SetText(fmt.Sprintf(color.FooterItemFmt, ServiceDeploymentKind)).SetTextAlign(L), help: tview.NewTextView().SetDynamicColors(true).SetText(fmt.Sprintf(color.FooterItemFmt, HelpKind)).SetTextAlign(L), } } @@ -46,7 +46,7 @@ func (v *view) addFooterItems() { v.footer.footerFlex. AddItem(tview.NewTextView(), 5, 0, false). AddItem(v.footer.taskDefinition, 0, 1, false) - } else if v.app.kind == ServiceDeployment { + } else if v.app.kind == ServiceDeploymentKind { v.footer.footerFlex. AddItem(tview.NewTextView(), 5, 0, false). AddItem(v.footer.serviceDeployment, 0, 1, false) diff --git a/internal/view/header.go b/internal/view/header.go index ea31a3f..c5b2f06 100644 --- a/internal/view/header.go +++ b/internal/view/header.go @@ -17,16 +17,17 @@ const ( var hotKeyMap = map[string]keyDescriptionPair{ "/": {key: "/", description: "Search in table"}, - "a": {key: "a", description: "Describe service auto scaling"}, + "a": {key: "a", description: "Show service auto scaling"}, "f": {key: "f", description: "Toggle full screen"}, "l": {key: "l", description: "Show cloudwatch logs(Only support awslogs logDriver)"}, "m": {key: "m", description: "Show metrics(CPU/Memory)"}, "r": {key: "r", description: "Realtime log streaming(Only support one log group)"}, "t": {key: "t", description: "Show task definitions"}, - "p": {key: "t", description: "Show service deployments"}, + "p": {key: "p", description: "Show service deployments"}, "n": {key: "n", description: "Show all cluster tasks"}, "s": {key: "s", description: "Toggle running/stopped tasks"}, - "w": {key: "w", description: "Describe service events"}, + "w": {key: "w", description: "Show service events"}, + "v": {key: "v", description: "Show service revision"}, "S": {key: "shift-s", description: "Stop task"}, "P": {key: "shift-p", description: "Transfer file though a S3 bucket"}, "D": {key: "shift-d", description: "Download text file content(beta)"}, diff --git a/internal/view/json.go b/internal/view/json.go index 967e142..c72ad75 100644 --- a/internal/view/json.go +++ b/internal/view/json.go @@ -67,6 +67,27 @@ func (v *view) switchToAutoScalingJson() { v.showJsonPages(entity) } +// Switch to service revision +func (v *view) switchToServiceRevisionJson() { + selected, err := v.getCurrentSelection() + if err != nil { + return + } + serviceRevisionArn := selected.serviceDeployment.TargetServiceRevision.Arn + + if serviceRevisionArn == nil { + return + } + + serviceRevision, err := v.app.Store.GetServiceRevision(serviceRevisionArn) + + if err != nil { + return + } + entity := Entity{serviceRevision: serviceRevision, entityName: *selected.serviceDeployment.ServiceDeploymentArn} + v.showJsonPages(entity) +} + // Show new page from JSON content in table area and handle done event to go back func (v *view) showJsonPages(entity Entity) { colorizedJsonString, rawJsonString, err := v.getJsonString(entity) @@ -218,7 +239,7 @@ func (v *view) getJsonString(entity Entity) (string, []byte, error) { data = entity.events case entity.service != nil && v.app.kind == ServiceKind: data = entity.service - case entity.serviceDeployment != nil && v.app.kind == ServiceDeployment: + case entity.serviceDeployment != nil && v.app.kind == ServiceDeploymentKind: data = entity.serviceDeployment case entity.task != nil && v.app.kind == TaskKind: data = entity.task @@ -230,6 +251,8 @@ func (v *view) getJsonString(entity Entity) (string, []byte, error) { data = entity.metrics case entity.autoScaling != nil: data = entity.autoScaling + case entity.serviceRevision != nil: + data = entity.serviceRevision default: slog.Error("failed to get json string", "data", data) data = struct { diff --git a/internal/view/kind.go b/internal/view/kind.go index 032075f..b30fc28 100644 --- a/internal/view/kind.go +++ b/internal/view/kind.go @@ -11,9 +11,10 @@ const ( HelpKind DescriptionKind ServiceEventsKind - ServiceDeployment + ServiceDeploymentKind LogKind AutoScalingKind + ServiceRevisionKind ModalKind EmptyKind ) @@ -36,8 +37,10 @@ func (k kind) String() string { return "task definitions" case ServiceEventsKind: return "service events" - case ServiceDeployment: + case ServiceDeploymentKind: return "service deployments" + case ServiceRevisionKind: + return "service revision" case LogKind: return "logs" case AutoScalingKind: @@ -68,7 +71,7 @@ func (k kind) prevKind() kind { return ClusterKind case ServiceKind: return ClusterKind - case TaskKind, TaskDefinitionKind, ServiceDeployment: + case TaskKind, TaskDefinitionKind, ServiceDeploymentKind: return ServiceKind case ContainerKind: return TaskKind @@ -82,7 +85,7 @@ func (k kind) getAppPageName(name string) string { switch k { case ClusterKind: return k.String() - case ServiceKind, TaskKind, ContainerKind, TaskDefinitionKind, ServiceDeployment, DescriptionKind: + case ServiceKind, TaskKind, ContainerKind, TaskDefinitionKind, ServiceDeploymentKind, DescriptionKind: return k.String() + "." + name default: return k.String() diff --git a/internal/view/service_deployment.go b/internal/view/service_deployment.go index 725386a..6821dff 100644 --- a/internal/view/service_deployment.go +++ b/internal/view/service_deployment.go @@ -19,11 +19,12 @@ type serviceDeploymentView struct { // Constructor for service deployment view func newServiceDeploymentView(serviceDeployments []types.ServiceDeployment, app *App) *serviceDeploymentView { keys := append(basicKeyInputs, []keyDescriptionPair{ - hotKeyMap["U"], + hotKeyMap["v"], }...) return &serviceDeploymentView{ view: *newView(app, keys, secondaryPageKeyMap{ - DescriptionKind: describePageKeys, + DescriptionKind: describePageKeys, + ServiceRevisionKind: describePageKeys, }), serviceDeployments: serviceDeployments, } @@ -91,8 +92,8 @@ func (v *serviceDeploymentView) headerPagesParam(d types.ServiceDeployment) (ite return "-" } return fmt.Sprintf("Max: %d%%, Min: %d%%", - d.DeploymentConfiguration.MaximumPercent, - d.DeploymentConfiguration.MinimumHealthyPercent) + *d.DeploymentConfiguration.MaximumPercent, + *d.DeploymentConfiguration.MinimumHealthyPercent) }()}, {name: "Circuit Breaker Config", value: func() string { if d.DeploymentConfiguration == nil || d.DeploymentConfiguration.DeploymentCircuitBreaker == nil { @@ -102,24 +103,23 @@ func (v *serviceDeploymentView) headerPagesParam(d types.ServiceDeployment) (ite d.DeploymentConfiguration.DeploymentCircuitBreaker.Enable, d.DeploymentConfiguration.DeploymentCircuitBreaker.Rollback) }()}, - {name: "Source revision", value: func() string { - if len(d.SourceServiceRevisions) == 0 { + {name: "Target task count", value: func() string { + if d.TargetServiceRevision == nil { return "-" } - return fmt.Sprintf("%s (Running: %d, Pending: %d)", - utils.ArnToName(d.SourceServiceRevisions[0].Arn), - d.SourceServiceRevisions[0].RunningTaskCount, - d.SourceServiceRevisions[0].PendingTaskCount) + return fmt.Sprintf("Requested: %d, Running: %d, Pending: %d", + d.TargetServiceRevision.RequestedTaskCount, + d.TargetServiceRevision.RunningTaskCount, + d.TargetServiceRevision.PendingTaskCount) }()}, - {name: "Target revision", value: func() string { - if d.TargetServiceRevision == nil { + {name: "Source task count", value: func() string { + if len(d.SourceServiceRevisions) == 0 { return "-" } - return fmt.Sprintf("%s (Running: %d, Pending: %d, Requested: %d)", - utils.ArnToName(d.TargetServiceRevision.Arn), - d.TargetServiceRevision.RunningTaskCount, - d.TargetServiceRevision.PendingTaskCount, - d.TargetServiceRevision.RequestedTaskCount) + return fmt.Sprintf("Requested: %d, Running: %d, Pending: %d", + d.SourceServiceRevisions[0].RequestedTaskCount, + d.SourceServiceRevisions[0].RunningTaskCount, + d.SourceServiceRevisions[0].PendingTaskCount) }()}, {name: "Created At", value: utils.ShowTime(d.CreatedAt)}, {name: "Started At", value: utils.ShowTime(d.StartedAt)}, @@ -163,7 +163,7 @@ func (v *serviceDeploymentView) tableParam() (title string, headers []string, da headers = []string{ "Deployment ID ▾", "Status", - "Revision", + "Target service revision", "Created At", "Started At", "Finished At", diff --git a/internal/view/table.go b/internal/view/table.go index f4ddf2f..1b002f3 100644 --- a/internal/view/table.go +++ b/internal/view/table.go @@ -129,7 +129,13 @@ func (v *view) handleInputCapture(event *tcell.EventKey) *tcell.EventKey { } case 'p': if v.app.kind == ServiceKind { - v.showKindPage(ServiceDeployment, false) + v.showKindPage(ServiceDeploymentKind, false) + return event + } + case 'v': + if v.app.kind == ServiceDeploymentKind { + v.app.secondaryKind = ServiceRevisionKind + v.showSecondaryKindPage(false) return event } case 's': @@ -284,7 +290,7 @@ func (v *view) changeSelectedValues() { slog.Warn("unexpected in changeSelectedValues", "kind", v.app.kind) return } - case ServiceDeployment: + case ServiceDeploymentKind: serviceDeployment := selected.serviceDeployment if serviceDeployment != nil { v.app.serviceDeployment = selected.serviceDeployment @@ -320,7 +326,7 @@ func (v *view) openInBrowser() { arn = *v.app.task.TaskArn case TaskDefinitionKind: arn = *v.app.taskDefinition.TaskDefinitionArn - case ServiceDeployment: + case ServiceDeploymentKind: arn = *v.app.serviceDeployment.ServiceDeploymentArn } url := utils.ArnToUrl(arn, taskService) diff --git a/internal/view/view.go b/internal/view/view.go index 492d417..962b76b 100644 --- a/internal/view/view.go +++ b/internal/view/view.go @@ -107,6 +107,8 @@ func (v *view) showSecondaryKindPage(reload bool) { v.switchToLogsList() case ServiceEventsKind: v.switchToServiceEventsList() + case ServiceRevisionKind: + v.switchToServiceRevisionJson() } if !reload { v.app.Notice.Infof("Viewing %s...", v.app.secondaryKind.String()) @@ -158,7 +160,7 @@ func (v *view) handleSecondaryPageSwitch(entity Entity, colorizedJsonString stri v.realtimeAwsLog(entity) } case 'e': - if v.app.secondaryKind == DescriptionKind || v.app.secondaryKind == AutoScalingKind || v.app.secondaryKind == LogKind { + if v.app.secondaryKind == DescriptionKind || v.app.secondaryKind == AutoScalingKind || v.app.secondaryKind == ServiceRevisionKind || v.app.secondaryKind == LogKind { v.openInEditor(jsonBytes) } }