Skip to content

Commit eeee872

Browse files
authored
Merge pull request #76 from DevopsArtFactory/codex/issue-58-s3-browser
feat: add S3 bucket browser
2 parents 083cda6 + 161bcfa commit eeee872

13 files changed

Lines changed: 1085 additions & 1 deletion

File tree

go.mod

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,14 @@ require (
2525
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
2626
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
2727
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect
28+
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 // indirect
2829
github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.68.0 // indirect
2930
github.com/aws/aws-sdk-go-v2/service/iam v1.53.7 // indirect
3031
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
31-
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 // indirect
32+
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect
33+
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
34+
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect
35+
github.com/aws/aws-sdk-go-v2/service/s3 v1.98.0 // indirect
3236
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect
3337
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect
3438
github.com/aws/smithy-go v1.24.2 // indirect

go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgq
2020
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps=
2121
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw=
2222
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
23+
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 h1:rWyie/PxDRIdhNf4DzRk0lvjVOqFJuNnO8WwaIRVxzQ=
24+
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22/go.mod h1:zd/JsJ4P7oGfUhXn1VyLqaRZwPmZwg44Jf2dS84Dm3Y=
2325
github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.68.0 h1:+/lmB/+i2oqkzbmlQxsW0kr/+wmJgmyiEF9VDJicX34=
2426
github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.68.0/go.mod h1:PobeppEnIjw4pcgjFryNDZCTH7AiqZw0yb5r98Gvf9c=
2527
github.com/aws/aws-sdk-go-v2/service/ec2 v1.296.0 h1:98Miqj16un1WLNyM1RjVDhXYumhqZrQfAeG8i4jPG6o=
@@ -28,12 +30,20 @@ github.com/aws/aws-sdk-go-v2/service/iam v1.53.7 h1:n9YLiWtX3+6pTLZWvRJmtq5JIB9N
2830
github.com/aws/aws-sdk-go-v2/service/iam v1.53.7/go.mod h1:sP46Vo6MeJcM4s0ZXcG2PFmfiSyixhIuC/74W52yKuk=
2931
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
3032
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
33+
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 h1:JRaIgADQS/U6uXDqlPiefP32yXTda7Kqfx+LgspooZM=
34+
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13/go.mod h1:CEuVn5WqOMilYl+tbccq8+N2ieCy0gVn3OtRb0vBNNM=
3135
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y=
3236
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk=
37+
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto=
38+
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA=
39+
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 h1:ZlvrNcHSFFWURB8avufQq9gFsheUgjVD9536obIknfM=
40+
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21/go.mod h1:cv3TNhVrssKR0O/xxLJVRfd2oazSnZnkUeTf6ctUwfQ=
3341
github.com/aws/aws-sdk-go-v2/service/rds v1.116.3 h1:H/ZYZ6QR4EXJAYElI5xkIM/yCz+A4uHIvWpzl+IfJks=
3442
github.com/aws/aws-sdk-go-v2/service/rds v1.116.3/go.mod h1:QbXW4coAMakHQhf1qhE0eVVCen9gwB/Kvn+HHHKhpGY=
3543
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.4 h1:64aYPyHg3RjLvnMMSYQSg7aP+r1WRCPIS9SP9KfHjWg=
3644
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.4/go.mod h1:bPSPzWTn9LSX6e0KPp4LlPoaspouZdKAlIdSMdhBBrs=
45+
github.com/aws/aws-sdk-go-v2/service/s3 v1.98.0 h1:foqo/ocQ7WqKwy3FojGtZQJo0FR4vto9qnz9VaumbCo=
46+
github.com/aws/aws-sdk-go-v2/service/s3 v1.98.0/go.mod h1:uoA43SdFwacedBfSgfFSjjCvYe8aYBS7EnU5GZ/YKMM=
3747
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.4 h1:9aZbO86sraeCIHHCpZhxwN9tnVy9POkSKzi4/TpT54A=
3848
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.4/go.mod h1:cxiXDhEzIq7Xx1BtmC4lGBK3SwAZ79+EUWiKawYHo14=
3949
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow=

internal/app/app.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ const (
4848
screenCWLogGroupList
4949
screenCWLogStreamList
5050
screenCWLogViewer
51+
screenS3BucketList
52+
screenS3ObjectList
53+
screenS3ObjectDetail
5154
screenContextPicker
5255
screenContextAdd
5356
screenLoading
@@ -198,6 +201,22 @@ type Model struct {
198201
cwLogTailing bool // live tail active
199202
cwLogTailToken *string
200203

204+
// S3 browser state
205+
s3Buckets []awsservice.S3Bucket
206+
filteredS3Buckets []awsservice.S3Bucket
207+
s3BucketIdx int
208+
s3BucketFilter string
209+
s3BucketFilterActive bool
210+
selectedS3Bucket *awsservice.S3Bucket
211+
s3Objects []awsservice.S3Object
212+
filteredS3Objects []awsservice.S3Object
213+
s3ObjectIdx int
214+
s3ObjectFilter string
215+
s3ObjectFilterActive bool
216+
s3CurrentPrefix string
217+
s3PrefixStack []string
218+
selectedS3Object *awsservice.S3ObjectDetail
219+
201220
// Context picker
202221
configPath string
203222
ctxList []config.ContextInfo
@@ -310,6 +329,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
310329
m.handleIAMMsg,
311330
m.handleSecretMsg,
312331
m.handleCloudWatchLogsMsg,
332+
m.handleS3Msg,
313333
m.handleContextMsg,
314334
} {
315335
if newM, cmd, handled := h(msg); handled {
@@ -378,6 +398,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
378398
return m.updateCWLogStreamList(msg)
379399
case screenCWLogViewer:
380400
return m.updateCWLogViewer(msg)
401+
case screenS3BucketList:
402+
return m.updateS3BucketList(msg)
403+
case screenS3ObjectList:
404+
return m.updateS3ObjectList(msg)
405+
case screenS3ObjectDetail:
406+
return m.updateS3ObjectDetail(msg)
381407
case screenSecurityGroupList:
382408
return m.updateSecurityGroupList(msg)
383409
case screenSecurityGroupDetail:
@@ -487,6 +513,9 @@ func (m Model) updateFeatureList(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
487513
case domain.FeatureCloudWatchLogsBrowser:
488514
m.screen = screenLoading
489515
return m, m.loadCWLogGroups()
516+
case domain.FeatureS3Browser:
517+
m.screen = screenLoading
518+
return m, m.loadS3Buckets()
490519
case domain.FeatureSecurityGroupBrowser:
491520
m.screen = screenLoading
492521
return m, m.loadSecurityGroups()
@@ -566,6 +595,12 @@ func (m Model) View() string {
566595
v = m.viewCWLogStreamList()
567596
case screenCWLogViewer:
568597
v = m.viewCWLogViewer()
598+
case screenS3BucketList:
599+
v = m.viewS3BucketList()
600+
case screenS3ObjectList:
601+
v = m.viewS3ObjectList()
602+
case screenS3ObjectDetail:
603+
v = m.viewS3ObjectDetail()
569604
case screenSecurityGroupList:
570605
v = m.viewSecurityGroupList()
571606
case screenSecurityGroupDetail:

internal/app/app_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,6 +1090,70 @@ func TestRotateAccessKeyFeatureUsesCurrentIdentityFlow(t *testing.T) {
10901090
}
10911091
}
10921092

1093+
func TestS3BrowserFeatureExistsInCatalog(t *testing.T) {
1094+
m := New(testConfig(), "", "dev")
1095+
1096+
for _, svc := range m.services {
1097+
if svc.Name != domain.ServiceS3 {
1098+
continue
1099+
}
1100+
if len(svc.Features) != 1 {
1101+
t.Fatalf("expected 1 S3 feature, got %d", len(svc.Features))
1102+
}
1103+
if svc.Features[0].Kind != domain.FeatureS3Browser {
1104+
t.Fatalf("expected S3 browser feature, got %s", svc.Features[0].Kind)
1105+
}
1106+
return
1107+
}
1108+
1109+
t.Fatal("expected S3 service in catalog")
1110+
}
1111+
1112+
func TestS3FeatureStartsBucketLoading(t *testing.T) {
1113+
m := New(testConfig(), "", "dev")
1114+
for _, svc := range m.services {
1115+
if svc.Name == domain.ServiceS3 {
1116+
m.features = svc.Features
1117+
break
1118+
}
1119+
}
1120+
m.screen = screenFeatureList
1121+
1122+
updated, cmd := m.Update(tea.KeyMsg{Type: tea.KeyEnter})
1123+
model := updated.(Model)
1124+
if model.screen != screenLoading {
1125+
t.Fatalf("expected loading screen, got %d", model.screen)
1126+
}
1127+
if cmd == nil {
1128+
t.Fatal("expected load S3 buckets command")
1129+
}
1130+
}
1131+
1132+
func TestS3ObjectListEscAtRootReturnsToBucketList(t *testing.T) {
1133+
m := New(testConfig(), "", "dev")
1134+
m.screen = screenS3ObjectList
1135+
m.selectedS3Bucket = &awsservice.S3Bucket{Name: "my-bucket"}
1136+
m.s3CurrentPrefix = ""
1137+
1138+
updated, _ := m.Update(tea.KeyMsg{Type: tea.KeyEsc})
1139+
model := updated.(Model)
1140+
if model.screen != screenS3BucketList {
1141+
t.Fatalf("expected bucket list screen, got %d", model.screen)
1142+
}
1143+
}
1144+
1145+
func TestS3ObjectListShowsBreadcrumb(t *testing.T) {
1146+
m := New(testConfig(), "", "dev")
1147+
m.screen = screenS3ObjectList
1148+
m.selectedS3Bucket = &awsservice.S3Bucket{Name: "my-bucket"}
1149+
m.s3CurrentPrefix = "logs/app/"
1150+
1151+
view := m.viewS3ObjectList()
1152+
if !strings.Contains(view, "Path: /logs/app") {
1153+
t.Fatalf("expected breadcrumb in S3 object list, got %q", view)
1154+
}
1155+
}
1156+
10931157
func TestCWLogViewerDownDoesNotOverflowShortEventList(t *testing.T) {
10941158
m := New(testConfig(), "", "dev")
10951159
m.screen = screenCWLogViewer

internal/app/messages.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,20 @@ type cwLogEventsLoadedMsg struct {
106106

107107
type cwLogTailTickMsg struct{}
108108

109+
type s3BucketsLoadedMsg struct {
110+
buckets []awsservice.S3Bucket
111+
}
112+
113+
type s3ObjectsLoadedMsg struct {
114+
bucket string
115+
prefix string
116+
objects awsservice.S3ListResult
117+
}
118+
119+
type s3ObjectDetailLoadedMsg struct {
120+
detail *awsservice.S3ObjectDetail
121+
}
122+
109123
type secretsLoadedMsg struct {
110124
secrets []awsservice.Secret
111125
}

0 commit comments

Comments
 (0)