Skip to content

Commit 618a8f7

Browse files
authored
Merge pull request #18 from larkly/feat/sg-network-crud
feat: SG group CRUD, network/subnet CRUD, port listing, LB fix
2 parents b71ff17 + 156a2f1 commit 618a8f7

19 files changed

Lines changed: 1793 additions & 45 deletions

File tree

PRD.md

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -126,15 +126,15 @@ src/
126126
servers.go # Server CRUD + pause/suspend/shelve/resize/reboot
127127
actions.go # Instance action history
128128
flavors.go # Flavor listing
129-
keypairs.go # Keypair listing + delete
129+
keypairs.go # Keypair CRUD (list, get, create/generate, import, delete)
130130
image/
131131
images.go # Image listing
132132
network/
133133
networks.go # Network listing, external networks, port lookup
134134
floatingips.go # Floating IP CRUD (allocate, associate, disassociate, release)
135135
secgroups.go # Security group listing, rule create/delete
136136
volume/
137-
volumes.go # Volume CRUD (list, get, create, delete, attach, detach)
137+
volumes.go # Volume CRUD (list, get, create, delete, attach, detach, volume types)
138138
loadbalancer/
139139
lb.go # Octavia LB, listener, pool, member CRUD
140140
quota/
@@ -161,12 +161,24 @@ src/
161161
floatingiplist.go # Floating IP table with sorting
162162
secgroupview/
163163
secgroupview.go # Security group viewer with expandable rules, rule deletion
164+
keypaircreate/
165+
keypaircreate.go # Key pair create/import form with type picker, file browser
166+
keypairdetail/
167+
keypairdetail.go # Key pair detail view showing public key
164168
keypairlist/
165-
keypairlist.go # Key pair table with sorting, delete
169+
keypairlist.go # Key pair table with sorting, delete, auto-refresh
166170
lblist/
167171
lblist.go # Load balancer table with status colors, sorting
168172
lbdetail/
169173
lbdetail.go # LB detail with listener/pool/member tree
174+
networklist/
175+
networklist.go # Network browser with expandable subnets
176+
volumecreate/
177+
volumecreate.go # Volume create form with type/AZ pickers
178+
serverpicker/
179+
serverpicker.go # Server picker modal for volume attach
180+
sgrulecreate/
181+
sgrulecreate.go # Security group rule create modal
170182
fippicker/
171183
fippicker.go # Floating IP picker modal for server association
172184
projectpicker/
@@ -292,8 +304,9 @@ src/
292304
- Scrollable with ↑/↓ when content doesn't fit
293305

294306
#### Status Bar
295-
- Shows current cloud name and region
307+
- Shows current cloud name, project, and region
296308
- Context-sensitive key hints per view (adapts to server state, selection count)
309+
- Sticky hints for action success messages (survives background auto-refresh, clears on next key press)
297310
- Error/warning display
298311
- Truncates gracefully when bar overflows
299312

@@ -353,7 +366,7 @@ src/
353366
### Phase 3: Additional Resources (Complete)
354367

355368
#### Tabbed Navigation
356-
- Dynamic tabs built from service catalog (see Phase 4 refactor): Servers, Volumes, Floating IPs, Security Groups, Key Pairs (always present), Load Balancers (if Octavia available)
369+
- Dynamic tabs built from service catalog (see Phase 4 refactor): Servers, Volumes, Floating IPs, Security Groups, Networks, Key Pairs (always present), Load Balancers (if Octavia available)
357370
- Switch with number keys `1-9` or `←/→` from any top-level list view
358371
- Tab bar with active tab highlighted, inactive tabs muted
359372
- Each tab lazily initializes on first visit, auto-refreshes independently
@@ -362,9 +375,10 @@ src/
362375
#### Volume Management
363376
- **Volume List**: Adaptive columns (Name, Status, Size, Type, Attached To, Device, Bootable), auto-refresh, sorting
364377
- **Volume Detail**: Enter on a volume shows full properties — Name, ID, Status, Size, Type, AZ, Bootable, Encrypted, Multiattach, Description, Created, Updated, Snapshot ID, Source Volume ID, Attached Server (resolved name), Device, Metadata (key=value)
378+
- **Create** (`Ctrl+N`): Form with name, size (GB), type picker (from volume types API), AZ, description
365379
- **Delete** (`Ctrl+D`): Confirmation modal, works from list or detail
380+
- **Attach** (`Ctrl+A`): Server picker modal showing ACTIVE/SHUTOFF servers with type-to-filter
366381
- **Detach** (`Ctrl+T`): From detail view, finds attached server and detaches
367-
- **Attach** (`Ctrl+A`): Deferred (complex server picker — use CLI for now)
368382
- Status colors: available=green, in-use=cyan, creating/extending=yellow, error=red, deleting=muted
369383

