Skip to content

Commit

Permalink
feat: n support list ec2 and s start session to ec2 (#325)
Browse files Browse the repository at this point in the history
* feat: support list ec2 and start session to ec2

* fix text
  • Loading branch information
keidarcy authored Feb 23, 2025
1 parent af367d1 commit 58368e7
Show file tree
Hide file tree
Showing 15 changed files with 363 additions and 14 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ tail -f /tmp/e1s.log
- [x] Read only mode
- [x] Auto refresh
- [x] Describe clusters
- [x] Describe instances
- [x] Describe services
- [x] Describe service deployments
- [x] Describe service revisions
Expand All @@ -216,7 +217,8 @@ tail -f /tmp/e1s.log
- [x] MemoryUtilization
- [x] Show autoscaling target and policy
- [x] Open selected resource in browser(support new UI(v2))
- [x] Interactively exec towards containers(like ssh)
- [x] Interactively shell to containers(like ssh)
- [x] Interactively shell to instances(like ssh)
- [x] Edit service
- [x] Desired count
- [x] Force new deployment
Expand Down
46 changes: 46 additions & 0 deletions internal/api/instance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package api

import (
"context"
"log/slog"

"github.com/aws/aws-sdk-go-v2/service/ecs"
"github.com/aws/aws-sdk-go-v2/service/ecs/types"
)

// ListContainerInstances gets container instances in an ECS cluster
// Equivalent to:
// aws ecs list-container-instances --cluster ${cluster}
// aws ecs describe-container-instances --cluster ${cluster} --container-instances ${instance1} ${instance2}
func (store *Store) ListContainerInstances(cluster *string) ([]types.ContainerInstance, error) {
batchSize := 100
limit := int32(batchSize)
params := &ecs.ListContainerInstancesInput{
Cluster: cluster,
MaxResults: &limit,
}

listOutput, err := store.ecs.ListContainerInstances(context.Background(), params)
if err != nil {
slog.Warn("failed to run aws api to list container instances", "error", err)
return []types.ContainerInstance{}, err
}

// If no instances found, return empty slice
if len(listOutput.ContainerInstanceArns) == 0 {
return []types.ContainerInstance{}, nil
}

// Get detailed information about the container instances
describeOutput, err := store.ecs.DescribeContainerInstances(context.Background(), &ecs.DescribeContainerInstancesInput{
Cluster: cluster,
ContainerInstances: listOutput.ContainerInstanceArns,
})

if err != nil {
slog.Warn("failed to run aws api to describe container instances", "error", err)
return []types.ContainerInstance{}, err
}

return describeOutput.ContainerInstances, nil
}
20 changes: 20 additions & 0 deletions internal/api/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package api

import (
"context"
"fmt"
"log/slog"
"sort"

Expand Down Expand Up @@ -115,3 +116,22 @@ func (store *Store) StopTask(input *ecs.StopTaskInput) error {
}
return nil
}

// aws ecs describe-container-instances --cluster ${cluster} --container-instances ${instanceId}
func (store *Store) GetTaskInstanceId(cluster, containerInstance *string) (string, error) {
describeOutput, err := store.ecs.DescribeContainerInstances(context.Background(), &ecs.DescribeContainerInstancesInput{
Cluster: cluster,
ContainerInstances: []string{*containerInstance},
})

if err != nil {
slog.Warn("failed to run aws api to describe container instances", "error", err)
return "", err
}

if len(describeOutput.ContainerInstances) != 1 {
return "", fmt.Errorf("expect 1 container instance, got %d", len(describeOutput.ContainerInstances))
}

return *describeOutput.ContainerInstances[0].Ec2InstanceId, nil
}
3 changes: 3 additions & 0 deletions internal/view/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type Entity struct {
events []types.ServiceEvent
metrics *api.MetricsData
autoScaling *api.AutoScalingData
instance *types.ContainerInstance
serviceDeployment *types.ServiceDeployment
serviceRevision *types.ServiceRevision
entityName string
Expand Down Expand Up @@ -275,6 +276,8 @@ func (app *App) showPrimaryKindPage(k kind, reload bool) error {
switch k {
case ClusterKind:
err = app.showClustersPage(reload)
case InstanceKind:
err = app.showInstancesPage(reload)
case ServiceKind:
err = app.showServicesPage(reload)
case TaskKind:
Expand Down
1 change: 1 addition & 0 deletions internal/view/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type clusterView struct {
func newClusterView(clusters []types.Cluster, app *App) *clusterView {
keys := append(basicKeyInputs, []keyDescriptionPair{
hotKeyMap["n"],
hotKeyMap["N"],
}...)
return &clusterView{
view: *newView(app, keys, secondaryPageKeyMap{
Expand Down
2 changes: 1 addition & 1 deletion internal/view/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func newContainerView(containers []types.Container, app *App) *containerView {
hotKeyMap["P"],
hotKeyMap["D"],
hotKeyMap["E"],
hotKeyMap["enter"],
hotKeyMap["s"],
hotKeyMap["ctrlD"],
}...)
return &containerView{
Expand Down
6 changes: 6 additions & 0 deletions internal/view/footer.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type footer struct {
service *tview.TextView
task *tview.TextView
container *tview.TextView
instance *tview.TextView
taskDefinition *tview.TextView
serviceDeployment *tview.TextView
help *tview.TextView
Expand All @@ -29,6 +30,7 @@ func newFooter() *footer {
service: tview.NewTextView().SetDynamicColors(true).SetText(fmt.Sprintf(color.FooterItemFmt, ServiceKind)),
task: tview.NewTextView().SetDynamicColors(true).SetText(fmt.Sprintf(color.FooterItemFmt, TaskKind)),
container: tview.NewTextView().SetDynamicColors(true).SetText(fmt.Sprintf(color.FooterItemFmt, ContainerKind)),
instance: tview.NewTextView().SetDynamicColors(true).SetText(fmt.Sprintf(color.FooterItemFmt, InstanceKind)).SetTextAlign(L),
taskDefinition: tview.NewTextView().SetDynamicColors(true).SetText(fmt.Sprintf(color.FooterItemFmt, TaskDefinitionKind)).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),
Expand All @@ -46,6 +48,10 @@ 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 == InstanceKind {
v.footer.footerFlex.
AddItem(tview.NewTextView(), 5, 0, false).
AddItem(v.footer.instance, 0, 1, false)
} else if v.app.kind == ServiceDeploymentKind {
v.footer.footerFlex.
AddItem(tview.NewTextView(), 5, 0, false).
Expand Down
6 changes: 4 additions & 2 deletions internal/view/header.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ var hotKeyMap = map[string]keyDescriptionPair{
"r": {key: "r", description: "Realtime log streaming(Only support one log group)"},
"t": {key: "t", description: "Show task definitions"},
"p": {key: "p", description: "Show service deployments"},
"n": {key: "n", description: "Show all cluster tasks"},
"s": {key: "s", description: "Toggle running/stopped tasks"},
"n": {key: "n", description: "Show related EC2 instances"},
"N": {key: "shift-n", description: "Show all cluster tasks"},
"s": {key: "s", description: "Shell access"},
"x": {key: "x", description: "Toggle running/stopped tasks"},
"w": {key: "w", description: "Show service events"},
"v": {key: "v", description: "Show service revision"},
"S": {key: "shift-s", description: "Stop task"},
Expand Down
150 changes: 150 additions & 0 deletions internal/view/instance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package view

import (
"fmt"
"log/slog"

"github.com/aws/aws-sdk-go-v2/service/ecs/types"
"github.com/keidarcy/e1s/internal/color"
"github.com/keidarcy/e1s/internal/utils"
"github.com/rivo/tview"
)

// Add new type for instance view
type instanceView struct {
view
instances []types.ContainerInstance
}

// Constructor for instance view
func newInstanceView(instances []types.ContainerInstance, app *App) *instanceView {
keys := append(basicKeyInputs, []keyDescriptionPair{
hotKeyMap["s"],
}...)
return &instanceView{
view: *newView(app, keys, secondaryPageKeyMap{
DescriptionKind: describePageKeys,
}),
instances: instances,
}
}

// Show instances page
func (app *App) showInstancesPage(reload bool) error {
if switched := app.switchPage(reload); switched {
return nil
}

instances, err := app.Store.ListContainerInstances(app.cluster.ClusterName)
if err != nil {
slog.Warn("failed to show instances page", "error", err)
app.back()
return err
}

if len(instances) == 0 {
app.back()
return fmt.Errorf("no instances found")
}

view := newInstanceView(instances, app)
page := buildAppPage(view)
app.addAppPage(page)
view.table.Select(app.rowIndex, 0)
return nil
}

// Build info pages for instance page
func (v *instanceView) headerBuilder() *tview.Pages {
for _, instance := range v.instances {
title := utils.ArnToName(instance.ContainerInstanceArn)
entityName := *instance.ContainerInstanceArn
items := v.headerPagesParam(instance)

v.buildHeaderPages(items, title, entityName)
}

if len(v.instances) > 0 && v.instances[0].ContainerInstanceArn != nil {
v.headerPages.SwitchToPage(*v.instances[0].ContainerInstanceArn)
v.changeSelectedValues()
}
return v.headerPages
}

// Generate info pages params
func (v *instanceView) headerPagesParam(instance types.ContainerInstance) (items []headerItem) {
items = []headerItem{
{name: "Instance ID", value: utils.ShowString(instance.Ec2InstanceId)},
{name: "Status", value: utils.ShowString(instance.Status)},
{name: "Capacity Provider", value: utils.ShowString(instance.CapacityProviderName)},
{name: "Agent Connected", value: fmt.Sprintf("%v", instance.AgentConnected)},
{name: "Running Tasks Count", value: fmt.Sprintf("%d", instance.RunningTasksCount)},
{name: "Pending Tasks Count", value: fmt.Sprintf("%d", instance.PendingTasksCount)},
{name: "Agent Version", value: utils.ShowString(instance.VersionInfo.AgentVersion)},
{name: "Docker Version", value: utils.ShowString(instance.VersionInfo.DockerVersion)},
{name: "Registered At", value: utils.ShowTime(instance.RegisteredAt)},
}
return
}

// Build footer for instance page
func (v *instanceView) footerBuilder() *tview.Flex {
v.footer.instance.SetText(fmt.Sprintf(color.FooterSelectedItemFmt, v.app.kind))
v.addFooterItems()
return v.footer.footerFlex
}

// Build table for instance page
func (v *instanceView) bodyBuilder() *tview.Pages {
title, headers, dataBuilder := v.tableParam()
v.buildTable(title, headers, dataBuilder)
v.tableHandler()
return v.bodyPages
}

// Handlers for instance table
func (v *instanceView) tableHandler() {
for row, instance := range v.instances {
i := instance
v.table.GetCell(row+1, 0).SetReference(Entity{instance: &i, entityName: *i.ContainerInstanceArn})
}
}

// Generate table params
func (v *instanceView) tableParam() (title string, headers []string, dataBuilder func() [][]string) {
clusterName := ""
if v.app.cluster.ClusterName != nil {
clusterName = *v.app.cluster.ClusterName
}

title = fmt.Sprintf(color.TableTitleFmt, v.app.kind, clusterName, len(v.instances))
headers = []string{
"Instance ID ▾",
"Status",
"Running Tasks",
"Pending Tasks",
"Agent Connected",
"Agent Version",
"Docker Version",
"Registered At",
}

dataBuilder = func() (data [][]string) {
for _, instance := range v.instances {
row := []string{
utils.ArnToName(instance.ContainerInstanceArn),
utils.ShowGreenGrey(instance.Status, "active"),
fmt.Sprintf("%d", instance.RunningTasksCount),
fmt.Sprintf("%d", instance.PendingTasksCount),
fmt.Sprintf("%v", instance.AgentConnected),
utils.ShowString(instance.VersionInfo.AgentVersion),
utils.ShowString(instance.VersionInfo.DockerVersion),
utils.ShowTime(instance.RegisteredAt),
}
data = append(data, row)
}
return data
}

return
}
2 changes: 2 additions & 0 deletions internal/view/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@ func (v *view) getJsonString(entity Entity) (string, []byte, error) {
switch {
case entity.cluster != nil && v.app.kind == ClusterKind:
data = entity.cluster
case entity.instance != nil && v.app.kind == InstanceKind:
data = entity.instance
// events need be upper then service
case entity.events != nil && v.app.secondaryKind == ServiceEventsKind:
data = entity.events
Expand Down
7 changes: 5 additions & 2 deletions internal/view/kind.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const (
ClusterKind kind = iota
ServiceKind
TaskKind
InstanceKind
ContainerKind
TaskDefinitionKind
HelpKind
Expand Down Expand Up @@ -35,6 +36,8 @@ func (k kind) String() string {
return "description"
case TaskDefinitionKind:
return "task definitions"
case InstanceKind:
return "instances"
case ServiceEventsKind:
return "service events"
case ServiceDeploymentKind:
Expand Down Expand Up @@ -67,7 +70,7 @@ func (k kind) nextKind() kind {

func (k kind) prevKind() kind {
switch k {
case ClusterKind:
case ClusterKind, InstanceKind:
return ClusterKind
case ServiceKind:
return ClusterKind
Expand All @@ -85,7 +88,7 @@ func (k kind) getAppPageName(name string) string {
switch k {
case ClusterKind:
return k.String()
case ServiceKind, TaskKind, ContainerKind, TaskDefinitionKind, ServiceDeploymentKind, DescriptionKind:
case ServiceKind, TaskKind, ContainerKind, TaskDefinitionKind, ServiceDeploymentKind, DescriptionKind, InstanceKind:
return k.String() + "." + name
default:
return k.String()
Expand Down
Loading

0 comments on commit 58368e7

Please sign in to comment.