Skip to content
Open
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
9 changes: 9 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,12 @@
- Use lipgloss for styled TUI output — column-aligned tables with dimmed labels
- Tests use mock client interfaces (see `rds_test.go` pattern)
- Scroll windowing: `visibleLines := max(m.height-N, 5)`

## README Maintenance

- **피쳐를 추가, 수정, 삭제할 때 반드시 `README.md`를 함께 업데이트한다.**
- `Currently Implemented Features` 테이블: 새 서비스/기능 추가, 상태 변경(🚧→✅), 삭제된 항목 제거
- `TUI Key Bindings` 테이블: 키 바인딩이 추가/변경/삭제된 경우 반영
- `Usage` 섹션: 새로운 CLI 명령이나 플래그가 추가된 경우 반영
- `Configuration` 섹션: 설정 형식이 변경된 경우 반영
- PR 생성 전 README가 현재 구현 상태와 일치하는지 확인한다.
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,10 @@ contexts:
| EC2 | SSM Session Manager (connect to EC2 instances) | ✅ Implemented |
| VPC | VPC Browser (VPCs → subnets → available IPs) | ✅ Implemented |
| RDS | RDS Browser (list, start/stop, failover, Aurora cluster support) | ✅ Implemented |
| Route53 | ListHostedZones | 🚧 Coming Soon |
| IAM | ListUsers | 🚧 Coming Soon |
| Route53 | Route53 Browser (hosted zones, DNS records) | ✅ Implemented |
| Secrets Manager | Secrets Browser (browse secrets, view key/value pairs) | ✅ Implemented |
| IAM | Access Key Browser (list keys with status, age, last used) | ✅ Implemented |
| IAM | Access Key Rotation (create → verify/apply → deactivate → delete) | ✅ Implemented |

## TUI Key Bindings

Expand All @@ -83,6 +85,11 @@ contexts:
| `/` | Filter (instances, IPs) |
| `C` | Context switcher |
| `s`/`x`/`f` | Start/Stop/Failover (RDS detail) |
| `r` | Rotate access key (IAM key detail) |
| `c` | Copy new key as export commands (IAM rotation result) |
| `a` | Apply new key to ~/.aws/credentials (IAM rotation result) |
| `d` | Deactivate old key (IAM rotation result) |
| `x` | Delete old inactive key (IAM rotation result) |
| `q` (on service list) | Quit |

## Documentation
Expand Down
7 changes: 4 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module unic
go 1.24.0

