Skip to content

Commit 111f7cd

Browse files
authored
Merge pull request #193 from DevopsArtFactory/feature/168-eks-access-helper
feat: add EKS access helper for kubeconfig commands
2 parents 2204bd2 + 405e88b commit 111f7cd

12 files changed

Lines changed: 237 additions & 14 deletions

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ checks:
342342
| ECR Login | CLI helper: `unic ecr login [--runtime docker|podman] [--copy]` |
343343
| ECS Exec | `r` refresh, `Enter` drill down / exec |
344344
| ECS Rollout / Exec | cluster/service lists support refresh and drill-down, service detail shows deployments/task definition images/events, `Enter` continues into tasks and exec |
345-
| EKS Browser | cluster/node group/add-on lists support `/` filter and `r` refresh, cluster view shows version/status/endpoint visibility/ARN summary, `a` opens managed add-ons, `U` opens current-version upgrade readiness, node group detail shows desired/min/max scaling plus health issues |
345+
| EKS Browser | cluster/node group/add-on lists support `/` filter and `r` refresh, cluster view shows version/status/endpoint visibility/ARN summary, `a` opens managed add-ons, `U` opens current-version upgrade readiness, `u` opens kubeconfig access helper, node group detail shows desired/min/max scaling plus health issues |
346346
| Inspector Mode | `i` open mode from the service list, `Enter` open the selected workflow, `l` open the checklist file picker |
347347
| Security Inspector | `r` run/rescan, `1`-`5` severity filter, `Enter` finding detail |
348348
| Checklist Inspector | `l` load or switch checklist files, `r` run/rerun the loaded checklist, `Enter` result detail |
@@ -358,6 +358,8 @@ The ECR Repository Browser opens image/tag lists from each repository. Image row
358358

359359
The EKS Browser includes a current-version upgrade readiness view for each selected cluster. It compares the control plane version with managed node group versions, checks installed managed add-on versions against EKS compatibility metadata for the current cluster version, includes EKS `UPGRADE_READINESS` insights, and highlights blockers or warnings before planning a target-version upgrade.
360360

361+
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.
362+
361363
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.
362364

