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) } }