require (
github.com/aws/aws-sdk-go-v2 v1.41.4
github.com/aws/aws-sdk-go-v2 v1.41.5
github.com/aws/aws-sdk-go-v2/config v1.32.12
github.com/aws/aws-sdk-go-v2/credentials v1.19.12
github.com/aws/aws-sdk-go-v2/service/ec2 v1.296.0
Expand All @@ -20,9 +20,10 @@ require (

require (
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect
github.com/aws/aws-sdk-go-v2/service/iam v1.53.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 // indirect
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.4 // indirect
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
github.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k=
github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY=
github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0=
github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g=
github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8=
Expand All @@ -8,12 +10,18 @@ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqb
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 h1:CNXO7mvgThFGqOFgbNAP2nol2qAWBOGfqR/7tQlvLmc=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 h1:tN6W/hg+pkM+tf9XDkWUbDEjGLb+raoBMFsTodcoYKw=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
github.com/aws/aws-sdk-go-v2/service/ec2 v1.296.0 h1:98Miqj16un1WLNyM1RjVDhXYumhqZrQfAeG8i4jPG6o=
github.com/aws/aws-sdk-go-v2/service/ec2 v1.296.0/go.mod h1:T6ndRfdhnXLIY5oKBHjYZDVj706los2zGdpThppquvA=
github.com/aws/aws-sdk-go-v2/service/iam v1.53.7 h1:n9YLiWtX3+6pTLZWvRJmtq5JIB9NA/KFelyCg5fOlTU=
github.com/aws/aws-sdk-go-v2/service/iam v1.53.7/go.mod h1:sP46Vo6MeJcM4s0ZXcG2PFmfiSyixhIuC/74W52yKuk=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y=
Expand Down
106 changes: 100 additions & 6 deletions internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ const (
screenRoute53RecordDetail
screenSecretList
screenSecretDetail
screenIAMKeyList
screenIAMKeyDetail
screenIAMKeyRotateConfirm
screenIAMKeyRotateResult
screenContextPicker
screenContextAdd
screenLoading
Expand Down Expand Up @@ -101,13 +105,27 @@ type Model struct {
route53RecordFilterActive bool
selectedRoute53Record *awsservice.DNSRecord

// IAM credentials state
iamKeys []awsservice.AccessKey
iamKeyIdx int
selectedIAMKey *awsservice.AccessKey
iamRotationEnabled bool
iamRotateConfirm string // typed input for rotate confirmation
iamRotationOldKeyID string
iamNewKey *awsservice.NewAccessKey
iamCopyMsg string // feedback message for clipboard copy
iamRotationStatus string
iamNewKeyVerified bool
iamOldKeyDeleted bool
iamOldKeyInactive bool

// Secrets Manager browser state
secrets []awsservice.Secret
filteredSecrets []awsservice.Secret
secretIdx int
secretFilter string
secretFilterActive bool
selectedSecret *awsservice.SecretDetail
secrets []awsservice.Secret
filteredSecrets []awsservice.Secret
secretIdx int
secretFilter string
secretFilterActive bool
selectedSecret *awsservice.SecretDetail

// Context picker
configPath string
Expand Down Expand Up @@ -240,6 +258,58 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.screen = screenSecretDetail
return m, nil

case iamKeysLoadedMsg:
m.iamKeys = msg.keys
m.iamKeyIdx = 0
m.screen = screenIAMKeyList
return m, nil

case iamKeyCreatedMsg:
if msg.err != nil {
m.errMsg = msg.err.Error()
m.screen = screenError
return m, nil
}
m.iamNewKey = msg.newKey
m.iamCopyMsg = ""
m.iamRotationStatus = ""
m.iamNewKeyVerified = false
m.iamOldKeyInactive = false
m.iamOldKeyDeleted = false
m.screen = screenIAMKeyRotateResult
return m, nil

case iamKeyVerifiedMsg:
if msg.err != nil {
m.iamRotationStatus = fmt.Sprintf("Verification failed: %s", msg.err)
return m, nil
}
m.iamNewKeyVerified = true
if msg.identity != nil {
m.iamRotationStatus = fmt.Sprintf("Verified new key as %s", msg.identity.Arn)
} else {
m.iamRotationStatus = "Verified new key"
}
return m, nil

case iamKeyDeactivatedMsg:
if msg.err != nil {
m.iamRotationStatus = msg.err.Error()
return m, nil
}
m.iamOldKeyInactive = true
m.iamRotationStatus = fmt.Sprintf("Old key %s marked Inactive", msg.keyID)
return m, nil

case iamKeyDeletedMsg:
if msg.err != nil {
m.iamRotationStatus = msg.err.Error()
return m, nil
}
m.iamOldKeyDeleted = true
m.iamRotationStatus = fmt.Sprintf("Old key %s deleted", msg.keyID)
return m, nil

case rdsActionDoneMsg:
if msg.err != nil {
m.errMsg = msg.err.Error()
Expand Down Expand Up @@ -365,6 +435,14 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m.updateSecretList(msg)
case screenSecretDetail:
return m.updateSecretDetail(msg)
case screenIAMKeyList:
return m.updateIAMKeyList(msg)
case screenIAMKeyDetail:
return m.updateIAMKeyDetail(msg)
case screenIAMKeyRotateConfirm:
return m.updateIAMKeyRotateConfirm(msg)
case screenIAMKeyRotateResult:
return m.updateIAMKeyRotateResult(msg)
case screenContextPicker:
return m.updateContextPicker(msg)
case screenContextAdd:
Expand Down Expand Up @@ -434,6 +512,14 @@ func (m Model) updateFeatureList(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
case domain.FeatureSecretsBrowser:
m.screen = screenLoading
return m, m.loadSecrets()
case domain.FeatureListAccessKeys:
m.iamRotationEnabled = false
m.screen = screenLoading
return m, m.loadIAMKeys()
case domain.FeatureRotateAccessKey:
m.iamRotationEnabled = true
m.screen = screenLoading
return m, m.loadIAMKeys()
}
}
}
Expand Down Expand Up @@ -487,6 +573,14 @@ func (m Model) View() string {
v = m.viewSecretList()
case screenSecretDetail:
v = m.viewSecretDetail()
case screenIAMKeyList:
v = m.viewIAMKeyList()
case screenIAMKeyDetail:
v = m.viewIAMKeyDetail()
case screenIAMKeyRotateConfirm:
v = m.viewIAMKeyRotateConfirm()
case screenIAMKeyRotateResult:
v = m.viewIAMKeyRotateResult()
case screenContextPicker:
v = m.viewContextPicker()
case screenContextAdd:
Expand Down
Loading
Loading