diff --git a/cmd/ignite/cmd/vmcmd/ps.go b/cmd/ignite/cmd/vmcmd/ps.go index 31bf222ed..e9982a4f7 100644 --- a/cmd/ignite/cmd/vmcmd/ps.go +++ b/cmd/ignite/cmd/vmcmd/ps.go @@ -21,6 +21,28 @@ func NewCmdPs(out io.Writer) *cobra.Command { Long: dedent.Dedent(` List all running VMs. By specifying the all flag (-a, --all), also list VMs that are not currently running. + Using the -f (--filter) flag, you can give conditions VMs should fullfilled to be displayed. + You can filter on all the underlying fields of the VM struct, see the documentation: + https://ignite.readthedocs.io/en/stable/api/ignite_v1alpha2.html#VM. + + Different operators can be used: + - "=" and "==" for the equal + - "!=" for the is not equal + - "=~" for the contains + - "!~" for the not contains + + Non-exhaustive list of identifiers to apply filter on: + - the VM name + - CPUs usage + - Labels + - Image + - Kernel + - Memory + + Example usage: + $ ignite ps -f "{{.ObjectMeta.Name}}=my-vm2,{{.Spec.CPUs}}!=3,{{.Spec.Image.OCI}}=~weaveworks/ignite-ubuntu" + + $ ignite ps -f "{{.Spec.Memory}}=~1024,{{.Status.Running}}=true" `), Run: func(cmd *cobra.Command, args []string) { // If `ps` is called via any of its aliases @@ -46,4 +68,5 @@ func NewCmdPs(out io.Writer) *cobra.Command { func addPsFlags(fs *pflag.FlagSet, pf *run.PsFlags) { fs.BoolVarP(&pf.All, "all", "a", false, "Show all VMs, not just running ones") + fs.StringVarP(&pf.Filter, "filter", "f", "", "Filter the VMs") } diff --git a/cmd/ignite/run/ps.go b/cmd/ignite/run/ps.go index db93555db..fd4a768b3 100644 --- a/cmd/ignite/run/ps.go +++ b/cmd/ignite/run/ps.go @@ -10,7 +10,8 @@ import ( ) type PsFlags struct { - All bool + All bool + Filter string } type psOptions struct { @@ -25,16 +26,37 @@ func (pf *PsFlags) NewPsOptions() (po *psOptions, err error) { } func Ps(po *psOptions) error { + var filters *filter.MultipleMetaFilter + var err error + var filtering bool + if po.PsFlags.Filter != "" { + filtering = true + filters, err = filter.GenerateMultipleMetadataFiltering(po.PsFlags.Filter) + if err != nil { + return err + } + } o := util.NewOutput() defer o.Flush() o.Write("VM ID", "IMAGE", "KERNEL", "SIZE", "CPUS", "MEMORY", "CREATED", "STATUS", "IPS", "PORTS", "NAME") for _, vm := range po.allVMs { - o.Write(vm.GetUID(), vm.Spec.Image.OCI, vm.Spec.Kernel.OCI, - vm.Spec.DiskSize, vm.Spec.CPUs, vm.Spec.Memory, formatCreated(vm), formatStatus(vm), vm.Status.IPAddresses, - vm.Spec.Network.Ports, vm.GetName()) + isExpectedVM := true + if filtering { + isExpectedVM, err = filters.AreExpected(vm) + if err != nil { + return err + } + } + if err != nil { + return err + } + if isExpectedVM { + o.Write(vm.GetUID(), vm.Spec.Image.OCI, vm.Spec.Kernel.OCI, + vm.Spec.DiskSize, vm.Spec.CPUs, vm.Spec.Memory, formatCreated(vm), formatStatus(vm), vm.Status.IPAddresses, + vm.Spec.Network.Ports, vm.GetName()) + } } - return nil } diff --git a/docs/cli/ignite/ignite_ps.md b/docs/cli/ignite/ignite_ps.md index 50b68aa07..b24b1d346 100644 --- a/docs/cli/ignite/ignite_ps.md +++ b/docs/cli/ignite/ignite_ps.md @@ -7,6 +7,28 @@ List running VMs List all running VMs. By specifying the all flag (-a, --all), also list VMs that are not currently running. +Using the -f (--filter) flag, you can give conditions VMs should fullfilled to be displayed. +You can filter on all the underlying fields of the VM struct, see the documentation: +https://ignite.readthedocs.io/en/stable/api/ignite_v1alpha2.html#VM. + +Different operators can be used: +- "=" and "==" for the equal +- "!=" for the is not equal +- "=~" for the contains +- "!~" for the not contains + +Non-exhaustive list of identifiers to apply filter on: +- the VM name +- CPUs usage +- Labels +- Image +- Kernel +- Memory + +Example usage: + $ ignite ps -f "{{.ObjectMeta.Name}}=my-vm2,{{.Spec.CPUs}}!=3,{{.Spec.Image.OCI}}=~weaveworks/ignite-ubuntu" + + $ ignite ps -f "{{.Spec.Memory}}=~1024,{{.Status.Running}}=true" ``` @@ -16,8 +38,9 @@ ignite ps [flags] ### Options ``` - -a, --all Show all VMs, not just running ones - -h, --help help for ps + -a, --all Show all VMs, not just running ones + -f, --filter string Filter the VMs + -h, --help help for ps ``` ### Options inherited from parent commands diff --git a/docs/cli/ignite/ignite_vm_ps.md b/docs/cli/ignite/ignite_vm_ps.md index 9bcb7165c..0f7c9b4ae 100644 --- a/docs/cli/ignite/ignite_vm_ps.md +++ b/docs/cli/ignite/ignite_vm_ps.md @@ -7,6 +7,28 @@ List running VMs List all running VMs. By specifying the all flag (-a, --all), also list VMs that are not currently running. +Using the -f (--filter) flag, you can give conditions VMs should fullfilled to be displayed. +You can filter on all the underlying fields of the VM struct, see the documentation: +https://ignite.readthedocs.io/en/stable/api/ignite_v1alpha2.html#VM. + +Different operators can be used: +- "=" and "==" for the equal +- "!=" for the is not equal +- "=~" for the contains +- "!~" for the not contains + +Non-exhaustive list of identifiers to apply filter on: +- the VM name +- CPUs usage +- Labels +- Image +- Kernel +- Memory + +Example usage: + $ ignite ps -f "{{.ObjectMeta.Name}}=my-vm2,{{.Spec.CPUs}}!=3,{{.Spec.Image.OCI}}=~weaveworks/ignite-ubuntu" + + $ ignite ps -f "{{.Spec.Memory}}=~1024,{{.Status.Running}}=true" ``` @@ -16,8 +38,9 @@ ignite vm ps [flags] ### Options ``` - -a, --all Show all VMs, not just running ones - -h, --help help for ps + -a, --all Show all VMs, not just running ones + -f, --filter string Filter the VMs + -h, --help help for ps ``` ### Options inherited from parent commands diff --git a/pkg/filter/meta.go b/pkg/filter/meta.go new file mode 100644 index 000000000..fe35496ed --- /dev/null +++ b/pkg/filter/meta.go @@ -0,0 +1,118 @@ +package filter + +import ( + "bytes" + "fmt" + "regexp" + "strings" + "text/template" + + api "github.com/weaveworks/ignite/pkg/apis/ignite" +) + +const ( + filterSeparator = "," + filterApplyFailed = "failed to apply filtering" + regexString = `^(?P{{(?:\.|[a-zA-Z0-9]+)+}})(?P(?:=|==|!=|=~|!~))(?P[a-zA-Z0-9-_:/\.\s]+)$` +) + +type metaFilter struct { + identifier string + expectedValue string + operator string +} + +func (mf metaFilter) isExpected(object *api.VM) (bool, error) { + w := &bytes.Buffer{} + tm, err := template.New("generic-filtering-vm").Parse(mf.identifier) + if err != nil { + return false, fmt.Errorf("failed to configure filtering with following template: %s", mf.identifier) + } + err = tm.Execute(w, object) + if err != nil { + return false, fmt.Errorf("failed to apply filtering on VM, the filter might be incorrect") + } + res := w.String() + switch mf.operator { + case "==": + return mf.isEqual(res), nil + case "=": + return mf.isEqual(res), nil + case "!=": + return !mf.isEqual(res), nil + case "=~": + return mf.contains(res), nil + case "!~": + return !mf.contains(res), nil + default: + return false, fmt.Errorf("Unexpected operator: %s", mf.operator) + } +} + +func (mf metaFilter) isEqual(value string) bool { + return mf.expectedValue == value +} + +func (mf metaFilter) contains(value string) bool { + return strings.Contains(value, mf.expectedValue) +} + +// MultipleMetaFilter stores multiples metaFilter rule +type MultipleMetaFilter struct { + filters []metaFilter +} + +// AreExpected checks fileting rules are expected, an AND logical condition is applid between the underlying filters +func (mmf *MultipleMetaFilter) AreExpected(object *api.VM) (bool, error) { + for _, mf := range mmf.filters { + res, err := mf.isExpected(object) + if err != nil { + return false, err + } else if !res { + return false, nil + } + } + return true, nil +} + +// extractKeyValueFiltering extracts the key to search for and the expected value form a string +func extractKeyValueFiltering(str string) (string, string, string, error) { + reg, err := regexp.Compile(regexString) + if err != nil { + return "", "", "", err + } + matches := reg.FindAllStringSubmatch(str, -1) + if len(matches) != 1 { + return "", "", "", fmt.Errorf("failed to generate filter") + } + match := matches[0] + if len(match) != 4 { + return "", "", "", fmt.Errorf("failed to generate filter") + } + return match[1], match[3], match[2], nil +} + +// extractMultipleKeyValueFiltering extracts all the keys and values to filter +func extractMultipleKeyValueFiltering(f string) ([]metaFilter, error) { + filterList := strings.Split(f, filterSeparator) + captureList := make([]metaFilter, 0, len(filterList)) + for _, filter := range filterList { + key, value, op, err := extractKeyValueFiltering(filter) + if err != nil { + return nil, fmt.Errorf("failed to extract keys-values from filter list %s", filterList) + } + captureList = append(captureList, metaFilter{identifier: key, expectedValue: value, operator: op}) + } + return captureList, nil +} + +// GenerateMultipleMetadataFiltering extract filterings and generates MultipleMetadataFiltering +func GenerateMultipleMetadataFiltering(str string) (*MultipleMetaFilter, error) { + metaFilterList, err := extractMultipleKeyValueFiltering(str) + if err != nil { + return nil, err + } + return &MultipleMetaFilter{ + filters: metaFilterList, + }, nil +} diff --git a/pkg/filter/meta_test.go b/pkg/filter/meta_test.go new file mode 100644 index 000000000..30b65be37 --- /dev/null +++ b/pkg/filter/meta_test.go @@ -0,0 +1,517 @@ +package filter + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/weaveworks/gitops-toolkit/pkg/runtime" + api "github.com/weaveworks/ignite/pkg/apis/ignite" +) + +func TestMetaFiltering(t *testing.T) { + t.Run("SuccessCPUsEqual", func(t *testing.T) { + oMeta := &api.VM{ + ObjectMeta: runtime.ObjectMeta{ + UID: runtime.UID("myuid"), + Created: runtime.Time{}, + Labels: map[string]string{ + "first": "f_value", + "second": "s_value", + }, + }, + Spec: api.VMSpec{ + CPUs: uint64(2), + }, + } + + f := metaFilter{ + identifier: "{{.Spec.CPUs}}", + expectedValue: "2", + operator: "=", + } + + res, err := f.isExpected(oMeta) + assert.Nil(t, err) + assert.True(t, res) + + }) + t.Run("SuccessNameEqual", func(t *testing.T) { + oMeta := &api.VM{ + ObjectMeta: runtime.ObjectMeta{ + Name: "success_object", + UID: runtime.UID("myuid"), + Created: runtime.Time{}, + Labels: map[string]string{ + "first": "f_value", + "second": "s_value", + }, + }, + } + + f := metaFilter{ + identifier: "{{.ObjectMeta.Name}}", + expectedValue: "success_object", + operator: "=", + } + + res, err := f.isExpected(oMeta) + assert.Nil(t, err) + assert.True(t, res) + }) + t.Run("SuccessNameDiff", func(t *testing.T) { + oMeta := &api.VM{ + ObjectMeta: runtime.ObjectMeta{ + Name: "success_object", + UID: runtime.UID("myuid"), + Created: runtime.Time{}, + Labels: map[string]string{ + "first": "f_value", + "second": "s_value", + }, + }, + } + + f := metaFilter{ + identifier: "{{.Name}}", + expectedValue: "success_object_diff", + operator: "!=", + } + + res, err := f.isExpected(oMeta) + assert.Nil(t, err) + assert.True(t, res) + }) + t.Run("FailNameEqual", func(t *testing.T) { + oMeta := &api.VM{ + ObjectMeta: runtime.ObjectMeta{ + Name: "fail_object", + }, + } + + f := metaFilter{ + identifier: "{{.Name}}", + expectedValue: "success_object", + operator: "=", + } + + res, err := f.isExpected(oMeta) + assert.Nil(t, err) + assert.False(t, res) + }) + t.Run("FailNameDiff", func(t *testing.T) { + oMeta := &api.VM{ + ObjectMeta: runtime.ObjectMeta{ + Name: "fail_object", + }, + } + + f := metaFilter{ + identifier: "{{.Name}}", + expectedValue: "fail_object", + operator: "!=", + } + + res, err := f.isExpected(oMeta) + assert.Nil(t, err) + assert.False(t, res) + }) + t.Run("SuccessNameContains", func(t *testing.T) { + oMeta := &api.VM{ + ObjectMeta: runtime.ObjectMeta{ + Name: "fail_object", + }, + } + + f := metaFilter{ + identifier: "{{.Name}}", + expectedValue: "object", + operator: "=~", + } + + res, err := f.isExpected(oMeta) + assert.Nil(t, err) + assert.True(t, res) + }) + t.Run("SuccessNameNotContains", func(t *testing.T) { + oMeta := &api.VM{ + ObjectMeta: runtime.ObjectMeta{ + Name: "fail_object", + }, + } + + f := metaFilter{ + identifier: "{{.Name}}", + expectedValue: "object2", + operator: "!~", + } + + res, err := f.isExpected(oMeta) + assert.Nil(t, err) + assert.True(t, res) + }) + t.Run("FailNameContains", func(t *testing.T) { + oMeta := &api.VM{ + ObjectMeta: runtime.ObjectMeta{ + Name: "fail_object", + }, + } + + f := metaFilter{ + identifier: "{{.Name}}", + expectedValue: "object2", + operator: "=~", + } + + res, err := f.isExpected(oMeta) + assert.Nil(t, err) + assert.False(t, res) + }) + t.Run("FailNameNotContains", func(t *testing.T) { + oMeta := &api.VM{ + ObjectMeta: runtime.ObjectMeta{ + Name: "fail_object", + }, + } + + f := metaFilter{ + identifier: "{{.Name}}", + expectedValue: "object", + operator: "!~", + } + + res, err := f.isExpected(oMeta) + assert.Nil(t, err) + assert.False(t, res) + }) + t.Run("SuccessUID", func(t *testing.T) { + oMeta := &api.VM{ + ObjectMeta: runtime.ObjectMeta{ + UID: runtime.UID("myuid"), + }, + } + + f := metaFilter{ + identifier: "{{.UID}}", + expectedValue: "myuid", + operator: "=", + } + + res, err := f.isExpected(oMeta) + assert.Nil(t, err) + assert.True(t, res) + }) + t.Run("FailUID", func(t *testing.T) { + oMeta := &api.VM{ + ObjectMeta: runtime.ObjectMeta{ + UID: "failuid", + }, + } + + f := metaFilter{ + identifier: "{{.UID}}", + expectedValue: "myuid", + operator: "=", + } + + res, err := f.isExpected(oMeta) + assert.Nil(t, err) + assert.False(t, res) + }) + t.Run("SuccessCreated", func(t *testing.T) { + nowtime := runtime.Timestamp() + oMeta := &api.VM{ + ObjectMeta: runtime.ObjectMeta{ + Created: nowtime, + }, + } + + f := metaFilter{ + identifier: "{{.Created}}", + expectedValue: nowtime.String(), + operator: "=", + } + + res, err := f.isExpected(oMeta) + assert.Nil(t, err) + assert.True(t, res) + }) + t.Run("FailCreated", func(t *testing.T) { + nowtime := runtime.Timestamp() + oMeta := &api.VM{ + ObjectMeta: runtime.ObjectMeta{ + Created: nowtime, + }, + } + + othertime := nowtime.Add(time.Duration(5)) + f := metaFilter{ + identifier: "{{.Created}}", + expectedValue: othertime.String(), + operator: "=", + } + + res, err := f.isExpected(oMeta) + assert.Nil(t, err) + assert.False(t, res) + }) + t.Run("SuccessLabels", func(t *testing.T) { + oMeta := &api.VM{ + ObjectMeta: runtime.ObjectMeta{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + } + + f := metaFilter{ + identifier: "{{.Labels.foo}}", + expectedValue: "bar", + operator: "=", + } + + res, err := f.isExpected(oMeta) + assert.Nil(t, err) + assert.True(t, res) + }) + t.Run("FailLabels", func(t *testing.T) { + oMeta := &api.VM{ + ObjectMeta: runtime.ObjectMeta{ + Labels: map[string]string{ + "foo": "bar2", + }, + }, + } + + f := metaFilter{ + identifier: "{{.Labels.foo}}", + expectedValue: "bar", + operator: "=", + } + + res, err := f.isExpected(oMeta) + assert.Nil(t, err) + assert.False(t, res) + }) +} + +func TestExtractKeyValueFiltering(t *testing.T) { + tests := []struct { + name string + str string + key string + val string + op string + err error + }{ + { + name: "Success1", + str: "{{.Name}}=t/a-r:g_et", + key: "{{.Name}}", + val: "t/a-r:g_et", + op: "=", + err: nil, + }, + { + name: "Success2", + str: "{{.Name}}!=ta-rg_et", + key: "{{.Name}}", + val: "ta-rg_et", + op: "!=", + err: nil, + }, + { + name: "Success3", + str: "{{.Name}}==ta-rg_et", + key: "{{.Name}}", + val: "ta-rg_et", + op: "==", + err: nil, + }, + { + name: "Success4", + str: "{{.Name}}=~ta-rg_et", + key: "{{.Name}}", + val: "ta-rg_et", + op: "=~", + err: nil, + }, + { + name: "Success5", + str: "{{.Name}}!~ta-rg_et", + key: "{{.Name}}", + val: "ta-rg_et", + op: "!~", + err: nil, + }, + { + name: "Success6", + str: "{{.Name}}=8", + key: "{{.Name}}", + val: "8", + op: "=", + err: nil, + }, + { + name: "FailEqualBadPlace", + str: "{{.Name=}}target", + key: "", + val: "", + op: "", + err: fmt.Errorf("expected error"), + }, + { + name: "FailEqualBadPlace2", + str: "={{.Name}}target", + key: "", + val: "", + op: "", + err: fmt.Errorf("expected error"), + }, + { + name: "FailEqualBadPlace3", + str: "{{.Name}}tar=get", + key: "", + val: "", + op: "", + err: fmt.Errorf("expected error"), + }, + } + for _, utest := range tests { + t.Run(utest.name, func(t *testing.T) { + key, val, op, err := extractKeyValueFiltering(utest.str) + if utest.err == nil { + assert.Nil(t, err) + } else { + assert.NotNil(t, err) + } + assert.Equal(t, utest.key, key) + assert.Equal(t, utest.val, val) + assert.Equal(t, utest.op, op) + }) + } +} + +func TestExtractMultipleKeyValueFiltering(t *testing.T) { + tests := []struct { + name string + str string + res []metaFilter + err error + }{ + { + name: "Success", + str: "{{.Name}}=target1,{{.Age}}=38", + res: []metaFilter{ + + { + identifier: "{{.Name}}", + expectedValue: "target1", + operator: "=", + }, + { + identifier: "{{.Age}}", + expectedValue: "38", + operator: "=", + }, + }, + err: nil, + }, + { + name: "FailWithoutSeparator", + str: "{{.Name}}=target1{{.Age}}=38", + res: nil, + err: fmt.Errorf("expected error"), + }, + { + name: "FailBadFormat", + str: "{{.Name}}=target1{{.Age}}38", + res: nil, + err: fmt.Errorf("expected error"), + }, + } + for _, utest := range tests { + t.Run(utest.name, func(t *testing.T) { + res, err := extractMultipleKeyValueFiltering(utest.str) + if err != nil { + assert.NotNil(t, err) + } else { + assert.Nil(t, err) + } + assert.Equal(t, utest.res, res) + }) + } +} + +func TestMultipleMetaFilter(t *testing.T) { + tests := []struct { + name string + str string + object *api.VM + expected bool + err error + }{ + { + name: "SuccessOneFilter", + str: "{{.Name}}=hello", + object: &api.VM{ + ObjectMeta: runtime.ObjectMeta{ + Name: "hello", + UID: "123", + }, + }, + expected: true, + err: nil, + }, + { + name: "SuccessTwoFilter", + str: "{{.Name}}=hello,{{.UID}}=123", + object: &api.VM{ + ObjectMeta: runtime.ObjectMeta{ + Name: "hello", + UID: "123", + }, + }, + expected: true, + err: nil, + }, + { + name: "SuccessOneValueDiffer", + str: "{{.Name}}=hello,{{.UID}}=1234", + object: &api.VM{ + ObjectMeta: runtime.ObjectMeta{ + Name: "hello", + UID: "123", + }, + }, + expected: false, + err: nil, + }, + { + name: "FailBadFormat", + str: "{{.Name}}=hello,{{.Unexisting}}=1234", + object: &api.VM{ + ObjectMeta: runtime.ObjectMeta{ + Name: "hello", + UID: "123", + }, + }, + expected: false, + err: fmt.Errorf("expected error"), + }, + } + + for _, utest := range tests { + t.Run(utest.name, func(t *testing.T) { + mmf, err := GenerateMultipleMetadataFiltering(utest.str) + expected, err := mmf.AreExpected(utest.object) + if utest.err != nil { + assert.NotNil(t, err) + } else { + assert.Nil(t, err) + } + assert.Equal(t, utest.expected, expected) + }) + } +}