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
90 changes: 72 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ unic
unic --profile my-profile
unic --region ap-northeast-2

# Enable verbose debug logging (writes to ~/.config/unic/logs/unic.log)
unic --verbose
unic -v

# Initialize config file
unic init # Create default config
unic init --force # Overwrite existing config
Expand All @@ -40,50 +44,100 @@ unic init --force # Overwrite existing config

`~/.config/unic/config.yaml` (created via `unic init` or auto-generated on first run)

### Legacy Format (Flat)

```yaml
# Simple format
default_profile: my-profile
default_region: ap-northeast-2
```

### Context-Based Format

```yaml
# Context-based format
current: dev-sso

defaults:
region: us-east-1

contexts:
# SSO authentication
- name: dev-sso
profile: dev-sso
region: ap-northeast-2
auth_type: sso
sso_start_url: https://my-sso-portal.awsapps.com/start
sso_account_id: "123456789012"
sso_role_name: DeveloperRole

- name: prod-admin
profile: prod-admin
# Assume Role (cross-account)
- name: prod-assume
region: us-east-1
auth_type: assume_role
profile: base-profile
role_arn: arn:aws:iam::987654321098:role/CrossAccountRole
external_id: optional-external-id

# Credential profile
- name: staging-creds
region: eu-west-1
auth_type: credential
profile: staging
```

### Auth Types

| Auth Type | Required Fields | Description |
|-----------|----------------|-------------|
| `sso` | `sso_start_url`, `sso_account_id`, `sso_role_name` | AWS SSO portal login with token caching |
| `credential` | `profile` | Uses `~/.aws/credentials` profile directly |
| `assume_role` | `profile`, `role_arn` | Assumes a cross-account role from a base profile |

**Priority**: CLI flags (`--profile`, `--region`) > context settings > config defaults > hardcoded defaults (`us-east-1`)

## Currently Implemented Features

| Service | Feature | Status |
|---------|---------|--------|
| EC2 | SSM Session Manager (connect to EC2 instances) | ✅ Implemented |
| VPC | VPC Browser (VPCs → subnetsavailable IPs) | ✅ Implemented |
| RDS | RDS Browser (list, start/stop, failover, Aurora cluster support) | ✅ Implemented |
| Route53 | ListHostedZones | 🚧 Coming Soon |
| IAM | ListUsers | 🚧 Coming Soon |
| EC2 | SSM Session Manager (connect to running, SSM-managed instances) | ✅ Implemented |
| VPC | VPC Browser (VPCs → SubnetsAvailable IPs with reserved-IP exclusion) | ✅ Implemented |
| RDS | RDS Browser (list, start/stop, failover, Aurora cluster support, auto-polling) | ✅ Implemented |
| Route53 | DNS Browser (Hosted Zones → Records → Record Detail, public/private zones) | ✅ Implemented |
| Secrets Manager | Secrets Browser (list secrets, view key-value pairs or raw values) | ✅ Implemented |

## TUI Key Bindings

### Global Navigation

| Key | Action |
|-----|--------|
| `j`/`k` or `↑`/`↓` | Navigate list |
| `Enter` | Select item |
| `Esc` | Go back one screen |
| `q` | Quit (on service list) |
| `H` | Jump to home (service list) |
| `C` | Open context switcher |
| `/` | Toggle filter mode |
| `Ctrl+C` | Force quit |

### RDS Detail Actions

| Key | Action | Condition |
|-----|--------|-----------|
| `s` | Start database | Instance/cluster is stopped |
| `x` | Stop database | Instance/cluster is available |
| `f` | Failover database | Multi-AZ standalone or Aurora cluster |
| `r` | Refresh status | Always |

### Context Switcher

| Key | Action |
|-----|--------|
| `j`/`k` or `↑`/`↓` | Navigate |
| `Enter` | Select |
| `Esc`/`q` | Go back |
| `H` | Go to home (service list) |
| `/` | Filter (instances, IPs) |
| `C` | Context switcher |
| `s`/`x`/`f` | Start/Stop/Failover (RDS detail) |
| `q` (on service list) | Quit |
| `Enter` | Switch to selected context |
| `a` | Add new context (wizard) |
| `Esc` | Back |

### Filtering

Available on: EC2 instances, VPC/Subnets, RDS instances, Route53 zones/records, Secrets Manager. Press `/` to enter filter mode, type to search, `Esc` or `Enter` to exit filter mode.

## Documentation

Expand Down
13 changes: 13 additions & 0 deletions cmd/unic/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,18 @@ import (
"unic/internal/app"
"unic/internal/cli"
"unic/internal/config"
uniclog "unic/internal/log"
)

