Skip to content

Support server-side filtering #1872

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 1, 2025
Merged
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
45 changes: 33 additions & 12 deletions cmd/incus/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"io"
"os"
"path/filepath"
"reflect"
"regexp"
"sort"
"strings"
Expand All @@ -15,6 +16,7 @@ import (

incus "github.com/lxc/incus/v6/client"
cli "github.com/lxc/incus/v6/internal/cmd"
internalFilter "github.com/lxc/incus/v6/internal/filter"
"github.com/lxc/incus/v6/internal/i18n"
internalUtil "github.com/lxc/incus/v6/internal/util"
"github.com/lxc/incus/v6/shared/api"
Expand Down Expand Up @@ -1262,9 +1264,7 @@ func (c *cmdImageList) imageShouldShow(filters []string, state *api.Image) bool
}

for configKey, configValue := range state.Properties {
list := cmdList{}
list.global = c.global
if list.dotPrefixMatch(key, configKey) {
if internalFilter.DotPrefixMatch(key, configKey) {
// try to test filter value as a regexp
regexpValue := value
if !(strings.Contains(value, "^") || strings.Contains(value, "$")) {
Expand Down Expand Up @@ -1353,7 +1353,8 @@ func (c *cmdImageList) Run(cmd *cobra.Command, args []string) error {
return err
}

serverFilters, clientFilters := getServerSupportedFilters(filters, api.Image{}, false)
serverFilters, clientFilters := getServerSupportedFilters(filters, []string{}, false)
serverFilters = prepareImageServerFilters(serverFilters, api.Image{})

var allImages, images []api.Image
if c.flagAllProjects {
Expand All @@ -1373,20 +1374,13 @@ func (c *cmdImageList) Run(cmd *cobra.Command, args []string) error {
}
}

data := [][]string{}
for _, image := range allImages {
if !c.imageShouldShow(clientFilters, &image) {
continue
}

images = append(images, image)
}

// Render the table
data := [][]string{}
for _, image := range images {
if !c.imageShouldShow(clientFilters, &image) {
continue
}

row := []string{}
for _, column := range columns {
Expand Down Expand Up @@ -1743,3 +1737,30 @@ func structToMap(data any) map[string]any {

return mapData
}

// prepareImageServerFilter processes and formats filter criteria
// for images, ensuring they are in a format that the server can interpret.
func prepareImageServerFilters(filters []string, i any) []string {
formatedFilters := []string{}

for _, filter := range filters {
membs := strings.SplitN(filter, "=", 2)

if len(membs) == 1 {
continue
}

firstPart := membs[0]
if strings.Contains(membs[0], ".") {
firstPart = strings.Split(membs[0], ".")[0]
}

if !structHasField(reflect.TypeOf(i), firstPart) {
filter = fmt.Sprintf("properties.%s", filter)
}

formatedFilters = append(formatedFilters, filter)
}

return formatedFilters
}
16 changes: 16 additions & 0 deletions cmd/incus/image_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package main

import (
"testing"

"github.com/stretchr/testify/assert"

"github.com/lxc/incus/v6/shared/api"
)

func TestPrepareImageServerFilters(t *testing.T) {
filters := []string{"foo", "requirements.secureboot=false", "type=container"}

result := prepareImageServerFilters(filters, api.InstanceFull{})
assert.Equal(t, []string{"properties.requirements.secureboot=false", "type=container"}, result)
}
159 changes: 49 additions & 110 deletions cmd/incus/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"fmt"
"net"
"os"
"regexp"
"reflect"
"slices"
"sort"
"strconv"
Expand Down Expand Up @@ -158,25 +158,7 @@ const (
deviceColumnType = "devices"
)

// This seems a little excessive.
func (c *cmdList) dotPrefixMatch(short string, full string) bool {
fullMembs := strings.Split(full, ".")
shortMembs := strings.Split(short, ".")

if len(fullMembs) != len(shortMembs) {
return false
}

for i := range fullMembs {
if !strings.HasPrefix(fullMembs[i], shortMembs[i]) {
return false
}
}

return true
}

func (c *cmdList) shouldShow(filters []string, inst *api.Instance, state *api.InstanceState, initial bool) bool {
func (c *cmdList) shouldShow(filters []string, inst *api.Instance, state *api.InstanceState) bool {
c.mapShorthandFilters()

for _, filter := range filters {
Expand All @@ -190,43 +172,11 @@ func (c *cmdList) shouldShow(filters []string, inst *api.Instance, state *api.In
value = membs[1]
}

if initial || c.evaluateShorthandFilter(key, value, inst, state) {
continue
}

found := false
for configKey, configValue := range inst.ExpandedConfig {
if c.dotPrefixMatch(key, configKey) {
// Try to test filter value as a regexp.
regexpValue := value
if !strings.Contains(value, "^") && !strings.Contains(value, "$") {
regexpValue = "^" + regexpValue + "$"
}

r, err := regexp.Compile(regexpValue)
// If not regexp compatible use original value.
if err != nil {
if value == configValue {
found = true
break
}

// The property was found but didn't match.
return false
} else if r.MatchString(configValue) {
found = true
break
}
}
}

if inst.ExpandedConfig[key] == value {
if c.evaluateShorthandFilter(key, value, inst, state) {
continue
}

if !found {
return false
}
return false
}

return true
Expand Down Expand Up @@ -422,7 +372,7 @@ func (c *cmdList) showInstances(instances []api.InstanceFull, filters []string,
instancesFiltered := []api.InstanceFull{}

for _, inst := range instances {
if !c.shouldShow(filters, &inst.Instance, inst.State, false) {
if !c.shouldShow(filters, &inst.Instance, inst.State) {
continue
}

Expand Down Expand Up @@ -501,29 +451,12 @@ func (c *cmdList) Run(cmd *cobra.Command, args []string) error {
return err
}

// Support for alternative filter names.
for i, filter := range filters {
fields := strings.SplitN(filter, "=", 2)
if len(fields) == 2 && fields[0] == "state" {
filters[i] = fmt.Sprintf("status=%s", fields[1])
}
}

singleValueFilterModifier := func(value string) string {
regexpValue := value
if !strings.Contains(value, "^") && !strings.Contains(value, "$") {
regexpValue = "^" + regexpValue + "$"
}

return fmt.Sprintf("name=(%s|^%s.*)", regexpValue, value)
}

if needsData && d.HasExtension("container_full") {
// Using the GetInstancesFull shortcut
var instances []api.InstanceFull

serverFilters, clientFilters := getServerSupportedFilters(filters, api.InstanceFull{}, true)
modifySingleValueFilters(serverFilters, singleValueFilterModifier)
serverFilters, clientFilters := getServerSupportedFilters(filters, []string{"ipv4", "ipv6"}, true)
serverFilters = prepareInstanceServerFilters(serverFilters, api.InstanceFull{})

if c.flagAllProjects {
instances, err = d.GetInstancesFullAllProjectsWithFilter(api.InstanceTypeAny, serverFilters)
Expand All @@ -540,8 +473,8 @@ func (c *cmdList) Run(cmd *cobra.Command, args []string) error {

// Get the list of instances
var instances []api.Instance
serverFilters, clientFilters := getServerSupportedFilters(filters, api.Instance{}, true)
modifySingleValueFilters(serverFilters, singleValueFilterModifier)
serverFilters, clientFilters := getServerSupportedFilters(filters, []string{"ipv4", "ipv6"}, true)
serverFilters = prepareInstanceServerFilters(serverFilters, api.Instance{})

if c.flagAllProjects {
instances, err = d.GetInstancesAllProjectsWithFilter(api.InstanceTypeAny, serverFilters)
Expand All @@ -553,18 +486,8 @@ func (c *cmdList) Run(cmd *cobra.Command, args []string) error {
return err
}

// Apply filters
instancesFiltered := []api.Instance{}
for _, inst := range instances {
if !c.shouldShow(clientFilters, &inst, nil, true) {
continue
}

instancesFiltered = append(instancesFiltered, inst)
}

// Fetch any remaining data and render the table
return c.listInstances(d, instancesFiltered, clientFilters, columns)
return c.listInstances(d, instances, clientFilters, columns)
}

func (c *cmdList) parseColumns(clustered bool) ([]column, bool, error) {
Expand Down Expand Up @@ -967,22 +890,6 @@ func (c *cmdList) locationColumnData(cInfo api.InstanceFull) string {
return cInfo.Location
}

func (c *cmdList) matchByType(cInfo *api.Instance, cState *api.InstanceState, query string) bool {
return strings.EqualFold(cInfo.Type, query)
}

func (c *cmdList) matchByStatus(cInfo *api.Instance, cState *api.InstanceState, query string) bool {
return strings.EqualFold(cInfo.Status, query)
}

func (c *cmdList) matchByArchitecture(cInfo *api.Instance, cState *api.InstanceState, query string) bool {
return strings.EqualFold(cInfo.InstancePut.Architecture, query)
}

func (c *cmdList) matchByLocation(cInfo *api.Instance, cState *api.InstanceState, query string) bool {
return strings.EqualFold(cInfo.Location, query)
}

func (c *cmdList) matchByNet(cState *api.InstanceState, query string, family string) bool {
// Skip if no state.
if cState == nil {
Expand Down Expand Up @@ -1034,12 +941,44 @@ func (c *cmdList) matchByIPV4(_ *api.Instance, cState *api.InstanceState, query

func (c *cmdList) mapShorthandFilters() {
c.shorthandFilters = map[string]func(*api.Instance, *api.InstanceState, string) bool{
"type": c.matchByType,
"state": c.matchByStatus,
"status": c.matchByStatus,
"architecture": c.matchByArchitecture,
"location": c.matchByLocation,
"ipv4": c.matchByIPV4,
"ipv6": c.matchByIPV6,
"ipv4": c.matchByIPV4,
"ipv6": c.matchByIPV6,
}
}

// prepareInstanceServerFilters processes and formats filter criteria
// for instances, ensuring they are in a format that the server can interpret.
func prepareInstanceServerFilters(filters []string, i any) []string {
formatedFilters := []string{}

for _, filter := range filters {
membs := strings.SplitN(filter, "=", 2)
key := membs[0]

if len(membs) == 1 {
regexpValue := key
if !strings.Contains(key, "^") && !strings.Contains(key, "$") {
regexpValue = "^" + regexpValue + "$"
}

filter = fmt.Sprintf("name=(%s|^%s.*)", regexpValue, key)
} else {
firstPart := key
if strings.Contains(key, ".") {
firstPart = strings.Split(key, ".")[0]
}

if !structHasField(reflect.TypeOf(i), firstPart) {
filter = fmt.Sprintf("expanded_config.%s", filter)
}

if key == "state" {
filter = fmt.Sprintf("status=%s", membs[1])
}
}

formatedFilters = append(formatedFilters, filter)
}

return formatedFilters
}
Loading