370384
#### Floating IP Management
@@ -378,12 +392,21 @@ src/
378392
- **Group List**: Expandable groups showing name, description, rule count
379393
- **Rule View**: Enter expands/collapses group rules. Rules show direction, protocol, port range, remote, ethertype
380394
- **Rule Navigation**: Down arrow enters rule list within expanded group, Up arrow exits back to group level
381-
- **Delete Rule** (`Ctrl+D`): When cursor is on a rule, confirmation modal then deletes
395+
- **Create Rule** (`Ctrl+N` in rules): Modal with cycle pickers for direction/ethertype/protocol, port range, remote IP prefix
396+
- **Delete Rule** (`Ctrl+D` in rules): When cursor is on a rule, confirmation modal then deletes
382397
- Selected rule highlighted with `` prefix and background color
383398

384399
#### Key Pair Management
385-
- **List**: Columns (Name, Type), sorting
386-
- **Delete** (`Ctrl+D`): Confirmation modal
400+
- **List**: Columns (Name, Type), sorting, auto-refresh
401+
- **Detail** (`Enter`): Shows name, type, and full public key with scroll
402+
- **Create/Import** (`Ctrl+N`): Form with name, type picker (RSA 2048/RSA 4096/ED25519), public key field with `~/.ssh/` file browser. Keys generated locally using Go crypto (`crypto/rsa`, `crypto/ed25519`, `x/crypto/ssh`), imported via Nova API
403+
- **Save Private Key** (`s` in private key view): Save generated private key to file (default `~/.ssh/<name>`, 0600 permissions), public key saved alongside as `.pub`
404+
- **Delete** (`Ctrl+D`): Confirmation modal, works from list or detail
405+
406+
#### Network Browser
407+
- **Networks Tab**: Network list with Name, Status, Subnets count, Shared columns
408+
- **Expandable Subnets**: Enter expands/collapses network to show subnet details (name, CIDR, gateway, IP version, DHCP status)
409+
- Auto-refresh, read-only browsing
387410

388411
#### Column Sorting
389412
- `s` cycles sort to next visible column (ascending), `S` toggles sort direction
@@ -516,13 +539,15 @@ src/
516539
|-----|--------|
517540
| `↑/k` `↓/j` | Navigate |
518541
| `Enter` | View detail |
542+
| `Ctrl+N` | Create volume |
519543
| `Ctrl+D` | Delete volume |
520544

521545
#### Volume Detail
522546
| Key | Action |
523547
|-----|--------|
524548
| `↑/k` `↓/j` | Scroll |
525549
| `Ctrl+D` | Delete volume |
550+
| `Ctrl+A` | Attach to server (server picker modal) |
526551
| `Ctrl+T` | Detach from server |
527552
| `Esc` | Back to list |
528553

@@ -539,15 +564,24 @@ src/
539564
|-----|--------|
540565
| `↑/k` `↓/j` | Navigate groups / rules |
541566
| `Enter` | Expand / collapse group |
542-
| `Ctrl+D` | Delete selected rule |
567+
| `Ctrl+N` | Add rule (when in rules) |
568+
| `Ctrl+D` | Delete selected rule (when in rules) |
543569
| `Esc` | Back to group level (from rules) |
544570

545571
#### Key Pairs
546572
| Key | Action |
547573
|-----|--------|
548574
| `↑/k` `↓/j` | Navigate |
575+
| `Enter` | View detail (public key) |
576+
| `Ctrl+N` | Create / import key pair |
549577
| `Ctrl+D` | Delete key pair |
550578

