Skip to content
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: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ checks:
| ECS Exec | `r` refresh, `Enter` drill down / exec |
| 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 |
| 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 |
| FIS | `Enter` template detail/run detail, `/` filter, `h` selected-template history, `H` all experiment history, `r` refresh, detail scrolls through targets/actions/stop conditions |
| FIS | `Enter` template detail/run detail, `/` filter, `h` selected-template history, `H` all experiment history, `r` refresh, template detail includes safe-run preview and detail scrolls through targets/actions/stop conditions |
| Inspector Mode | `i` open mode from the service list, `Enter` open the selected workflow, `l` open the checklist file picker |
| Security Inspector | `r` run/rescan, `1`-`5` severity filter, `Enter` finding detail |
| Checklist Inspector | `l` load or switch checklist files, `r` run/rerun the loaded checklist, `Enter` result detail |
Expand All @@ -358,7 +358,7 @@ The EKS Browser includes a managed add-on status view for each cluster. Add-on r

The ECR Repository Browser opens image/tag lists from each repository. Image rows include tags, digest, pushed time, and size, and mark untagged images or images older than 90 days as cleanup candidates. Image detail exposes digest and tag values for clipboard copy.

The FIS Experiment Template Browser lists experiment templates in the active region and opens a detail screen with role ARN, targets, actions, target mappings, parameters, filters, and stop condition summaries without leaving the TUI. Press `h` on a selected template or template detail to inspect recent runs for that template, or `H` from the template list to inspect recent experiment history across the active account/region. History rows include run status, timing, and stop/failure summaries, with failed, stopped, stopping, and cancelled runs visually highlighted; `Enter` opens run detail with start/end times, duration, action states, targets, stop conditions, and failure metadata.
The FIS Experiment Template Browser lists experiment templates in the active region and opens a detail screen with role ARN, targets, actions, target mappings, parameters, filters, and stop condition summaries without leaving the TUI. Template detail includes a Safe Run Preview that summarizes blast radius, target selection modes, action count, active stop conditions, IAM role, and warnings for missing stop conditions, missing role ARN, broad selection, or unbounded selectors. The preview also states the template ID that any future execution path must type to confirm before a run can start. Press `h` on a selected template or template detail to inspect recent runs for that template, or `H` from the template list to inspect recent experiment history across the active account/region. History rows include run status, timing, and stop/failure summaries, with failed, stopped, stopping, and cancelled runs visually highlighted; `Enter` opens run detail with start/end times, duration, action states, targets, stop conditions, and failure metadata.

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.

Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ Current screen families include:
- ECS cluster/service/rollout detail/task/container flows
- EKS cluster/node group/add-on status, upgrade readiness, and access helper flows
- ECR repository/image/detail flows
- FIS experiment template list/detail and experiment history/detail flows
- FIS experiment template list/detail, safe-run preview, and experiment history/detail flows
- S3 bucket/object/detail flows
- Inspector mode home, checklist setup, security findings/detail, and checklist results/detail flows
- context picker, context add, and TUI-native context setup/export/unset flows
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ UNIC은 현재 세 가지 인증 모드를 지원한다.
- ECS cluster/service/rollout detail/task/container
- EKS cluster/node group/add-on status, upgrade readiness, access helper
- ECR repository/image/detail
- FIS experiment template list/detail 및 experiment history/detail
- FIS experiment template list/detail, safe-run preview 및 experiment history/detail
- S3 bucket/object/detail
- Inspector mode home, checklist setup, security findings/detail, checklist results/detail
- context picker, context add, TUI-native context setup/export/unset
Expand Down
2 changes: 1 addition & 1 deletion docs/project-overview.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Implemented service areas currently include:
- Lambda
- Inspector mode

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. FIS includes experiment template browsing with targets, actions, role ARN, stop condition summaries, and recent experiment history with status, timing, and failure/stop reasons.
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. FIS includes experiment template browsing with safe-run blast-radius preview, targets, actions, role ARN, stop condition summaries, and recent experiment history with status, timing, and failure/stop reasons.
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.

