-
Notifications
You must be signed in to change notification settings - Fork 673
Support filtering instances by labels #3659
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -63,6 +63,7 @@ The following legacy flags continue to function: | |||||
listCommand.Flags().Bool("json", false, "JSONify output") | ||||||
listCommand.Flags().BoolP("quiet", "q", false, "Only show names") | ||||||
listCommand.Flags().Bool("all-fields", false, "Show all fields") | ||||||
listCommand.Flags().StringToString("label", nil, "Filter instances by labels. Multiple labels can be specified (AND-match)") | ||||||
|
||||||
return listCommand | ||||||
} | ||||||
|
@@ -77,6 +78,16 @@ func instanceMatches(arg string, instances []string) []string { | |||||
return matches | ||||||
} | ||||||
|
||||||
// instanceMatchesAllLabels returns true if inst matches all labels, or, labels is nil. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
func instanceMatchesAllLabels(inst *store.Instance, labels map[string]string) bool { | ||||||
for k, v := range labels { | ||||||
if inst.Config.Labels[k] != v { | ||||||
return false | ||||||
} | ||||||
} | ||||||
return true | ||||||
} | ||||||
|
||||||
// unmatchedInstancesError is created when unmatched instance names found. | ||||||
type unmatchedInstancesError struct{} | ||||||
|
||||||
|
@@ -107,6 +118,10 @@ func listAction(cmd *cobra.Command, args []string) error { | |||||
if err != nil { | ||||||
return err | ||||||
} | ||||||
labels, err := cmd.Flags().GetStringToString("label") | ||||||
if err != nil { | ||||||
return err | ||||||
} | ||||||
|
||||||
if jsonFormat { | ||||||
format = "json" | ||||||
|
@@ -177,7 +192,12 @@ func listAction(cmd *cobra.Command, args []string) error { | |||||
if err != nil { | ||||||
return fmt.Errorf("unable to load instance %s: %w", instanceName, err) | ||||||
} | ||||||
instances = append(instances, instance) | ||||||
if instanceMatchesAllLabels(instance, labels) { | ||||||
instances = append(instances, instance) | ||||||
} | ||||||
} | ||||||
if len(instances) == 0 { | ||||||
return unmatchedInstancesError{} | ||||||
} | ||||||
|
||||||
for _, instance := range instances { | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
// SPDX-FileCopyrightText: Copyright The Lima Authors | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
// From https://github.com/containerd/containerd/blob/v2.1.1/pkg/identifiers/validate.go | ||
// SPDX-FileCopyrightText: Copyright The containerd Authors | ||
// LICENSE: https://github.com/containerd/containerd/blob/v2.1.1/LICENSE | ||
// NOTICE: https://github.com/containerd/containerd/blob/v2.1.1/NOTICE | ||
|
||
/* | ||
Copyright The containerd Authors. | ||
|
||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
|
||
http://www.apache.org/licenses/LICENSE-2.0 | ||
|
||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
// Package labels provides common validation for labels. | ||
// Labels are similar to [github.com/lima-vm/lima/pkg/identifiers], but allows '/'. | ||
package labels | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"regexp" | ||
) | ||
|
||
const ( | ||
maxLength = 76 | ||
alphanum = `[A-Za-z0-9]+` | ||
separators = `[/._-]` // contains slash, unlike identifiers | ||
) | ||
|
||
// labelRe defines the pattern for valid identifiers. | ||
var labelRe = regexp.MustCompile(reAnchor(alphanum + reGroup(separators+reGroup(alphanum)) + "*")) | ||
|
||
// Validate returns nil if the string s is a valid label. | ||
// | ||
// Labels are similar to [github.com/lima-vm/lima/pkg/identifiers], but allows '/'. | ||
// | ||
// Labels that pass this validation are NOT safe for use as filesystem path components. | ||
func Validate(s string) error { | ||
if s == "" { | ||
return errors.New("label must not be empty") | ||
} | ||
|
||
if len(s) > maxLength { | ||
return fmt.Errorf("label %q greater than maximum length (%d characters)", s, maxLength) | ||
} | ||
|
||
if !labelRe.MatchString(s) { | ||
return fmt.Errorf("label %q must match %v", s, labelRe) | ||
} | ||
return nil | ||
} | ||
|
||
func reGroup(s string) string { | ||
return `(?:` + s + `)` | ||
} | ||
|
||
func reAnchor(s string) string { | ||
return `^` + s + `$` | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
// SPDX-FileCopyrightText: Copyright The Lima Authors | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
// From https://github.com/containerd/containerd/blob/v2.1.1/pkg/identifiers/validate_test.go | ||
// SPDX-FileCopyrightText: Copyright The containerd Authors | ||
// LICENSE: https://github.com/containerd/containerd/blob/v2.1.1/LICENSE | ||
// NOTICE: https://github.com/containerd/containerd/blob/v2.1.1/NOTICE | ||
|
||
/* | ||
Copyright The containerd Authors. | ||
|
||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
|
||
http://www.apache.org/licenses/LICENSE-2.0 | ||
|
||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package labels | ||
|
||
import ( | ||
"strings" | ||
"testing" | ||
|
||
"gotest.tools/v3/assert" | ||
) | ||
|
||
func TestValidLabels(t *testing.T) { | ||
for _, input := range []string{ | ||
"default", | ||
"Default", | ||
t.Name(), | ||
"default-default", | ||
"containerd.io", | ||
"foo.boo", | ||
"swarmkit.docker.io", | ||
"0912341234", | ||
"task.0.0123456789", | ||
"container.system-75-f19a.00", | ||
"underscores_are_allowed", | ||
"foo/foo", | ||
"foo.example.com/foo", | ||
strings.Repeat("a", maxLength), | ||
} { | ||
t.Run(input, func(t *testing.T) { | ||
assert.NilError(t, Validate(input)) | ||
}) | ||
} | ||
} | ||
|
||
func TestInvalidLabels(t *testing.T) { | ||
for _, input := range []string{ | ||
"", | ||
".foo..foo", | ||
"foo/..", | ||
"foo..foo", | ||
"foo.-boo", | ||
"-foo.boo", | ||
"foo.boo-", | ||
"but__only_tasteful_underscores", | ||
"zn--e9.org", // or something like it! | ||
"default--default", | ||
strings.Repeat("a", maxLength+1), | ||
} { | ||
t.Run(input, func(t *testing.T) { | ||
assert.ErrorContains(t, Validate(input), "") | ||
}) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -22,6 +22,7 @@ import ( | |||
"github.com/sirupsen/logrus" | ||||
|
||||
"github.com/lima-vm/lima/pkg/identifiers" | ||||
"github.com/lima-vm/lima/pkg/labels" | ||||
"github.com/lima-vm/lima/pkg/localpathutil" | ||||
"github.com/lima-vm/lima/pkg/networks" | ||||
"github.com/lima-vm/lima/pkg/osutil" | ||||
|
@@ -54,6 +55,13 @@ func validateFileObject(f File, fieldName string) error { | |||
func Validate(y *LimaYAML, warn bool) error { | ||||
var errs error | ||||
|
||||
for k := range y.Labels { | ||||
if err := labels.Validate(k); err != nil { | ||||
errs = errors.Join(errs, fmt.Errorf("field `labels` has an invalid label %q: %w", k, err)) | ||||
} | ||||
// No validation for label values | ||||
} | ||||
Comment on lines
+58
to
+63
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's cover these lines with a unit test in the file validate_test.go lima/pkg/limayaml/validate_test.go Line 4 in f2a2b64
|
||||
|
||||
if len(y.Base) > 0 { | ||||
errs = errors.Join(errs, errors.New("field `base` must be empty for YAML validation")) | ||||
} | ||||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -5,6 +5,11 @@ | |||||
# Default values in this YAML file are specified by `null` instead of Lima's "builtin default" values, | ||||||
# so they can be overridden by the $LIMA_HOME/_config/default.yaml mechanism documented at the end of this file. | ||||||
|
||||||
# Arbitrary labels. e.g., "category", "description". | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
# 🟢 Builtin default: {} | ||||||
# labels: | ||||||
# KEY: value | ||||||
|
||||||
# VM type: "qemu", "vz" (on macOS 13 and later), or "default". | ||||||
# The vmType can be specified only on creating the instance. | ||||||
# The vmType of existing instances cannot be changed. | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not
labels
?For consistency with
lima/cmd/limactl/usernet.go
Line 31 in f2a2b64
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we add an example of labels with values here?