diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml
index 740bf95..9941810 100644
--- a/.idea/dictionaries/project.xml
+++ b/.idea/dictionaries/project.xml
@@ -5,6 +5,7 @@
keypairs
loadbalancer
recordsets
+ securitygroups
\ No newline at end of file
diff --git a/internal/resource/commands.go b/internal/resource/commands.go
index 63f0faf..d2e0971 100644
--- a/internal/resource/commands.go
+++ b/internal/resource/commands.go
@@ -11,6 +11,12 @@ type NavigateFilteredMsg struct {
Value string
}
+type NavigateFilteredMultiMsg struct {
+ Resource string
+ Field string
+ Values []string
+}
+
type DetailsMsg struct {
ID string
Content any
@@ -21,3 +27,7 @@ type NavigateScopedMsg struct {
Resource string
Scope map[string]string
}
+
+type ErrorMsg struct {
+ Err error
+}
diff --git a/internal/resources/compute/servers/commands.go b/internal/resources/compute/servers/commands.go
index b794d04..1deaa6b 100644
--- a/internal/resources/compute/servers/commands.go
+++ b/internal/resources/compute/servers/commands.go
@@ -7,6 +7,7 @@ import (
"github.com/akyriako/o7k/internal/resource"
tea "github.com/charmbracelet/bubbletea"
computeservers "github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers"
+ "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports"
)
func (r *Resource) image(row resource.Row) tea.Cmd {
@@ -33,6 +34,55 @@ func (r *Resource) flavor(row resource.Row) tea.Cmd {
}
}
+// securityGroups violates the "no N+1 calls" rule, but this is a special case because we
+// have to first load the ports and then find out the corresponding security group ids.
+// this method should not be considered as the norm of jumping to resources
+func (r *Resource) securityGroups(row resource.Row) tea.Cmd {
+ return func() tea.Msg {
+
+ client, err := r.context.NetworkV2()
+ if err != nil {
+ return resource.ErrorMsg{Err: err}
+ }
+
+ pages, err := ports.List(client, ports.ListOpts{
+ DeviceID: row.ID,
+ }).AllPages(context.Background())
+ if err != nil {
+ return resource.ErrorMsg{
+ Err: fmt.Errorf("listing ports for server %q: %w", row.ID, err),
+ }
+ }
+
+ items, err := ports.ExtractPorts(pages)
+ if err != nil {
+ return resource.ErrorMsg{
+ Err: fmt.Errorf("extracting ports for server %q: %w", row.ID, err),
+ }
+ }
+
+ uniqueSecurityGroups := make(map[string]struct{})
+ securityGroupIDs := make([]string, 0)
+
+ for _, port := range items {
+ for _, securityGroupID := range port.SecurityGroups {
+ if _, exists := uniqueSecurityGroups[securityGroupID]; exists {
+ continue
+ }
+
+ uniqueSecurityGroups[securityGroupID] = struct{}{}
+ securityGroupIDs = append(securityGroupIDs, securityGroupID)
+ }
+ }
+
+ return resource.NavigateFilteredMultiMsg{
+ Resource: "securitygroups",
+ Field: "id",
+ Values: securityGroupIDs,
+ }
+ }
+}
+
func (r *Resource) show(id string) tea.Cmd {
return func() tea.Msg {
client, err := r.context.ComputeV2()
diff --git a/internal/resources/compute/servers/servers.go b/internal/resources/compute/servers/servers.go
index a264e47..126675b 100644
--- a/internal/resources/compute/servers/servers.go
+++ b/internal/resources/compute/servers/servers.go
@@ -41,8 +41,7 @@ func (r *Resource) Columns() []resource.Column {
{Key: "name", Title: "NAME", MinWidth: 24, Flex: 1},
{Key: "status", Title: "STATUS", MinWidth: 12, Flex: 0},
{Key: "flavor", Title: "FLAVOR", MinWidth: 15, Flex: 1},
- {Key: "image", Title: "IMAGE", MinWidth: 40, Flex: 0},
- {Key: "addresses", Title: "ADDRESSES", MinWidth: 40, Flex: 2},
+ {Key: "addresses", Title: "ADDRESSES", MinWidth: 40, Flex: 1},
}
}
@@ -51,6 +50,7 @@ func (r *Resource) Commands() []resource.Command {
{Key: "s", Description: "Show", Default: true},
{Key: "shift-i", Description: "Image"},
{Key: "shift-f", Description: "Flavor"},
+ {Key: "shift-g", Description: "Security Groups"},
}
}
@@ -96,9 +96,7 @@ func (r *Resource) List(ctx context.Context) ([]resource.Row, error) {
flavorName = flavorID
}
- //fmt.Printf("SERVER IMAGE: server=%q image=%#v\n", server.Name, server.Image)
imageID, _ := server.Image["id"].(string)
-
image := imageID
if name, ok := server.Image["name"].(string); ok && name != "" {
image = name
@@ -124,7 +122,7 @@ func (r *Resource) List(ctx context.Context) ([]resource.Row, error) {
func serverAddresses(addresses map[string]any) string {
values := make([]string, 0)
- for network, raw := range addresses {
+ for _, raw := range addresses {
items, ok := raw.([]any)
if !ok {
continue
@@ -141,7 +139,7 @@ func serverAddresses(addresses map[string]any) string {
continue
}
- values = append(values, network+"="+ip)
+ values = append(values, ip)
}
}
@@ -156,6 +154,8 @@ func (r *Resource) Execute(command resource.Command, row resource.Row) tea.Cmd {
return r.image(row)
case "shift-f":
return r.flavor(row)
+ case "shift-g":
+ return r.securityGroups(row)
}
return nil
diff --git a/internal/ui/app.go b/internal/ui/app.go
index 14f191b..c13ad40 100644
--- a/internal/ui/app.go
+++ b/internal/ui/app.go
@@ -173,6 +173,17 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
slog.Error(msg.err.Error(), "cloud", m.context.Cloud)
return m, nil
+ case resource.ErrorMsg:
+ m.autoRefreshPaused = true
+ m.loading = false
+ m.showLoading = false
+ m.loadingLabel = ""
+
+ m.err = msg.Err
+
+ slog.Error(msg.Err.Error(), "cloud", m.context.Cloud)
+ return m, nil
+
case clipboardResultMsg:
m.status = "copied to clipboard"
return m, clearStatus(m.status)
@@ -480,6 +491,26 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, m.navigateScopedResource(msg.Resource)
+ case resource.NavigateFilteredMultiMsg:
+ cursor := m.table.Cursor()
+
+ if cursor >= 0 && cursor < len(m.resourceRows) {
+ m.navigation = append(m.navigation, navigationEntry{
+ resource: m.resource.Kind(),
+ id: m.resourceRows[cursor].ID,
+ filter: m.filter,
+ scope: m.scope,
+ })
+ }
+
+ m.navigateID = ""
+ m.filter = &resourceFilter{
+ field: msg.Field,
+ values: msg.Values,
+ }
+
+ return m, m.navigateFilteredResource(msg.Resource)
+
case resource.DetailsMsg:
m.showLoading = false
m.loadingLabel = ""
diff --git a/internal/ui/commands.go b/internal/ui/commands.go
index fc9c66c..f8e7f02 100644
--- a/internal/ui/commands.go
+++ b/internal/ui/commands.go
@@ -3,6 +3,7 @@ package ui
import (
"context"
"fmt"
+ "slices"
"strings"
"time"
@@ -43,8 +44,9 @@ type navigationEntry struct {
}
type resourceFilter struct {
- field string
- value string
+ field string
+ value string
+ values []string
}
type autoRefreshMsg struct{}
@@ -230,7 +232,17 @@ func filterResourceRows(rows []resource.Row, filter *resourceFilter) []resource.
filtered := make([]resource.Row, 0, len(rows))
for _, row := range rows {
- if row.Fields[filter.field] == filter.value {
+ value := row.Fields[filter.field]
+
+ if len(filter.values) > 0 {
+ if slices.Contains(filter.values, value) {
+ filtered = append(filtered, row)
+ }
+
+ continue
+ }
+
+ if value == filter.value {
filtered = append(filtered, row)
}
}