## Primary User Flows
Expand Down
2 changes: 1 addition & 1 deletion docs/project-overview.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ UNIC은 다음 세 가지를 결합한 Go 기반 AWS 터미널 콘솔이다.
- Lambda
- Inspector mode

애플리케이션은 이미 상호작용형 변경 작업 플로우, 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 후보로 드러낸다. FIS는 experiment template 목록과 target, action, role ARN, stop condition 요약 상세 화면에 더해 최근 experiment history의 상태, 시간, failure/stop reason을 보여준다.
애플리케이션은 이미 상호작용형 변경 작업 플로우, 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 후보로 드러낸다. FIS는 experiment template 목록과 safe-run blast-radius preview, target, action, role ARN, stop condition 요약 상세 화면에 더해 최근 experiment history의 상태, 시간, failure/stop reason을 보여준다.
Inspector mode는 이제 built-in security scan과 함께 RDS, security group, secret, Route53, VPC/subnet, CloudWatch Logs, baseline posture wrapper를 다루는 checklist 기반 readiness check도 포함한다.

## 주요 사용자 흐름
Expand Down
29 changes: 27 additions & 2 deletions internal/app/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1150,7 +1150,7 @@ func TestFISTemplateDetailViewShowsTargetsActionsAndStops(t *testing.T) {
Description: "Terminate application targets",
RoleARN: "arn:aws:iam::123456789012:role/fis-role",
Targets: []awsservice.FISTemplateTarget{
{Name: "instances", ResourceType: "aws:ec2:instance", SelectionMode: "COUNT(1)"},
{Name: "instances", ResourceType: "aws:ec2:instance", SelectionMode: "COUNT(1)", ResourceTags: map[string]string{"env": "dev"}},
},
Actions: []awsservice.FISTemplateAction{
{Name: "stop", ActionID: "aws:ec2:stop-instances"},
Expand All @@ -1161,7 +1161,32 @@ func TestFISTemplateDetailViewShowsTargetsActionsAndStops(t *testing.T) {
}

view := stripANSI(m.View())
for _, want := range []string{"FIS Experiment Template Detail", "Role ARN", "Targets", "aws:ec2:instance", "Actions", "aws:ec2:stop-instances", "Stop Conditions", "fis-stop"} {
for _, want := range []string{"FIS Experiment Template Detail", "Role ARN", "Safe Run Preview", "Risk", "guarded", "Blast radius", "Future execution must type", "Targets", "aws:ec2:instance", "Actions", "aws:ec2:stop-instances", "Stop Conditions", "fis-stop"} {
if !strings.Contains(view, want) {
t.Fatalf("expected view to contain %q, got %q", want, view)
}
}
}

func TestFISTemplateDetailViewShowsSafetyWarnings(t *testing.T) {
m := New(testConfig(), "", "dev")
m.screen = screenFISTemplateDetail
m.height = 40
m.fis.selectedTemplate = &awsservice.FISExperimentTemplate{
ID: "broad-outage",
Targets: []awsservice.FISTemplateTarget{
{Name: "instances", ResourceType: "aws:ec2:instance", SelectionMode: "ALL"},
},
Actions: []awsservice.FISTemplateAction{
{Name: "terminate", ActionID: "aws:ec2:terminate-instances"},
},
StopConditions: []awsservice.FISTemplateStopCondition{
{Source: "none"},
},
}

view := stripANSI(m.View())
for _, want := range []string{"Safe Run Preview", "review required", "Missing IAM role ARN", "No active stop conditions configured", "Broad target selection", "Unbounded target selector", "Future execution must type \"broad-outage\""} {
if !strings.Contains(view, want) {
t.Fatalf("expected view to contain %q, got %q", want, view)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/app/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,7 @@ func (m Model) currentScreenShortcuts() []helpShortcut {
}, shortcuts[3:]...)...)
case screenFISTemplateDetail:
return []helpShortcut{
{"↑/↓, j/k", "Scroll template targets, actions, and stop conditions"},
{"↑/↓, j/k", "Scroll safe-run preview, targets, actions, and stop conditions"},
{"pgup / pgdn", "Scroll by one page"},
{"h", "Open experiment history for this template"},
{"r", "Refresh template detail"},
Expand Down
31 changes: 31 additions & 0 deletions internal/app/screen_fis.go
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,12 @@ func (fm fisModel) viewExperimentDetail(m Model) string {
}

func (fm fisModel) templateDetailLines(template awsservice.FISExperimentTemplate) []string {
preview := template.SafeRunPreview()
risk := successStyle.Render(preview.RiskLevel)
if preview.HasWarnings() {
risk = warningStyle.Render(preview.RiskLevel)
}

lines := []string{
renderDetailLine("ID", normalStyle.Render(template.ID)),
renderDetailLine("Description", normalStyle.Render(defaultDash(template.Description))),
Expand All @@ -505,6 +511,31 @@ func (fm fisModel) templateDetailLines(template awsservice.FISExperimentTemplate
}
lines = append(lines, renderDetailLine("Tags", normalStyle.Render(defaultDash(formatDetailMap(template.Tags)))))

lines = append(lines, "", selectedStyle.Render("Safe Run Preview"))
lines = append(lines, renderDetailLine("Risk", risk))
lines = append(lines, renderDetailLine("Targets", normalStyle.Render(fmt.Sprintf("%d target group(s)", preview.TargetCount))))
lines = append(lines, renderDetailLine("Actions", normalStyle.Render(fmt.Sprintf("%d action(s)", preview.ActionCount))))
lines = append(lines, renderDetailLine("Stop Conditions", normalStyle.Render(fmt.Sprintf("%d active", preview.StopConditionCount))))
lines = append(lines, renderDetailLine("IAM Role", normalStyle.Render(defaultDash(template.RoleARN))))
if len(preview.TargetModes) > 0 {
lines = append(lines, renderDetailLine("Selection Modes", normalStyle.Render(strings.Join(preview.TargetModes, ", "))))
}
if len(preview.TargetSummaries) > 0 {
lines = append(lines, " "+dimStyle.Render("Blast radius"))
for _, summary := range preview.TargetSummaries {
lines = append(lines, " "+normalStyle.Render(summary))
}
}
if len(preview.Warnings) == 0 {
lines = append(lines, " "+successStyle.Render("No obvious missing safeguards detected"))
} else {
lines = append(lines, " "+warningStyle.Render("Review before any future run"))
for _, warning := range preview.Warnings {
lines = append(lines, " "+warningStyle.Render(warning))
}
}
lines = append(lines, " "+dimStyle.Render(fmt.Sprintf("Future execution must type %q to confirm.", preview.ConfirmationToken)))

lines = append(lines, "", selectedStyle.Render("Targets"))
if len(template.Targets) == 0 {
lines = append(lines, dimStyle.Render(" No targets"))
Expand Down
2 changes: 1 addition & 1 deletion internal/domain/catalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ func Catalog() []Service {
Features: []Feature{
{
Kind: FeatureFISTemplateBrowser,
Description: "Browse FIS experiment templates and recent experiment history with status, timing, and stop reasons",
Description: "Browse FIS experiment templates, safe-run preview, and recent experiment history",
},
},
},
Expand Down
100 changes: 100 additions & 0 deletions internal/services/aws/fis_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,21 @@ type FISExperimentTemplate struct {
StopConditions []FISTemplateStopCondition
}

type FISSafeRunPreview struct {
RiskLevel string
TargetCount int
ActionCount int
StopConditionCount int
TargetModes []string
TargetSummaries []string
Warnings []string
ConfirmationToken string
}

func (p FISSafeRunPreview) HasWarnings() bool {
return len(p.Warnings) > 0
}

type FISExperiment struct {
ID string
ARN string
Expand Down Expand Up @@ -146,6 +161,56 @@ func (t FISExperimentTemplate) DisplayTitle() string {
return fmt.Sprintf("%s %s", t.ID, description)
}

func (t FISExperimentTemplate) SafeRunPreview() FISSafeRunPreview {
preview := FISSafeRunPreview{
RiskLevel: "guarded",
TargetCount: len(t.Targets),
ActionCount: len(t.Actions),
ConfirmationToken: t.ID,
}

stopConditionCount := 0
for _, condition := range t.StopConditions {
if !condition.IsNone() {
stopConditionCount++
}
}
preview.StopConditionCount = stopConditionCount

if strings.TrimSpace(t.RoleARN) == "" {
preview.Warnings = append(preview.Warnings, "Missing IAM role ARN")
}
if preview.TargetCount == 0 {
preview.Warnings = append(preview.Warnings, "No experiment targets configured")
}
if preview.ActionCount == 0 {
preview.Warnings = append(preview.Warnings, "No experiment actions configured")
}
if preview.StopConditionCount == 0 {
preview.Warnings = append(preview.Warnings, "No active stop conditions configured")
}

for _, target := range t.Targets {
mode := defaultString(target.SelectionMode, "-")
preview.TargetModes = append(preview.TargetModes, fmt.Sprintf("%s:%s", defaultString(target.Name, "-"), mode))
preview.TargetSummaries = append(preview.TargetSummaries, target.BlastRadiusSummary())
if target.IsBroadSelection() {
preview.Warnings = append(preview.Warnings, fmt.Sprintf("Broad target selection: %s uses %s", defaultString(target.Name, "-"), mode))
}
if !target.HasTargetConstraint() {
preview.Warnings = append(preview.Warnings, fmt.Sprintf("Unbounded target selector: %s has no ARNs, tags, filters, or parameters", defaultString(target.Name, "-")))
}
}
sort.Strings(preview.TargetModes)
sort.Strings(preview.TargetSummaries)
sort.Strings(preview.Warnings)

if len(preview.Warnings) > 0 {
preview.RiskLevel = "review required"
}
return preview
}

func (t FISExperimentTemplate) FilterText() string {
parts := []string{t.ID, t.ARN, t.Description, t.RoleARN, formatStringMap(t.Tags)}
for _, target := range t.Targets {
Expand Down Expand Up @@ -181,6 +246,36 @@ func (t FISTemplateTarget) Summary() string {
return strings.Join(nonEmptyStrings(parts), " ")
}

func (t FISTemplateTarget) BlastRadiusSummary() string {
parts := []string{
defaultString(t.Name, "-"),
defaultString(t.ResourceType, "-"),
"mode:" + defaultString(t.SelectionMode, "-"),
}
if len(t.ResourceARNs) > 0 {
parts = append(parts, fmt.Sprintf("arns:%d", len(t.ResourceARNs)))
}
if len(t.ResourceTags) > 0 {
parts = append(parts, fmt.Sprintf("tags:%d", len(t.ResourceTags)))
}
if len(t.Filters) > 0 {
parts = append(parts, fmt.Sprintf("filters:%d", len(t.Filters)))
}
if len(t.Parameters) > 0 {
parts = append(parts, fmt.Sprintf("parameters:%d", len(t.Parameters)))
}
return strings.Join(nonEmptyStrings(parts), " ")
}

func (t FISTemplateTarget) HasTargetConstraint() bool {
return len(t.ResourceARNs) > 0 || len(t.ResourceTags) > 0 || len(t.Filters) > 0 || len(t.Parameters) > 0
}

func (t FISTemplateTarget) IsBroadSelection() bool {
mode := strings.ToUpper(strings.TrimSpace(t.SelectionMode))
return mode == "ALL" || mode == "PERCENT(100)" || mode == "COUNT(0)"
}

func (t FISTemplateTarget) FilterText() string {
parts := []string{t.Name, t.ResourceType, t.SelectionMode, strings.Join(t.ResourceARNs, " "), formatStringMap(t.ResourceTags), formatStringMap(t.Parameters)}
for _, filter := range t.Filters {
Expand Down Expand Up @@ -245,6 +340,11 @@ func (s FISTemplateStopCondition) Summary() string {
return fmt.Sprintf("%s %s", s.Source, s.Value)
}

func (s FISTemplateStopCondition) IsNone() bool {
source := strings.TrimSpace(s.Source)
return source == "" || strings.EqualFold(source, "none")
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func (s FISTemplateStopCondition) FilterText() string {
return s.Source + " " + s.Value
}
Expand Down
Loading
Loading