363365
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=...`.

docs/architecture.en.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ Current screen families include:
194194
- CloudWatch metric list/detail flows
195195
- CloudWatch Logs group/stream/viewer flows
196196
- ECS cluster/service/rollout detail/task/container flows
197-
- EKS cluster/node group/add-on status and upgrade readiness flows
197+
- EKS cluster/node group/add-on status, upgrade readiness, and access helper flows
198198
- ECR repository/image/detail flows
199199
- S3 bucket/object/detail flows
200200
- Inspector mode home, checklist setup, security findings/detail, and checklist results/detail flows

docs/architecture.ko.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ UNIC은 현재 세 가지 인증 모드를 지원한다.
194194
- CloudWatch metric list/detail
195195
- CloudWatch Logs group/stream/viewer
196196
- ECS cluster/service/rollout detail/task/container
197-
- EKS cluster/node group/add-on status, upgrade readiness
197+
- EKS cluster/node group/add-on status, upgrade readiness, access helper
198198
- ECR repository/image/detail
199199
- S3 bucket/object/detail
200200
- Inspector mode home, checklist setup, security findings/detail, checklist results/detail

docs/project-overview.en.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Implemented service areas currently include:
2424
- Lambda
2525
- Inspector mode
2626

27-
The application already includes interactive mutation flows, polling-based status flows, context helpers, and per-service drill-down screens. CloudWatch Metrics now includes resource-centric preset groups plus time-range, period, and statistic controls for faster terminal triage. EKS includes managed add-on status review plus current-version upgrade readiness checks that compare control plane, managed node group, managed add-on version alignment, and EKS upgrade insights before a target upgrade is planned. ECR includes repository and image/tag browsing with cleanup-oriented untagged and stale image signals.
27+
The application already includes interactive mutation flows, polling-based status flows, context helpers, and per-service drill-down screens. CloudWatch Metrics now includes resource-centric preset groups plus time-range, period, and statistic controls for faster terminal triage. EKS includes managed add-on status review, current-version upgrade readiness checks that compare control plane, managed node group, managed add-on version alignment, and EKS upgrade insights before a target upgrade is planned, plus a kubeconfig access helper that prepares copyable `aws eks update-kubeconfig` and `kubectl` handoff commands. ECR includes repository and image/tag browsing with cleanup-oriented untagged and stale image signals.
2828
Inspector mode now includes built-in security scans plus checklist-driven readiness checks for RDS, security groups, secrets, Route53, VPCs/subnets, CloudWatch Logs, and baseline posture wrappers.
2929

3030
## Primary User Flows

docs/project-overview.ko.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ UNIC은 다음 세 가지를 결합한 Go 기반 AWS 터미널 콘솔이다.
2424
- Lambda
2525
- Inspector mode
2626

27-
애플리케이션은 이미 상호작용형 변경 작업 플로우, polling 기반 상태 확인, context helper, 서비스별 drill-down 화면을 포함한다. CloudWatch Metrics는 이제 resource-centric preset 그룹과 time-range / period / statistic control을 제공해 터미널에서 더 빠르게 triage할 수 있다. EKS는 cluster 화면에서 managed add-on 상태를 확인하고, target upgrade를 계획하기 전에 control plane, managed node group, managed add-on의 current-version alignment와 EKS upgrade insight를 함께 확인하는 upgrade readiness check를 포함한다. ECR은 repository와 image/tag 탐색을 제공하고, untagged image와 오래된 image를 cleanup 후보로 드러낸다.
27+
애플리케이션은 이미 상호작용형 변경 작업 플로우, polling 기반 상태 확인, context helper, 서비스별 drill-down 화면을 포함한다. CloudWatch Metrics는 이제 resource-centric preset 그룹과 time-range / period / statistic control을 제공해 터미널에서 더 빠르게 triage할 수 있다. EKS는 cluster 화면에서 managed add-on 상태를 확인하고, target upgrade를 계획하기 전에 control plane, managed node group, managed add-on의 current-version alignment와 EKS upgrade insight를 함께 확인하는 upgrade readiness check와 복사 가능한 `aws eks update-kubeconfig` / `kubectl` handoff 명령을 준비하는 kubeconfig access helper를 포함한다. ECR은 repository와 image/tag 탐색을 제공하고, untagged image와 오래된 image를 cleanup 후보로 드러낸다.
2828
Inspector mode는 이제 built-in security scan과 함께 RDS, security group, secret, Route53, VPC/subnet, CloudWatch Logs, baseline posture wrapper를 다루는 checklist 기반 readiness check도 포함한다.
2929

3030
## 주요 사용자 흐름

internal/app/app.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ const (
6666
screenECSContainerList
6767
screenEKSClusterList
6868
screenEKSUpgradeReadiness
69+
screenEKSAccessHelper
6970
screenEKSNodeGroupList
7071
screenEKSNodeGroupDetail
7172
screenEKSAddonList
@@ -151,6 +152,7 @@ type Model struct {
151152
selectedEKSCluster *awsservice.EKSCluster
152153
eksUpgradeReadiness *awsservice.EKSUpgradeReadiness
153154
eksUpgradeScroll int
155+
eksAccessCopyMsg string
154156

155157
eksNodeGroups []awsservice.EKSNodeGroup
156158
filteredEKSNodeGroups []awsservice.EKSNodeGroup
@@ -492,6 +494,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
492494
return m.updateEKSClusterList(msg)
493495
case screenEKSUpgradeReadiness:
494496
return m.updateEKSUpgradeReadiness(msg)
497+
case screenEKSAccessHelper:
498+
return m.updateEKSAccessHelper(msg)
495499
case screenEKSNodeGroupList:
496500
return m.updateEKSNodeGroupList(msg)
497501
case screenEKSNodeGroupDetail:
@@ -688,6 +692,8 @@ func (m Model) View() string {
688692
v = m.viewEKSClusterList()
689693
case screenEKSUpgradeReadiness:
690694
v = m.viewEKSUpgradeReadiness()
695+
case screenEKSAccessHelper:
696+
v = m.viewEKSAccessHelper()
691697
case screenEKSNodeGroupList:
692698
v = m.viewEKSNodeGroupList()
693699
case screenEKSNodeGroupDetail:

internal/app/help.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,9 @@ func (m Model) currentScreenShortcuts() []helpShortcut {
411411
{"/", "Filter clusters"},
412412
{"r", "Refresh clusters"},
413413
{"enter", "Open managed node groups for the selected cluster"},
414+
{"a", "Open managed add-ons for the selected cluster"},
414415
{"U", "Open current-version upgrade readiness for the selected cluster"},
416+
{"u", "Open the kubeconfig access helper for the selected cluster"},
415417
{"q / esc", "Go back to the feature list"},
416418
}
417419
case screenEKSUpgradeReadiness:
@@ -422,6 +424,13 @@ func (m Model) currentScreenShortcuts() []helpShortcut {
422424
{"esc", "Go back to the cluster list"},
423425
{"q", "Go back to the feature list"},
424426
}
427+
case screenEKSAccessHelper:
428+
return []helpShortcut{
429+
{"c", "Copy the aws eks update-kubeconfig command"},
430+
{"k", "Copy the kubectl smoke-check command"},
431+
{"esc", "Go back to the cluster list"},
432+
{"q", "Go back to the feature list"},
433+
}
425434
case screenEKSNodeGroupList:
426435
return listScreenShortcuts("open the selected node group", "go back to the cluster list", true, true)
427436
case screenEKSNodeGroupDetail:
@@ -819,6 +828,8 @@ func (m Model) helpScreenTitle() string {
819828
return "EKS Clusters"
820829
case screenEKSUpgradeReadiness:
821830
return "EKS Upgrade Readiness"
831+
case screenEKSAccessHelper:
832+
return "EKS Access Helper"
822833
case screenEKSNodeGroupList:
823834
return "EKS Node Groups"
824835
case screenEKSNodeGroupDetail:

internal/app/screen_eks.go

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
tea "github.com/charmbracelet/bubbletea"
1010

11+
"unic/internal/clipboard"
1112
awsservice "unic/internal/services/aws"
1213
)
1314

@@ -22,6 +23,7 @@ func (m Model) handleEKSMsg(msg tea.Msg) (tea.Model, tea.Cmd, bool) {
2223
m.selectedEKSCluster = nil
2324
m.eksUpgradeReadiness = nil
2425
m.eksUpgradeScroll = 0
26+
m.eksAccessCopyMsg = ""
2527
m.eksNodeGroups = nil
2628
m.filteredEKSNodeGroups = nil
2729
m.selectedEKSNodeGroup = nil
@@ -88,6 +90,13 @@ func (m Model) updateEKSClusterList(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
8890
m.selectedEKSCluster = &cluster
8991
return m.startLoading(m.loadEKSUpgradeReadiness())
9092
}
93+
case "u":
94+
if len(m.filteredEKSClusters) > 0 && m.eksClusterIdx < len(m.filteredEKSClusters) {
95+
cluster := m.filteredEKSClusters[m.eksClusterIdx]
96+
m.selectedEKSCluster = &cluster
97+
m.eksAccessCopyMsg = ""
98+
m.screen = screenEKSAccessHelper
99+
}
91100
case "enter":
92101
if len(m.filteredEKSClusters) > 0 && m.eksClusterIdx < len(m.filteredEKSClusters) {
93102
cluster := m.filteredEKSClusters[m.eksClusterIdx]
@@ -146,7 +155,7 @@ func (m Model) viewEKSClusterList() string {
146155
b.WriteString(renderDetailLine("ARN", cluster.ARN))
147156
b.WriteString("\n\n")
148157
}
149-
b.WriteString(m.renderHelpBar("↑/↓: navigate • /: filter • enter: node groups • a: add-ons • U: readiness • r: refresh • esc: back • H: home"))
158+
b.WriteString(m.renderHelpBar("↑/↓: navigate • /: filter • enter: node groups • a: add-ons • U: readiness • u: access helper • r: refresh • esc: back • H: home"))
150159
return b.String()
151160
}
152161

@@ -210,6 +219,29 @@ func (m Model) viewEKSUpgradeReadiness() string {
210219
return b.String()
211220
}
212221

222+
func (m Model) updateEKSAccessHelper(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
223+
switch msg.String() {
224+
case "q":
225+
m.screen = screenFeatureList
226+
case "esc":
227+
m.eksAccessCopyMsg = ""
228+
m.screen = screenEKSClusterList
229+
case "c":
230+
if err := clipboard.Copy(m.eksUpdateKubeconfigCommand()); err != nil {
231+
m.eksAccessCopyMsg = fmt.Sprintf("Clipboard error: %s", err)
232+
} else {
233+
m.eksAccessCopyMsg = "Copied update-kubeconfig command"
234+
}
235+
case "k":
236+
if err := clipboard.Copy(awsservice.BuildEKSKubectlSmokeCommand()); err != nil {
237+
m.eksAccessCopyMsg = fmt.Sprintf("Clipboard error: %s", err)
238+
} else {
239+
m.eksAccessCopyMsg = "Copied kubectl command"
240+
}
241+
}
242+
return m, nil
243+
}
244+
213245
func (m Model) eksUpgradeReadinessLines() []string {
214246
if m.eksUpgradeReadiness == nil {
215247
return []string{dimStyle.Render("No readiness data loaded")}
@@ -277,6 +309,66 @@ func (m Model) eksUpgradeReadinessLines() []string {
277309
return lines
278310
}
279311

312+
func (m Model) viewEKSAccessHelper() string {
313+
var b strings.Builder
314+
b.WriteString(m.renderStatusBar())
315+
title := "EKS Access Helper"
316+
if m.selectedEKSCluster != nil {
317+
title = fmt.Sprintf("EKS Access Helper — %s", m.selectedEKSCluster.Name)
318+
}
319+
b.WriteString(titleStyle.Render(title))
320+
b.WriteString("\n\n")
321+
322+
cluster := m.selectedEKSCluster
323+
if cluster == nil {
324+
b.WriteString(dimStyle.Render("No EKS cluster selected"))
325+
b.WriteString("\n\n")
326+
b.WriteString(m.renderHelpBar("esc: clusters • H: home"))
327+
return b.String()
328+
}
329+
330+
var panel strings.Builder
331+
panel.WriteString(renderDetailLine("Cluster", cluster.Name))
332+
panel.WriteString("\n")
333+
panel.WriteString(renderDetailLine("Region", firstNonEmpty(m.cfg.Region, "-")))
334+
panel.WriteString("\n")
335+
panel.WriteString(renderDetailLine("Profile", firstNonEmpty(m.cfg.Profile, "-")))
336+
panel.WriteString("\n")
337+
panel.WriteString(renderDetailLine("Context", firstNonEmpty(m.cfg.ContextName, "-")))
338+
panel.WriteString("\n")
339+
panel.WriteString(renderDetailLine("Endpoint", firstNonEmpty(cluster.Endpoint, "-")))
340+
panel.WriteString("\n")
341+
panel.WriteString(renderDetailLine("ARN", cluster.ARN))
342+
panel.WriteString("\n\n")
343+
panel.WriteString(titleStyle.Render("Commands"))
344+
panel.WriteString("\n")
345+
panel.WriteString(" [c] " + m.eksUpdateKubeconfigCommand())
346+
panel.WriteString("\n")
347+
panel.WriteString(" [k] " + awsservice.BuildEKSKubectlSmokeCommand())
348+
panel.WriteString("\n")
349+
if m.eksAccessCopyMsg != "" {
350+
panel.WriteString("\n")
351+
panel.WriteString(selectedStyle.Render(" " + m.eksAccessCopyMsg))
352+
panel.WriteString("\n")
353+
}
354+
355+
b.WriteString(m.renderListPanel(panel.String()))
356+
b.WriteString("\n\n")
357+
b.WriteString(m.renderHelpBar("c: copy kubeconfig • k: copy kubectl • esc: clusters • q: feature list • H: home"))
358+
return b.String()
359+
}
360+
361+
func (m Model) eksUpdateKubeconfigCommand() string {
362+
if m.selectedEKSCluster == nil {
363+
return ""
364+
}
365+
alias := m.selectedEKSCluster.Name
366+
if strings.TrimSpace(m.cfg.ContextName) != "" {
367+
alias = m.cfg.ContextName + "-" + m.selectedEKSCluster.Name
368+
}
369+
return awsservice.BuildEKSUpdateKubeconfigCommand(m.selectedEKSCluster.Name, m.cfg.Region, m.cfg.Profile, alias)
370+
}
371+
280372
func (m Model) updateEKSNodeGroupList(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
281373
if cmd, handled := m.updateSharedFilter(msg, filterEKSNodeGroups); handled {
282374
return m, cmd

internal/app/screen_eks_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,31 @@ func TestEKSClusterListALoadsAddons(t *testing.T) {
8080
}
8181
}
8282

83+
func TestEKSClusterListUOpensAccessHelper(t *testing.T) {
84+
m := New(testConfig(), "", "dev")
85+
m.screen = screenEKSClusterList
86+
m.eksClusters = []awsservice.EKSCluster{{
87+
Name: "prod-eks",
88+
ARN: "arn:aws:eks:ap-northeast-2:123456789012:cluster/prod-eks",
89+
Version: "1.32",
90+
Status: "ACTIVE",
91+
Endpoint: "https://prod-eks.eks.amazonaws.com",
92+
}}
93+
m.filteredEKSClusters = m.eksClusters
94+
95+
updated, cmd := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'u'}})
96+
model := updated.(Model)
97+
if cmd != nil {
98+
t.Fatal("expected no async command")
99+
}
100+
if model.selectedEKSCluster == nil || model.selectedEKSCluster.Name != "prod-eks" {
101+
t.Fatalf("unexpected selected cluster: %+v", model.selectedEKSCluster)
102+
}
103+
if model.screen != screenEKSAccessHelper {
104+
t.Fatalf("expected access helper screen, got %v", model.screen)
105+
}
106+
}
107+
83108
func TestEKSClusterListULoadsUpgradeReadiness(t *testing.T) {
84109
m := New(testConfig(), "", "dev")
85110
m.screen = screenEKSClusterList
@@ -238,6 +263,39 @@ func TestEKSAddonDetailViewShowsHealthIssues(t *testing.T) {
238263
}
239264
}
240265

266+
func TestEKSAccessHelperViewShowsCommands(t *testing.T) {
267+
m := New(testConfig(), "", "dev")
268+
m.screen = screenEKSAccessHelper
269+
m.selectedEKSCluster = &awsservice.EKSCluster{
270+
Name: "prod-eks",
271+
ARN: "arn:aws:eks:ap-northeast-2:123456789012:cluster/prod-eks",
272+
Endpoint: "https://prod-eks.eks.amazonaws.com",
273+
}
274+
275+
view := stripANSI(m.viewEKSAccessHelper())
276+
for _, want := range []string{
277+
"EKS Access Helper — prod-eks",
278+
"https://prod-eks.eks.amazonaws.com",
279+
`aws eks update-kubeconfig --name 'prod-eks' --region 'us-east-1' --profile 'default' --alias 'prod-eks'`,
280+
"kubectl get nodes",
281+
} {
282+
if !strings.Contains(view, want) {
283+
t.Fatalf("expected view to contain %q, got %q", want, view)
284+
}
285+
}
286+
}
287+
288+
func TestEKSAccessHelperAliasIncludesContextAndCluster(t *testing.T) {
289+
m := New(testConfig(), "", "dev")
290+
m.cfg.ContextName = "prod"
291+
m.selectedEKSCluster = &awsservice.EKSCluster{Name: "blue"}
292+
293+
got := m.eksUpdateKubeconfigCommand()
294+
if !strings.Contains(got, "--alias 'prod-blue'") {
295+
t.Fatalf("expected context-cluster alias, got %s", got)
296+
}
297+
}
298+
241299
func TestEKSNodeGroupDetailViewShowsScalingAndHealth(t *testing.T) {
242300
m := New(testConfig(), "", "dev")
243301
m.width = 120

internal/services/aws/eks.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -228,10 +228,11 @@ func (r *AwsRepository) isEKSAddonVersionCompatible(ctx context.Context, addon E
228228

229229
func mapEKSCluster(cluster *ekstypes.Cluster) EKSCluster {
230230
item := EKSCluster{
231-
Name: awssdk.ToString(cluster.Name),
232-
ARN: awssdk.ToString(cluster.Arn),
233-
Version: awssdk.ToString(cluster.Version),
234-
Status: string(cluster.Status),
231+
Name: awssdk.ToString(cluster.Name),
232+
ARN: awssdk.ToString(cluster.Arn),
233+
Version: awssdk.ToString(cluster.Version),
234+
Status: string(cluster.Status),
235+
Endpoint: awssdk.ToString(cluster.Endpoint),
235236
}
236237
if cluster.ResourcesVpcConfig != nil {
237238
item.EndpointPublicAccess = cluster.ResourcesVpcConfig.EndpointPublicAccess

0 commit comments

Comments
 (0)