579+
#### Networks
580+
| Key | Action |
581+
|-----|--------|
582+
| `↑/k` `↓/j` | Navigate |
583+
| `Enter` | Expand / collapse subnets |
584+
551585
#### Load Balancers
552586
| Key | Action |
553587
|-----|--------|
@@ -587,12 +621,12 @@ src/
587621

588622
## Future Roadmap
589623

590-
### Backlog (deferred from Phase 3)
591-
- **Create Volume form**: Name, size, type, AZ, description, source snapshot/volume — similar to server create
592-
- **Create/Import Key Pair**: Generate or paste public key — requires text area input
593-
- **Create Security Group Rule**: Direction, protocol, port range, remote — complex form, better left to CLI for now
594-
- **Volume Attach from detail**: Needs a server picker modal (similar to FIP picker)
595-
- Network/subnet/port browsing
624+
### Backlog (deferred from Phase 3 — all complete)
625+
- ~~**Create Volume form**~~: ✓ Complete — name, size, type picker, AZ, description
626+
- ~~**Create/Import Key Pair**~~: ✓ Complete — RSA/ED25519, file browser, save-to-file
627+
- ~~**Create Security Group Rule**~~: ✓ Complete — modal with cycle pickers
628+
- ~~**Volume Attach from detail**~~: ✓ Complete — server picker modal
629+
- ~~**Network/subnet browsing**~~: ✓ Complete — Networks tab with expandable subnets
596630

597631
### Phase 5: Quality of Life
598632
- Configuration file (`~/.config/lazystack/config.yaml`) for defaults

