diff --git a/asserts/snap_asserts.go b/asserts/snap_asserts.go index dc3266db83e..84dcbe09a6e 100644 --- a/asserts/snap_asserts.go +++ b/asserts/snap_asserts.go @@ -676,6 +676,13 @@ func checkSnapRevisionWhat(headers map[string]interface{}, name, what string) (s return snapRevision, nil } +func checkOptionalSnapRevisionWhat(headers map[string]interface{}, name, what string) (snapRevision int, err error) { + if _, ok := headers[name]; !ok { + return 0, nil + } + return checkSnapRevisionWhat(headers, name, what) +} + func assembleSnapRevision(assert assertionBase) (Assertion, error) { _, err := checkDigest(assert.headers, "snap-sha3-384", crypto.SHA3_384) if err != nil { diff --git a/asserts/validation_set.go b/asserts/validation_set.go index e2831517b8a..67b0060a012 100644 --- a/asserts/validation_set.go +++ b/asserts/validation_set.go @@ -20,6 +20,7 @@ package asserts import ( + "errors" "fmt" "regexp" "strings" @@ -48,8 +49,8 @@ func presencesAsStrings(presences ...Presence) []string { var validValidationSetSnapPresences = presencesAsStrings(PresenceRequired, PresenceOptional, PresenceInvalid) -func checkPresence(snap map[string]interface{}, which string, valid []string) (Presence, error) { - presence, err := checkOptionalStringWhat(snap, "presence", which) +func checkOptionalPresence(headers map[string]interface{}, which string, valid []string) (Presence, error) { + presence, err := checkOptionalStringWhat(headers, "presence", which) if err != nil { return Presence(""), err } @@ -59,6 +60,17 @@ func checkPresence(snap map[string]interface{}, which string, valid []string) (P return Presence(presence), nil } +func checkPresence(headers map[string]interface{}, which string, valid []string) (Presence, error) { + presence, err := checkExistsStringWhat(headers, "presence", which) + if err != nil { + return "", err + } + if presence != "" && !strutil.ListContains(valid, presence) { + return "", fmt.Errorf("presence %s must be one of %s", which, strings.Join(valid, "|")) + } + return Presence(presence), nil +} + // ValidationSetSnap holds the details about a snap constrained by a validation-set assertion. type ValidationSetSnap struct { Name string @@ -66,6 +78,12 @@ type ValidationSetSnap struct { Presence Presence + Revision int + Components map[string]ValidationSetComponent +} + +type ValidationSetComponent struct { + Presence Presence Revision int } @@ -95,7 +113,7 @@ func checkValidationSetSnap(snap map[string]interface{}) (*ValidationSetSnap, er return nil, err } - presence, err := checkPresence(snap, what, validValidationSetSnapPresences) + presence, err := checkOptionalPresence(snap, what, validValidationSetSnapPresences) if err != nil { return nil, err } @@ -112,11 +130,76 @@ func checkValidationSetSnap(snap map[string]interface{}) (*ValidationSetSnap, er return nil, fmt.Errorf(`cannot specify revision %s at the same time as stating its presence is invalid`, what) } + components, err := checkValidationSetComponents(snap, what) + if err != nil { + return nil, err + } + return &ValidationSetSnap{ - Name: name, - SnapID: snapID, + Name: name, + SnapID: snapID, + Presence: presence, + Revision: snapRevision, + Components: components, + }, nil +} + +func checkValidationSetComponents(snap map[string]interface{}, what string) (map[string]ValidationSetComponent, error) { + mapping, err := checkMapWhat(snap, "components", what) + if err != nil { + return nil, errors.New(`"components" field in "snaps" header must be a map`) + } + + if len(mapping) == 0 { + return nil, nil + } + + components := make(map[string]ValidationSetComponent, len(mapping)) + for name, comp := range mapping { + var parsed map[string]interface{} + switch c := comp.(type) { + case map[string]interface{}: + parsed = c + case string: + parsed = map[string]interface{}{"presence": c} + default: + return nil, errors.New(`each field in "components" map must be either a map or a string`) + } + + component, err := checkValidationSetComponent(parsed, name) + if err != nil { + return nil, err + } + components[name] = component + } + + return components, nil +} + +func checkValidationSetComponent(comp map[string]interface{}, name string) (ValidationSetComponent, error) { + if err := naming.ValidateSnap(name); err != nil { + return ValidationSetComponent{}, fmt.Errorf("invalid component name %q", name) + } + + what := fmt.Sprintf("of component %q", name) + + presence, err := checkPresence(comp, what, validValidationSetSnapPresences) + if err != nil { + return ValidationSetComponent{}, err + } + + revision, err := checkOptionalSnapRevisionWhat(comp, "revision", what) + if err != nil { + return ValidationSetComponent{}, err + } + + if revision != 0 && presence == PresenceInvalid { + return ValidationSetComponent{}, fmt.Errorf(`cannot specify component revision %s at the same time as stating its presence is invalid`, what) + } + + return ValidationSetComponent{ Presence: presence, - Revision: snapRevision, + Revision: revision, }, nil } @@ -125,7 +208,7 @@ func checkValidationSetSnaps(snapList interface{}) ([]*ValidationSetSnap, error) entries, ok := snapList.([]interface{}) if !ok { - return nil, fmt.Errorf(wrongHeaderType) + return nil, errors.New(wrongHeaderType) } seen := make(map[string]bool, len(entries)) @@ -134,7 +217,7 @@ func checkValidationSetSnaps(snapList interface{}) ([]*ValidationSetSnap, error) for _, entry := range entries { snap, ok := entry.(map[string]interface{}) if !ok { - return nil, fmt.Errorf(wrongHeaderType) + return nil, errors.New(wrongHeaderType) } valSetSnap, err := checkValidationSetSnap(snap) if err != nil { diff --git a/asserts/validation_set_test.go b/asserts/validation_set_test.go index 5d9c222fb4f..42e1f726ad5 100644 --- a/asserts/validation_set_test.go +++ b/asserts/validation_set_test.go @@ -124,9 +124,16 @@ func (vss *validationSetSuite) TestDecodeInvalid(c *C) { {"OTHER", " -\n name: baz-linux\n id: bazlinuxidididididididididididid\n", `cannot list the same snap "baz-linux" multiple times`}, {"OTHER", " -\n name: baz-linux2\n id: bazlinuxidididididididididididid\n", `cannot specify the same snap id "bazlinuxidididididididididididid" multiple times, specified for snaps "baz-linux" and "baz-linux2"`}, {"presence: optional\n", "presence:\n - opt\n", `"presence" of snap "baz-linux" must be a string`}, - {"presence: optional\n", "presence: no\n", `"presence" of snap "baz-linux" must be one of must be one of required|optional|invalid`}, + {"presence: optional\n", "presence: no\n", `presence of snap "baz-linux" must be one of required\|optional\|invalid`}, {"revision: 99\n", "revision: 0\n", `"revision" of snap "baz-linux" must be >=1: 0`}, {"presence: optional\n", "presence: invalid\n", `cannot specify revision of snap "baz-linux" at the same time as stating its presence is invalid`}, + {"OTHER", " components:\n comp: no\n", `presence of component "comp" must be one of required\|optional\|invalid`}, + {"OTHER", " components:\n comp:\n revision: 1\n presence: invalid\n", `cannot specify component revision of component "comp" at the same time as stating its presence is invalid`}, + {"OTHER", " components:\n c: optional\n", `invalid component name "c"`}, + {"OTHER", " components:\n comp:\n revision: 1\n", `"presence" of component "comp" is mandatory`}, + {"OTHER", " components:\n comp:\n - test\n", `each field in "components" map must be either a map or a string`}, + {"OTHER", " components:\n comp:\n revision: -1\n presence: optional\n", `"revision" of component "comp" must be >=1: -1`}, + {"OTHER", " components: some-string", `"components" field in "snaps" header must be a map`}, } for _, test := range invalidTests { @@ -167,6 +174,43 @@ func (vss *validationSetSuite) TestSnapRevisionOptional(c *C) { c.Check(snaps[0].Revision, Equals, 0) } +func (vss *validationSetSuite) TestSnapComponents(c *C) { + encoded := strings.Replace(validationSetExample, "TSLINE", vss.tsLine, 1) + + const components = ` components: + string-only: optional + with-revision: + revision: 10 + presence: required + no-revision: + presence: invalid +` + + encoded = strings.Replace(encoded, "OTHER", components, 1) + + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.ValidationSetType) + + valset := a.(*asserts.ValidationSet) + snaps := valset.Snaps() + c.Assert(snaps, HasLen, 1) + c.Assert(snaps[0].Components, HasLen, 3) + + c.Check(snaps[0].Components, DeepEquals, map[string]asserts.ValidationSetComponent{ + "string-only": { + Presence: asserts.PresenceOptional, + }, + "with-revision": { + Presence: asserts.PresenceRequired, + Revision: 10, + }, + "no-revision": { + Presence: asserts.PresenceInvalid, + }, + }) +} + func (vss *validationSetSuite) TestIsValidValidationSetName(c *C) { names := []struct { name string