func main() {
rootCmd := cli.NewRootCmd()

rootCmd.RunE = func(cmd *cobra.Command, args []string) error {
if err := uniclog.Init(cli.Verbose()); err != nil {
return fmt.Errorf("logger init error: %w", err)
}
defer uniclog.Close()

configPath, err := config.DefaultPath()
if err != nil {
return err
Expand All @@ -30,6 +36,13 @@ func main() {
return fmt.Errorf("config load error: %w", err)
}

uniclog.Info("config", "config loaded",
"profile", cfg.Profile,
"region", cfg.Region,
"context", cfg.ContextName,
"auth_type", string(cfg.AuthType),
)

p := tea.NewProgram(app.New(cfg, configPath), tea.WithAltScreen())
if _, err := p.Run(); err != nil {
return fmt.Errorf("TUI error: %w", err)
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/ec2 v1.296.0
github.com/aws/aws-sdk-go-v2/service/rds v1.116.3
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.4
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.4
github.com/aws/aws-sdk-go-v2/service/ssm v1.68.3
github.com/aws/aws-sdk-go-v2/service/sso v1.30.13
github.com/aws/aws-sdk-go-v2/service/sts v1.41.9
Expand All @@ -25,7 +26,6 @@ require (
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // 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
github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect
github.com/aws/smithy-go v1.24.2 // indirect
Expand Down
6 changes: 6 additions & 0 deletions internal/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ import (
"github.com/aws/aws-sdk-go-v2/service/sts"

"unic/internal/config"
uniclog "unic/internal/log"
awsservice "unic/internal/services/aws"
)

// PostSwitch performs the auth action after switching to a context.
// Returns a human-readable status message.
func PostSwitch(cfg *config.Config) (string, error) {
uniclog.Info("auth", "post-switch started", "context", cfg.ContextName, "auth_type", string(cfg.AuthType))

var msg string
var err error

Expand All @@ -33,6 +36,7 @@ func PostSwitch(cfg *config.Config) (string, error) {
msg = fmt.Sprintf("Context %q activated (profile: %s, region: %s)", cfg.ContextName, cfg.Profile, cfg.Region)
}
if err != nil {
uniclog.Error("auth", "post-switch failed", "error", err.Error())
return "", err
}

Expand All @@ -46,6 +50,7 @@ func PostSwitch(cfg *config.Config) (string, error) {
}

func postSwitchSSO(cfg *config.Config) (string, error) {
uniclog.Debug("auth", "starting SSO login", "sso_start_url", cfg.SSOStartURL)
if err := awsservice.RunSSOLogin(cfg); err != nil {
return "", err
}
Expand Down Expand Up @@ -73,6 +78,7 @@ func postSwitchCredential(cfg *config.Config) (string, error) {
}

func postSwitchAssumeRole(cfg *config.Config) (string, error) {
uniclog.Debug("auth", "assuming role", "role_arn", cfg.RoleArn)
ctx := context.Background()

opts := []func(*awsconfig.LoadOptions) error{
Expand Down
7 changes: 7 additions & 0 deletions internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ var (

profile string
region string
verbose bool
)

func NewRootCmd() *cobra.Command {
Expand All @@ -22,6 +23,7 @@ func NewRootCmd() *cobra.Command {

cmd.PersistentFlags().StringVarP(&profile, "profile", "p", "", "AWS profile to use")
cmd.PersistentFlags().StringVar(&region, "region", "", "AWS region to use")
cmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose debug logging")

cmd.AddCommand(newInitCmd())

Expand All @@ -43,3 +45,8 @@ func Region() *string {
}
return &region
}

// Verbose returns true if --verbose was passed.
func Verbose() bool {
return verbose
}
7 changes: 7 additions & 0 deletions internal/cli/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,11 @@ func TestRootCmdHasFlags(t *testing.T) {
if rf == nil {
t.Error("expected --region flag")
}
vf := cmd.PersistentFlags().Lookup("verbose")
if vf == nil {
t.Error("expected --verbose flag")
}
if vf != nil && vf.Shorthand != "v" {
t.Errorf("expected --verbose shorthand 'v', got '%s'", vf.Shorthand)
}
}
10 changes: 10 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"path/filepath"

"gopkg.in/yaml.v3"

uniclog "unic/internal/log"
)

const (
Expand Down Expand Up @@ -139,6 +141,14 @@ func Load(cliProfile, cliRegion *string, configPath string) (*Config, error) {
region = *cliRegion
}

uniclog.Debug("config", "config resolved",
"path", configPath,
"profile", profile,
"region", region,
"context", contextName,
"auth_type", string(authType),
)

return &Config{
Profile: profile,
Region: region,
Expand Down
Loading
Loading