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
snap: add validation of gadget.yaml #6634
snap: add validation of gadget.yaml #6634
Conversation
Add validation of gadget definition based on what ubuntu-image does. Try to match error message where possible. Signed-off-by: Maciej Borzecki <maciej.zenon.borzecki@canonical.com>
Signed-off-by: Maciej Borzecki <maciej.zenon.borzecki@canonical.com>
Related to this work: what is the future interaction with ubuntu-image? I would expect that there is only one implementation of certain things. Will ubuntu-image depend on "snap" to pack/validate gadgets? Will we have to live with two implementations? |
As discussed, we'd like to eventually grow the functionality to be able to assemble the image ourselves. At which time ubuntu-image could be updated to use |
@bboozzoo that is a good plan. Thanks |
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.
Looks nice, some comments and nitpicks for consideration but no blockers, feel free to ignore most of them (or act, depending on how much you like them).
snap/gadget.go
Outdated
@@ -223,6 +244,7 @@ func ReadGadgetInfo(info *Info, classic bool) (*GadgetInfo, error) { | |||
default: | |||
return nil, fmt.Errorf(errorFormat, "bootloader must be one of grub, u-boot or android-boot") | |||
} | |||
|
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.
Extra whitespace? Or deliberate?
snap/gadget.go
Outdated
@@ -234,6 +256,225 @@ func ReadGadgetInfo(info *Info, classic bool) (*GadgetInfo, error) { | |||
return &gi, nil | |||
} | |||
|
|||
func indexAndName(idx int, name string) string { |
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.
(nitpick²) maybe fmtIndexAndName()
?
snap/gadget.go
Outdated
if err := validateVolumeStructure(&s, vol); err != nil { | ||
maybeName := "" | ||
if s.Name != "" { | ||
maybeName = fmt.Sprintf("(%q)", s.Name) |
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.
Don't we need a leading space here: " (%q)"
? The error will otherwise concat idx and name to something like 1("foo")
. Or am I misreading (or is it intentional)?
snap/gadget.go
Outdated
|
||
func validateVolume(name string, vol *GadgetVolume) error { | ||
if !validVolumeName.MatchString(name) { | ||
return errors.New("invalid volume name") |
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.
Do we need to include what volume name string is invalid here (i.e. - I see that this is wrapped.fmt.Error("invalid volume name %q")
, only ? Or will this error be wrapped somewhere that includes this info?
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.
do we need to mention "volume" in this error?
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.
Updated. The error just below needs the same treatment.
snap/gadget.go
Outdated
indexAndName(idx, s.Name), s.OffsetWrite.RelativeTo) | ||
} | ||
|
||
if s.Filesystem != "" && s.Filesystem != "none" { |
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.
We have a "IsBare" helper, could that be used here?
snap/gadget.go
Outdated
func ParseGadgetRelativeOffset(grs string) (*GadgetRelativeOffset, error) { | ||
toWhat := "" | ||
sizeSpec := grs | ||
idx := strings.IndexRune(grs, '+') |
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.
Would strings.Split()
be slightly shorter here?
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.
Did the change locally, looks very much the same.
snap/gadget.go
Outdated
if err != nil { | ||
return nil, fmt.Errorf("cannot parse offset %q: %v", sizeSpec, err) | ||
} | ||
if size > 4*2<<30 { |
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.
(nitpick) maybe var limit4G = 4*2<<30 \n if size > limit4G ...
?
target: / | ||
`) | ||
|
||
func mustParseGadgetSize(c *C, s string) snap.GadgetSize { |
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.
(nitpick²) all(?) of the other "mustFoo()" do not take a "check.C" and just panic. Probably not worth changing though.
|
||
// ParseGadgetSize parses a string expressing size in gadget declaration. The | ||
// accepted format is one of: <bytes> | <bytes/2^20>M | <bytes/2^30>G. | ||
func ParseGadgetSize(gs string) (GadgetSize, error) { |
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.
We have similar code in strutil.ParseByteSize
I was wondering if we could unify those, but the trouble is of course is that that strutil.ParseByteSize uses MB MegaByte (10^6) vs this code here which uses MiB (^20) but uses "M" as the ending which is not the correct extentision, it should be "MiB". So probably easiest to just keep it as is.
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.
Yeah, I wanted to use the helper we have, but the formats accepted in gadget.yaml are only these 3:
<number>
(bytes)<number>M
<number>G
snap/gadget_test.go
Outdated
err string | ||
}{ | ||
{"1234", 1234, ""}, | ||
{"1234M", 1234 * 2 << 20, ""}, |
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.
Given how many 2 << 20
and 2 << 30
I'm seeing I wonder if we should have a const in snapd for this somewhere? Like "2units.Mebibyte" and "2units.Gigibyte" etc ?
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.
Old partial review, starting afresh.
snap/gadget.go
Outdated
) | ||
|
||
var ( | ||
validVolumeName = regexp.MustCompile("^[a-z-]+$") |
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.
This allows a volume to start with -
, is that desirable? Do volume names have to be lower case? I would suspect at least some volumes wanting to have all-caps name for some legacy reason.
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.
Looks like ubuntu-image is bad too, this is regexp they use: ^[-a-zA-Z0-9]+$
, and this is what the docs page states: [a-z-]+
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.
Updated to match what ubuntu-image does.
var ( | ||
validVolumeName = regexp.MustCompile("^[a-z-]+$") | ||
validTypeID = regexp.MustCompile("^[0-9A-F]{2}$") | ||
validGUUID = regexp.MustCompile("^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$") |
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.
Nice :)
for _, v := range gi.Volumes { | ||
for name, v := range gi.Volumes { | ||
if err := validateVolume(name, &v); err != nil { | ||
return nil, fmt.Errorf("invalid volume %q: %v", name, err) |
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.
Would it be sensible that the error you create here was actually returned by validateVolume
and that this place simply returned it verbatim?
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.
Ha! I was thinking exactly the same and then wondered if we have a pattern and then wondered if go has a pattern and then got lost in the weeds. But I still wonder if there is a pattern for this in the go community in cases like this where it seems like both choices make sense. Maybe @pedronis has an opinion/thoughts?
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.
There's this https://github.com/pkg/errors that has very nicely defined Wrap
function: https://github.com/pkg/errors/blob/master/errors.go#L181 for the case of taking one error and wrapping it with some more messages.
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.
You can have multiple volumes in gadget.yaml. Returning error as is would imply the validate*
has to include all necessary information to be able to locate the invalid entity.
FWIW we have a similar problem with code in snap/validate.go
.
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.
I think this if fine, we just need to be careful not to use "volume" in the errors below too unnecessarily
Signed-off-by: Maciej Borzecki <maciej.zenon.borzecki@canonical.com>
Signed-off-by: Maciej Borzecki <maciej.zenon.borzecki@canonical.com>
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.
thank you, did a first pass
for _, v := range gi.Volumes { | ||
for name, v := range gi.Volumes { | ||
if err := validateVolume(name, &v); err != nil { | ||
return nil, fmt.Errorf("invalid volume %q: %v", name, err) |
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.
I think this if fine, we just need to be careful not to use "volume" in the errors below too unnecessarily
snap/gadget.go
Outdated
|
||
func validateVolume(name string, vol *GadgetVolume) error { | ||
if !validVolumeName.MatchString(name) { | ||
return errors.New("invalid volume name") |
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.
do we need to mention "volume" in this error?
if name != "" { | ||
return fmt.Sprintf("#%v (%q)", idx, name) | ||
} | ||
return fmt.Sprintf("#%v", idx) |
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.
this path seems not to be exercised in the tests
|
||
// GadgetSize describes the size of a structure item or an offset within the | ||
// structure. | ||
type GadgetSize uint64 |
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.
it's fine (in that world)
snap/gadget.go
Outdated
names := make(map[string]bool, len(vs.Update.Preserve)) | ||
for _, n := range vs.Update.Preserve { | ||
if names[n] { | ||
return fmt.Errorf("duplicate preserve entry %q", n) |
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.
this reads strangely, maybe quotes help:
duplicate "preserve" entry %q
snap/gadget.go
Outdated
bare := vs.IsBare() | ||
for i, c := range vs.Content { | ||
var err error | ||
if bare { |
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.
but we could simply set a checker outside and not have the if here:
var contentChecker func(*VolumeContent) error
structureNames := make(map[string]bool, len(vol.Structure)) | ||
for idx, s := range vol.Structure { | ||
if err := validateVolumeStructure(&s, vol); err != nil { | ||
return fmt.Errorf("invalid structure %s: %v", fmtIndexAndName(idx, s.Name), err) |
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.
seems tests never get to this error
} | ||
|
||
if s == "" { | ||
return errors.New(`type is not specified`) |
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.
this error is not exercised
snap/gadget.go
Outdated
|
||
func validateVolumeStructure(vs *VolumeStructure, vol *GadgetVolume) error { | ||
if err := validateStructureType(vs.Type, vol); err != nil { | ||
return fmt.Errorf("invalid type %q: %v", vs.Type, err) |
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.
tests don't get to this error
snap/gadget.go
Outdated
return fmt.Errorf("invalid type %q: %v", vs.Type, err) | ||
} | ||
if err := validateRole(vs, vol); err != nil { | ||
return fmt.Errorf("invalid role %q: %v", vs.Role, err) |
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.
tests don't get to this error
Signed-off-by: Maciej Borzecki <maciej.zenon.borzecki@canonical.com>
Signed-off-by: Maciej Borzecki <maciej.zenon.borzecki@canonical.com>
Signed-off-by: Maciej Borzecki <maciej.zenon.borzecki@canonical.com>
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.
thank you
snap/gadget.go
Outdated
Filesystem string `yaml:"filesystem"` | ||
Content []VolumeContent `yaml:"content"` | ||
Update VolumeUpdate `yaml:"update"` | ||
Name string `yaml:"name"` |
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.
What is the difference between the label and the name?
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.
Label is just a filesystem label. Name written as partition name if GPT schema is in use, otherwise it's informative.
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.
Ah, can you document that in the structure please?
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.
LGTM because it's green and it is Friday but please look at my comments for several simple follow-ups.
return fmt.Sprintf("#%v", idx) | ||
} | ||
|
||
func validateVolume(name string, vol *GadgetVolume) error { |
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.
+1
return nil | ||
} | ||
|
||
func validateVolumeStructure(vs *VolumeStructure, vol *GadgetVolume) error { |
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.
+1
// - "bare" | ||
// - [0-9A-Z]{2} - MBR type | ||
// - GPT UUID | ||
// - hybrid ID |
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.
Perhaps add a wikipedia reference to explain what hybrid IDs are for.
return nil | ||
} | ||
|
||
func validateStructureType(s string, vol *GadgetVolume) error { |
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.
+1
return nil | ||
} | ||
|
||
func validateStructureUpdate(up *VolumeUpdate, vs *VolumeStructure) error { |
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.
+1
|
||
// ParseGadgetSize parses a string expressing size in gadget declaration. The | ||
// accepted format is one of: <bytes> | <bytes/2^20>M | <bytes/2^30>G. | ||
func ParseGadgetSize(gs string) (GadgetSize, error) { |
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.
+1
if err != nil { | ||
return nil, fmt.Errorf("cannot parse offset %q: %v", sizeSpec, err) | ||
} | ||
if size > 4*SizeGiB { |
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.
The comment below is redundant. Can you please add a reference to the origin of the limit?
Signed-off-by: Maciej Borzecki <maciej.zenon.borzecki@canonical.com>
Pushed a patch adding comments to volume, structure and content, based on what's there in https://docs.snapcraft.io/the-gadget-snap/696. |
Add validation of gadget definition based on what ubuntu-image does. Try to match error message where possible.
The validation only considers the structure of gadget.yaml, internal references and sanity of defined entries.
Things like validation of structure overlap, offset sizes are best done in a separate step, when the structure is 'resolved' for writing. Meaning, that the information from gadget.yaml is conbined with the contents of the gadget snap (eg. when
size
is left unspecified) and platform requirements (eg. default sector size). Theubuntu-image
tries to do that upfront, but makes some assumptions along the way.The PR contains #6609