src/internal/app/actions_resource.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@ import (
77
"github.com/larkly/lazystack/internal/network"
88
"github.com/larkly/lazystack/internal/shared"
99
"github.com/larkly/lazystack/internal/ui/keypaircreate"
10+
"github.com/larkly/lazystack/internal/ui/networkcreate"
11+
"github.com/larkly/lazystack/internal/ui/subnetcreate"
1012
"github.com/larkly/lazystack/internal/ui/keypairdetail"
1113
"github.com/larkly/lazystack/internal/ui/lbdetail"
1214
"github.com/larkly/lazystack/internal/ui/modal"
1315
"github.com/larkly/lazystack/internal/ui/serverpicker"
16+
"github.com/larkly/lazystack/internal/ui/sgcreate"
1417
"github.com/larkly/lazystack/internal/ui/sgrulecreate"
1518
"github.com/larkly/lazystack/internal/ui/volumecreate"
1619
"github.com/larkly/lazystack/internal/ui/volumedetail"
@@ -141,6 +144,26 @@ func (m Model) openFIPDisassociateConfirm() (Model, tea.Cmd) {
141144

142145
// --- Security Group actions ---
143146

147+
func (m Model) openSGCreate() (Model, tea.Cmd) {
148+
m.sgCreate = sgcreate.New(m.client.Network)
149+
m.sgCreate.SetSize(m.width, m.height)
150+
return m, m.sgCreate.Init()
151+
}
152+
153+
func (m Model) openSGDeleteConfirm() (Model, tea.Cmd) {
154+
sgID := m.secGroupView.SelectedGroupID()
155+
sgName := m.secGroupView.SelectedGroupName()
156+
if sgID == "" {
157+
return m, nil
158+
}
159+
m.confirm = modal.NewConfirm("delete_sg", sgID, sgName)
160+
m.confirm.Title = "Delete Security Group"
161+
m.confirm.Body = fmt.Sprintf("Are you sure you want to delete security group %q?\nAll rules in this group will also be deleted.", sgName)
162+
m.confirm.SetSize(m.width, m.height)
163+
m.activeModal = modalConfirm
164+
return m, nil
165+
}
166+
144167
func (m Model) openSGRuleDeleteConfirm() (Model, tea.Cmd) {
145168
ruleID := m.secGroupView.SelectedRule()
146169
if ruleID == "" {
@@ -166,6 +189,54 @@ func (m Model) openSGRuleCreate() (Model, tea.Cmd) {
166189
return m, m.sgRuleCreate.Init()
167190
}
168191

192+
// --- Network actions ---
193+
194+
func (m Model) openNetworkCreate() (Model, tea.Cmd) {
195+
m.networkCreate = networkcreate.New(m.client.Network)
196+
m.networkCreate.SetSize(m.width, m.height)
197+
return m, m.networkCreate.Init()
198+
}
199+
200+
func (m Model) openNetworkDeleteConfirm() (Model, tea.Cmd) {
201+
netID := m.networkList.SelectedNetworkID()
202+
netName := m.networkList.SelectedNetworkName()
203+
if netID == "" {
204+
return m, nil
205+
}
206+
m.confirm = modal.NewConfirm("delete_network", netID, netName)
207+
m.confirm.Title = "Delete Network"
208+
m.confirm.Body = fmt.Sprintf("Are you sure you want to delete network %q?\nAll subnets will also be deleted.", netName)
209+
m.confirm.SetSize(m.width, m.height)
210+
m.activeModal = modalConfirm
211+
return m, nil
212+
}
213+
214+
func (m Model) openSubnetCreate() (Model, tea.Cmd) {
215+
netID := m.networkList.SelectedNetworkID()
216+
netName := m.networkList.SelectedNetworkName()
217+
if netID == "" {
218+
return m, nil
219+
}
220+
m.subnetCreate = subnetcreate.New(m.client.Network, netID, netName)
221+
m.subnetCreate.SetSize(m.width, m.height)
222+
return m, m.subnetCreate.Init()
223+
}
224+
225+
func (m Model) openSubnetDeleteConfirm() (Model, tea.Cmd) {
226+
subID := m.networkList.SelectedSubnetID()
227+
subName := m.networkList.SelectedSubnetName()
228+
if subID == "" {
229+
return m, nil
230+
}
231+
netName := m.networkList.SelectedNetworkName()
232+
m.confirm = modal.NewConfirm("delete_subnet", subID, subName)
233+
m.confirm.Title = "Delete Subnet"
234+
m.confirm.Body = fmt.Sprintf("Delete subnet %q from network %q?", subName, netName)
235+
m.confirm.SetSize(m.width, m.height)
236+
m.activeModal = modalConfirm
237+
return m, nil
238+
}
239+
169240
// --- Load Balancer actions ---
170241

171242
func (m Model) openLBDetail() (Model, tea.Cmd) {

src/internal/app/actions_server.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,39 @@ func (m Model) executeAction(action modal.ConfirmAction) (Model, tea.Cmd) {
517517
}
518518
return shared.ResourceActionMsg{Action: "Disassociated", Name: name}
519519
}
520+
case "delete_network":
521+
netClient := m.client.Network
522+
id := action.ServerID
523+
name := action.Name
524+
return m, func() tea.Msg {
525+
err := network.DeleteNetwork(context.Background(), netClient, id)
526+
if err != nil {
527+
return shared.ResourceActionErrMsg{Action: "Delete network", Name: name, Err: err}
528+
}
529+
return shared.ResourceActionMsg{Action: "Deleted network", Name: name}
530+
}
531+
case "delete_subnet":
532+
netClient := m.client.Network
533+
id := action.ServerID
534+
name := action.Name
535+
return m, func() tea.Msg {
536+
err := network.DeleteSubnet(context.Background(), netClient, id)
537+
if err != nil {
538+
return shared.ResourceActionErrMsg{Action: "Delete subnet", Name: name, Err: err}
539+
}
540+
return shared.ResourceActionMsg{Action: "Deleted subnet", Name: name}
541+
}
542+
case "delete_sg":
543+
netClient := m.client.Network
544+
id := action.ServerID
545+
name := action.Name
546+
return m, func() tea.Msg {
547+
err := network.DeleteSecurityGroup(context.Background(), netClient, id)
548+
if err != nil {
549+
return shared.ResourceActionErrMsg{Action: "Delete security group", Name: name, Err: err}
550+
}
551+
return shared.ResourceActionMsg{Action: "Deleted security group", Name: name}
552+
}
520553
case "delete_sg_rule":
521554
netClient := m.client.Network
522555
id := action.ServerID

0 commit comments

Comments
 (0)