Skip to content

Commit 3725a2f

Browse files
authored
Merge pull request #194 from DevopsArtFactory/feature/174-ec2-related-resources
feat: add EC2 related resource browser
2 parents 111f7cd + 9351abb commit 3725a2f

14 files changed

Lines changed: 1374 additions & 24 deletions

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ checks:
330330
| Area | Keys |
331331
|---|---|
332332
| EC2 SSM | `r` refresh, `Enter` connect |
333-
| EC2 Instance Browser | `r` refresh, `/` filter, `Enter` detail |
333+
| EC2 Instance Browser | `r` refresh, `/` filter, `Enter` detail, detail `g/a/t/b/n` opens related security groups/ASG/target groups/load balancers/listeners |
334334
| Security Groups | `a` add rule, `d` delete rule, `Tab` switch ingress/egress |
335335
| Reachability Analyzer | Region select first, `←`/`→` or `Tab` change type, `/` filter, `Enter` advance, `Tab`/`↑`/`↓` move config fields, `←`/`→` protocol, `r` rerun |
336336
| RDS | `s` start, `x` stop, `f` failover, `r` refresh |
@@ -360,7 +360,7 @@ The EKS Browser includes a current-version upgrade readiness view for each selec
360360

361361
The EKS Browser includes an access helper for each selected cluster. Press `u` from the cluster list to review the cluster endpoint, ARN, current region/profile, and copy an `aws eks update-kubeconfig` command or a `kubectl get nodes` smoke-check command for handoff into Kubernetes workflows.
362362

363-
The EC2 Instance Browser lists EC2 instances across available states for the active context and region, separate from the SSM session picker that only lists connectable running instances. The detail screen shows core metadata including instance ID, name tag, state, instance type, AZ, VPC, subnet, private and public IPs, launch time, platform details, IAM profile, and tags.
363+
The EC2 Instance Browser lists EC2 instances across available states for the active context and region, separate from the SSM session picker that only lists connectable running instances. The detail screen shows core metadata including instance ID, name tag, state, instance type, AZ, VPC, subnet, security groups, private and public IPs, launch time, platform details, IAM profile, and tags. From instance detail, related-resource drill-down screens open attached security groups (`g`), Auto Scaling membership (`a`), registered target groups (`t`), associated load balancers (`b`), and listeners (`n`). Related lists support filtering, refresh, wrap navigation, empty states for missing associations, and inline errors when a relationship cannot be loaded because of API or permission failures.
364364

