Skip to content
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

feat: n support list ec2 and s start session to ec2 #325

Merged
merged 2 commits into from
Feb 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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