365365
Bedrock API key management uses the active unic AWS context and IAM service-specific credential APIs for `bedrock.amazonaws.com`. The TUI lists long-term Bedrock API key metadata, opens a detail screen for inspection, defaults new key generation to the current IAM user when that user can be inferred from caller identity, and keeps another-user generation as an explicit option. Creation supports an optional expiration period, where blank or `0` means no expiration, rotates secrets with a one-time result screen, and deletes keys only after typed confirmation. Generated and rotated key values are intentionally copy-only and are not printed to the terminal; on the result screen, `c` copies the key and `e` copies `export AWS_BEARER_TOKEN_BEDROCK=...`.
366366

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/aws/aws-sdk-go-v2 v1.41.6
77
github.com/aws/aws-sdk-go-v2/config v1.32.12
88
github.com/aws/aws-sdk-go-v2/credentials v1.19.12
9+
github.com/aws/aws-sdk-go-v2/service/autoscaling v1.61.1
910
github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.55.9
1011
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.56.0
1112
github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.68.0
@@ -15,6 +16,7 @@ require (
1516
github.com/aws/aws-sdk-go-v2/service/ecs v1.76.0
1617
github.com/aws/aws-sdk-go-v2/service/eks v1.82.1
1718
github.com/aws/aws-sdk-go-v2/service/elasticache v1.52.0
19+
github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.54.11
1820
github.com/aws/aws-sdk-go-v2/service/guardduty v1.75.0
1921
github.com/aws/aws-sdk-go-v2/service/iam v1.53.7
2022
github.com/aws/aws-sdk-go-v2/service/lambda v1.89.0

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72
1818
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
1919
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 h1:rWyie/PxDRIdhNf4DzRk0lvjVOqFJuNnO8WwaIRVxzQ=
2020
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22/go.mod h1:zd/JsJ4P7oGfUhXn1VyLqaRZwPmZwg44Jf2dS84Dm3Y=
21+
github.com/aws/aws-sdk-go-v2/service/autoscaling v1.61.1 h1:VB+9RFYfUUY8TyL6W025CZToo6h9vC3zeYob7M7/2CE=
22+
github.com/aws/aws-sdk-go-v2/service/autoscaling v1.61.1/go.mod h1:6q/I1pH386VpPfB6FE62X/MOs6NW/oCsY9FXU33YXOU=
2123
github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.55.9 h1:ndjU1c3H1BYJy7fG0Y6Nh/sgD2M/KN+PZaqyCK/Fdfs=
2224
github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.55.9/go.mod h1:aH6yXmBYSr1k8uycfqxH5pv4T5N3x2cfFPvrlhyWjAU=
2325
github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.56.0 h1:ud2A364lLBkhGAC7oYw/1xg9BF4acwJC+qdLykxy83o=
@@ -36,6 +38,8 @@ github.com/aws/aws-sdk-go-v2/service/eks v1.82.1 h1:xTzXiQ8Q6U4ACdMNSCm72zd4Ds7Q
3638
github.com/aws/aws-sdk-go-v2/service/eks v1.82.1/go.mod h1:jjcGpziR11RTrr3JIgXg/Nn8GSwK3WOz2z1v/RqEBUI=
3739
github.com/aws/aws-sdk-go-v2/service/elasticache v1.52.0 h1:inluxH5ArTlQNGrFxP7RN5o5DEfP8bRbkPC/408Esgs=
3840
github.com/aws/aws-sdk-go-v2/service/elasticache v1.52.0/go.mod h1:DxywiXnEB21757xcql9xCqgt8vyTxSB7tVEIOdfKIY8=
41+
github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.54.11 h1:0iNKMyO0SXuRfl5FF6TQASAHTXnTYZlQS3/oJSfpEbQ=
42+
github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.54.11/go.mod h1:WdVArS8riWgCC1JoIVmKdqfBfj2+cSWAjBU5dEapMhA=
3943
github.com/aws/aws-sdk-go-v2/service/guardduty v1.75.0 h1:sZA3jpOkwrinRL0B5aZY6npTjDmmBCJRY51dee1Oh7Q=
4044
github.com/aws/aws-sdk-go-v2/service/guardduty v1.75.0/go.mod h1:lv/r48VXsENinEcSlNRYIERuliYk9dyc919tS+NWqW0=
4145
github.com/aws/aws-sdk-go-v2/service/iam v1.53.7 h1:n9YLiWtX3+6pTLZWvRJmtq5JIB9NA/KFelyCg5fOlTU=

internal/app/app.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ const (
2525
screenInstanceList
2626
screenEC2InstanceBrowserList
2727
screenEC2InstanceBrowserDetail
28+
screenEC2InstanceBrowserRelatedList
29+
screenEC2InstanceBrowserRelatedDetail
2830
screenVPCList
2931
screenSubnetList
3032
screenSubnetDetail

internal/app/app_test.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
"github.com/charmbracelet/bubbles/spinner"
1111
tea "github.com/charmbracelet/bubbletea"
12+
"github.com/charmbracelet/lipgloss"
1213

1314
"unic/internal/config"
1415
"unic/internal/domain"
@@ -472,6 +473,123 @@ func TestEC2BrowserDetailViewShowsMetadata(t *testing.T) {
472473
}
473474
}
474475

476+
func TestEC2BrowserDetailViewKeepsRowsSeparateAndClipped(t *testing.T) {
477+
m := New(testConfig(), "", "dev")
478+
m.screen = screenEC2InstanceBrowserDetail
479+
m.width = 96
480+
m.height = 30
481+
m.ec2Browser.selected = &awsservice.EC2Instance{
482+
InstanceID: "i-0f237d69dbc37d27f",
483+
Name: "plantadmext-plted_apnortheast2-v075",
484+
State: "running",
485+
InstanceType: "t4g.medium",
486+
SecurityGroups: []awsservice.EC2InstanceSecurityGroup{
487+
{GroupID: "sg-054e6a98f4a972b67", Name: "plantadmext-plted_apnortheast2"},
488+
{GroupID: "sg-0844134f83a41ab59", Name: "vpc-00a9c49eb2111f495-querypie-joined-sg-260205124724"},
489+
},
490+
Tags: map[string]string{
491+
"ansible-tags": "all app plantadmext apps_version plantadmext-release-202604221456-2f31d19c.jar",
492+
"aws:autoscaling:groupName": "coreprobeworker-plted_apnortheast2-v020",
493+
"aws:ec2launchtemplate:id": "lt-09f7820d193aba7d1",
494+
"branch": "main service_jar_file=plantadmext/main/plantadmext-release-202604221456-2f31d19c.jar",
495+
},
496+
}
497+
498+
view := stripANSI(m.View())
499+
lines := strings.Split(view, "\n")
500+
for _, line := range lines {
501+
if width := lipgloss.Width(line); width > m.width {
502+
t.Fatalf("expected EC2 detail line to fit width %d, got %d: %q", m.width, width, line)
503+
}
504+
}
505+
for _, want := range []string{"Instance ID", "Security Groups", "Tags", "ansible-tags", "branch"} {
506+
found := false
507+
for _, line := range lines {
508+
if strings.Contains(line, want) {
509+
found = true
510+
break
511+
}
512+
}
513+
if !found {
514+
t.Fatalf("expected EC2 detail view to contain separate row %q, got %q", want, view)
515+
}
516+
}
517+
for _, line := range lines {
518+
if strings.Contains(line, "oupName") && !strings.Contains(line, "aws:autoscaling:groupName") {
519+
t.Fatalf("expected long tag key to stay on one row without wrapping, got fragment row %q", line)
520+
}
521+
}
522+
}
523+
524+
func TestEC2BrowserRelatedListNavigationAndFilter(t *testing.T) {
525+
m := New(testConfig(), "", "dev")
526+
m.screen = screenEC2InstanceBrowserRelatedList
527+
m.ec2Browser.selected = &awsservice.EC2Instance{InstanceID: "i-123", Name: "prod-web"}
528+
m.ec2Browser.relatedKind = ec2RelatedTargetGroups
529+
m.ec2Browser.relatedItems = []ec2RelatedItem{
530+
{
531+
title: "prod-tg HTTP:80 [healthy]",
532+
filter: "prod-tg http healthy",
533+
details: []detailRow{
534+
{"Name", "prod-tg"},
535+
},
536+
},
537+
{
538+
title: "dev-tg HTTP:8080 [unused]",
539+
filter: "dev-tg http unused",
540+
details: []detailRow{
541+
{"Name", "dev-tg"},
542+
},
543+
},
544+
}
545+
m.ec2Browser.filteredRelated = m.ec2Browser.relatedItems
546+
547+
updated, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'k'}})
548+
model := updated.(Model)
549+
if model.ec2Browser.relatedIdx != 1 {
550+
t.Fatalf("expected related list up from first to wrap to last, got %d", model.ec2Browser.relatedIdx)
551+
}
552+
553+
updated, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'/'}})
554+
model = updated.(Model)
555+
for _, ch := range []rune("prod") {
556+
updated, _ = model.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{ch}})
557+
model = updated.(Model)
558+
}
559+
if len(model.ec2Browser.filteredRelated) != 1 {
560+
t.Fatalf("expected 1 filtered related item, got %d", len(model.ec2Browser.filteredRelated))
561+
}
562+
if model.ec2Browser.filteredRelated[0].title != "prod-tg HTTP:80 [healthy]" {
563+
t.Fatalf("unexpected filtered related item: %+v", model.ec2Browser.filteredRelated[0])
564+
}
565+
}
566+
567+
func TestEC2RelationshipsLoadedOpensRequestedRelatedList(t *testing.T) {
568+
m := New(testConfig(), "", "dev")
569+
m.screen = screenLoading
570+
msg := ec2RelationshipsLoadedMsg{
571+
kind: ec2RelatedLoadBalancers,
572+
relationships: &awsservice.EC2InstanceRelationships{
573+
InstanceID: "i-123",
574+
LoadBalancers: []awsservice.EC2LoadBalancer{
575+
{Name: "app-lb", DNSName: "app.example.com", Type: "application", State: "active"},
576+
},
577+
},
578+
}
579+
580+
updated, _ := m.Update(msg)
581+
model := updated.(Model)
582+
if model.screen != screenEC2InstanceBrowserRelatedList {
583+
t.Fatalf("expected related list screen, got %d", model.screen)
584+
}
585+
if model.ec2Browser.relatedKind != ec2RelatedLoadBalancers {
586+
t.Fatalf("expected load balancer related kind, got %q", model.ec2Browser.relatedKind)
587+
}
588+
if len(model.ec2Browser.relatedItems) != 1 || !strings.Contains(model.ec2Browser.relatedItems[0].title, "app-lb") {
589+
t.Fatalf("unexpected related items: %+v", model.ec2Browser.relatedItems)
590+
}
591+
}
592+
475593
func TestReachabilityFeatureOpensRegionSelection(t *testing.T) {
476594
m := New(testConfig(), "", "dev")
477595
m.screen = screenFeatureList

internal/app/filter.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const (
1212
filterServices
1313
filterInstances
1414
filterEC2BrowserInstances
15+
filterEC2BrowserRelated
1516
filterSubnetIPs
1617
filterRDS
1718
filterRoute53Zones

internal/app/help.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,19 @@ func (m Model) currentScreenShortcuts() []helpShortcut {
136136
return listScreenShortcuts("open the selected instance", "go back to the feature list", true, false)
137137
case screenEC2InstanceBrowserDetail:
138138
return []helpShortcut{
139+
{"g", "Open attached security groups"},
140+
{"a", "Open Auto Scaling membership"},
141+
{"t", "Open registered target groups"},
142+
{"b", "Open associated load balancers"},
143+
{"n", "Open associated listeners"},
139144
{"q / esc", "Go back to the instance list"},
140145
}
146+
case screenEC2InstanceBrowserRelatedList:
147+
return listScreenShortcuts("open the selected related resource", "go back to the instance detail", true, true)
148+
case screenEC2InstanceBrowserRelatedDetail:
149+
return []helpShortcut{
150+
{"q / esc", "Go back to the related resource list"},
151+
}
141152
case screenVPCList:
142153
return []helpShortcut{
143154
{"↑/↓, j/k", "Move between VPCs"},
@@ -746,6 +757,10 @@ func (m Model) helpScreenTitle() string {
746757
return "EC2 Instance Browser"
747758
case screenEC2InstanceBrowserDetail:
748759
return "EC2 Instance Detail"
760+
case screenEC2InstanceBrowserRelatedList:
761+
return "EC2 Related Resources"
762+
case screenEC2InstanceBrowserRelatedDetail:
763+
return "EC2 Related Resource Detail"
749764
case screenVPCList:
750765
return "VPC List"
751766
case screenSubnetList:

internal/app/messages.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ type ec2BrowserInstancesLoadedMsg struct {
1515
instances []awsservice.EC2Instance
1616
}
1717

18+
type ec2RelationshipsLoadedMsg struct {
19+
relationships *awsservice.EC2InstanceRelationships
20+
kind ec2RelatedKind
21+
}
22+
1823
type vpcsLoadedMsg struct {
1924
vpcs []awsservice.VPC
2025
}

0 commit comments

Comments
 (0)