diff --git a/asserts/header_checks.go b/asserts/header_checks.go index 3a91089ff58..f8263212f8c 100644 --- a/asserts/header_checks.go +++ b/asserts/header_checks.go @@ -62,18 +62,22 @@ func checkNotEmptyStringWhat(m map[string]interface{}, name, what string) (strin return s, nil } -func checkOptionalString(headers map[string]interface{}, name string) (string, error) { +func checkOptionalStringWhat(headers map[string]interface{}, name, what string) (string, error) { value, ok := headers[name] if !ok { return "", nil } s, ok := value.(string) if !ok { - return "", fmt.Errorf("%q header must be a string", name) + return "", fmt.Errorf("%q %s must be a string", name, what) } return s, nil } +func checkOptionalString(headers map[string]interface{}, name string) (string, error) { + return checkOptionalStringWhat(headers, name, "header") +} + func checkPrimaryKey(headers map[string]interface{}, primKey string) (string, error) { value, err := checkNotEmptyString(headers, primKey) if err != nil { diff --git a/asserts/model.go b/asserts/model.go index ca2e63b8172..9b2c078b218 100644 --- a/asserts/model.go +++ b/asserts/model.go @@ -25,16 +25,310 @@ import ( "strings" "time" + "github.com/snapcore/snapd/snap/channel" "github.com/snapcore/snapd/snap/naming" "github.com/snapcore/snapd/strutil" ) +// ModelSnap holds the details about a snap specified by a model assertion. +type ModelSnap struct { + Name string + SnapID string + // SnapType is one of: app|base|gadget|kernel|core, default is app + SnapType string + // Modes in which the snap must be made available + Modes []string + // DefaultChannel is the initial tracking channel, default is stable + DefaultChannel string + // Track is a locked track for the snap, if set DefaultChannel cannot be + Track string + // Presence is one of: required|optional + Presence string +} + +// SnapName implements naming.SnapRef. +func (s *ModelSnap) SnapName() string { + return s.Name +} + +// ID implements naming.SnapRef. +func (s *ModelSnap) ID() string { + return s.SnapID +} + +type modelSnaps struct { + Base *ModelSnap + Gadget *ModelSnap + Kernel *ModelSnap + Snaps []*ModelSnap +} + +func (ms *modelSnaps) list() (allSnaps []*ModelSnap, allRequiredSnaps []naming.SnapRef, numBootSnaps int) { + addSnap := func(snap *ModelSnap, bootSnap int) { + if snap == nil { + return + } + numBootSnaps += bootSnap + allSnaps = append(allSnaps, snap) + if snap.Presence == "required" { + allRequiredSnaps = append(allRequiredSnaps, snap) + } + } + + addSnap(ms.Base, 1) + addSnap(ms.Gadget, 1) + addSnap(ms.Kernel, 1) + for _, snap := range ms.Snaps { + addSnap(snap, 0) + } + return allSnaps, allRequiredSnaps, numBootSnaps +} + +var ( + bootSnapModes = []string{"run", "ephemeral"} + defaultModes = []string{"run"} +) + +func checkExtendSnaps(extendedSnaps interface{}, base string) (*modelSnaps, error) { + const wrongHeaderType = `"snaps" header must be a list of maps` + + entries, ok := extendedSnaps.([]interface{}) + if !ok { + return nil, fmt.Errorf(wrongHeaderType) + } + + var modelSnaps modelSnaps + seen := make(map[string]bool, len(entries)) + seenIDs := make(map[string]string, len(entries)) + + for _, entry := range entries { + snap, ok := entry.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf(wrongHeaderType) + } + modelSnap, err := checkModelSnap(snap) + if err != nil { + return nil, err + } + + if seen[modelSnap.Name] { + return nil, fmt.Errorf("cannot list the same snap %q multiple times", modelSnap.Name) + } + if underName := seenIDs[modelSnap.SnapID]; underName != "" { + return nil, fmt.Errorf("cannot specify the same snap id %q multiple times, specified for snaps %q and %q", modelSnap.SnapID, underName, modelSnap.Name) + } + seen[modelSnap.Name] = true + seenIDs[modelSnap.SnapID] = modelSnap.Name + + boot := false + switch { + case modelSnap.SnapType == "kernel": + boot = true + if modelSnaps.Kernel != nil { + return nil, fmt.Errorf("cannot specify multiple kernel snaps: %q and %q", modelSnaps.Kernel.Name, modelSnap.Name) + } + modelSnaps.Kernel = modelSnap + case modelSnap.SnapType == "gadget": + boot = true + if modelSnaps.Gadget != nil { + return nil, fmt.Errorf("cannot specify multiple gadget snaps: %q and %q", modelSnaps.Gadget.Name, modelSnap.Name) + } + modelSnaps.Gadget = modelSnap + case modelSnap.Name == base: + boot = true + if modelSnap.SnapType != "base" { + return nil, fmt.Errorf(`boot base %q must specify type "base", not %q`, base, modelSnap.SnapType) + } + modelSnaps.Base = modelSnap + } + + if boot { + if len(modelSnap.Modes) != 0 || modelSnap.Presence != "" { + return nil, fmt.Errorf("boot snaps are always available, cannot specify modes or presence for snap %q", modelSnap.Name) + } + modelSnap.Modes = bootSnapModes + } + + if len(modelSnap.Modes) == 0 { + modelSnap.Modes = defaultModes + } + if modelSnap.Presence == "" { + modelSnap.Presence = "required" + } + + if !boot { + modelSnaps.Snaps = append(modelSnaps.Snaps, modelSnap) + } + } + + return &modelSnaps, nil +} + +var ( + validSnapTypes = []string{"app", "base", "gadget", "kernel", "core"} + validSnapMode = regexp.MustCompile("^[a-z][-a-z]+$") + validSnapPresences = []string{"required", "optional"} +) + +func checkModelSnap(snap map[string]interface{}) (*ModelSnap, error) { + name, err := checkNotEmptyStringWhat(snap, "name", "of snap") + if err != nil { + return nil, err + } + if err := naming.ValidateSnap(name); err != nil { + return nil, fmt.Errorf("invalid snap name %q", name) + } + + what := fmt.Sprintf("of snap %q", name) + + snapID, err := checkStringMatchesWhat(snap, "id", what, validSnapID) + if err != nil { + return nil, err + } + + typ, err := checkOptionalStringWhat(snap, "type", what) + if err != nil { + return nil, err + } + if typ == "" { + typ = "app" + } + if !strutil.ListContains(validSnapTypes, typ) { + return nil, fmt.Errorf("type of snap %q must be one of app|base|gadget|kernel|core", name) + } + + modes, err := checkStringListInMap(snap, "modes", fmt.Sprintf("%q %s", "modes", what), validSnapMode) + if err != nil { + return nil, err + } + + defaultChannel, err := checkOptionalStringWhat(snap, "default-channel", what) + if err != nil { + return nil, err + } + // TODO: final name of this + track, err := checkOptionalStringWhat(snap, "track", what) + if err != nil { + return nil, err + } + + if defaultChannel != "" && track != "" { + return nil, fmt.Errorf("snap %q cannot specify both default channel and locked track", name) + } + if track == "" && defaultChannel == "" { + defaultChannel = "stable" + } + + if defaultChannel != "" { + _, err := channel.Parse(defaultChannel, "-") + if err != nil { + return nil, fmt.Errorf("invalid default channel for snap %q: %v", name, err) + } + } else { + trackCh, err := channel.ParseVerbatim(track, "-") + if err != nil || trackCh.Track == "" || trackCh.Risk != "" || trackCh.Branch != "" { + return nil, fmt.Errorf("invalid locked track for snap %q: %s", name, track) + } + } + + presence, err := checkOptionalStringWhat(snap, "presence", what) + if err != nil { + return nil, err + } + if presence != "" && !strutil.ListContains(validSnapPresences, presence) { + return nil, fmt.Errorf("presence of snap %q must be one of required|optional", name) + } + + return &ModelSnap{ + Name: name, + SnapID: snapID, + SnapType: typ, + Modes: modes, // can be empty + DefaultChannel: defaultChannel, + Track: track, + Presence: presence, // can be empty + }, nil +} + +// unextended case support + +func checkSnapWithTrack(headers map[string]interface{}, which string) (*ModelSnap, error) { + _, ok := headers[which] + if !ok { + return nil, nil + } + value, ok := headers[which].(string) + if !ok { + return nil, fmt.Errorf(`%q header must be a string`, which) + } + l := strings.SplitN(value, "=", 2) + + name := l[0] + track := "" + if err := validateSnapName(l[0], which); err != nil { + return nil, err + } + if len(l) > 1 { + track = l[1] + if strings.Count(track, "/") != 0 { + return nil, fmt.Errorf(`%q channel selector must be a track name only`, which) + } + channelRisks := []string{"stable", "candidate", "beta", "edge"} + if strutil.ListContains(channelRisks, track) { + return nil, fmt.Errorf(`%q channel selector must be a track name`, which) + } + } + + defaultChannel := "" + if track == "" { + defaultChannel = "stable" + } + + return &ModelSnap{ + Name: name, + SnapType: which, + Modes: defaultModes, + DefaultChannel: defaultChannel, + Track: track, + Presence: "required", + }, nil +} + +func validateSnapName(name string, headerName string) error { + if err := naming.ValidateSnap(name); err != nil { + return fmt.Errorf("invalid snap name in %q header: %s", headerName, name) + } + return nil +} + +func checkRequiredSnap(name string, headerName string, snapType string) (*ModelSnap, error) { + if err := validateSnapName(name, headerName); err != nil { + return nil, err + } + + return &ModelSnap{ + Name: name, + SnapType: snapType, + Modes: defaultModes, + DefaultChannel: "stable", + Presence: "required", + }, nil +} + // Model holds a model assertion, which is a statement by a brand // about the properties of a device model. type Model struct { assertionBase - classic bool - requiredSnaps []string + classic bool + + baseSnap *ModelSnap + gadgetSnap *ModelSnap + kernelSnap *ModelSnap + + allSnaps []*ModelSnap + allRequiredSnaps []naming.SnapRef + numBootSnaps int + sysUserAuthority []string timestamp time.Time } @@ -74,40 +368,49 @@ func (mod *Model) Architecture() string { return mod.HeaderString("architecture") } -// snapWithTrack represents a snap that includes optional track -// information like `snapName=trackName` -type snapWithTrack string - -func (s snapWithTrack) Snap() string { - return strings.SplitN(string(s), "=", 2)[0] -} - -func (s snapWithTrack) Track() string { - l := strings.SplitN(string(s), "=", 2) - if len(l) > 1 { - return l[1] - } - return "" +// GadgetSnap returns the details of the gadget snap the model uses. +func (mod *Model) GadgetSnap() *ModelSnap { + return mod.gadgetSnap } // Gadget returns the gadget snap the model uses. func (mod *Model) Gadget() string { - return snapWithTrack(mod.HeaderString("gadget")).Snap() + if mod.gadgetSnap == nil { + return "" + } + return mod.gadgetSnap.Name } // GadgetTrack returns the gadget track the model uses. +// XXX this should go away func (mod *Model) GadgetTrack() string { - return snapWithTrack(mod.HeaderString("gadget")).Track() + if mod.gadgetSnap == nil { + return "" + } + return mod.gadgetSnap.Track +} + +// KernelSnap returns the details of the kernel snap the model uses. +func (mod *Model) KernelSnap() *ModelSnap { + return mod.kernelSnap } // Kernel returns the kernel snap the model uses. +// XXX this should go away func (mod *Model) Kernel() string { - return snapWithTrack(mod.HeaderString("kernel")).Snap() + if mod.kernelSnap == nil { + return "" + } + return mod.kernelSnap.Name } // KernelTrack returns the kernel track the model uses. +// XXX this should go away func (mod *Model) KernelTrack() string { - return snapWithTrack(mod.HeaderString("kernel")).Track() + if mod.kernelSnap == nil { + return "" + } + return mod.kernelSnap.Track } // Base returns the base snap the model uses. @@ -115,14 +418,29 @@ func (mod *Model) Base() string { return mod.HeaderString("base") } +// BaseSnap returns the details of the base snap the model uses. +func (mod *Model) BaseSnap() *ModelSnap { + return mod.baseSnap +} + // Store returns the snap store the model uses. func (mod *Model) Store() string { return mod.HeaderString("store") } -// RequiredSnaps returns the snaps that must be installed at all times and cannot be removed for this model. -func (mod *Model) RequiredSnaps() []string { - return mod.requiredSnaps +// RequiredSnaps returns the snaps that must be installed at all times and cannot be removed for this model, excluding the boot snaps (gadget, kernel, boot base). +func (mod *Model) RequiredSnaps() []naming.SnapRef { + return mod.allRequiredSnaps[mod.numBootSnaps:] +} + +// AllRequiredSnaps returns the snaps that must be installed at all times and cannot be removed for this model, including the boot snaps (gadget, kernel, boot base). +func (mod *Model) AllRequiredSnaps() []naming.SnapRef { + return mod.allRequiredSnaps +} + +// AllSnaps returns all the snap listed by the model. +func (mod *Model) AllSnaps() []*ModelSnap { + return mod.allSnaps } // SystemUserAuthority returns the authority ids that are accepted as signers of system-user assertions for this model. Empty list means any. @@ -147,34 +465,6 @@ var _ consistencyChecker = (*Model)(nil) // limit model to only lowercase for now var validModel = regexp.MustCompile("^[a-zA-Z0-9](?:-?[a-zA-Z0-9])*$") -func checkSnapWithTrackHeader(header string, headers map[string]interface{}) error { - _, ok := headers[header] - if !ok { - return nil - } - value, ok := headers[header].(string) - if !ok { - return fmt.Errorf(`%q header must be a string`, header) - } - l := strings.SplitN(value, "=", 2) - - if err := validateSnapName(l[0], header); err != nil { - return err - } - if len(l) == 1 { - return nil - } - track := l[1] - if strings.Count(track, "/") != 0 { - return fmt.Errorf(`%q channel selector must be a track name only`, header) - } - channelRisks := []string{"stable", "candidate", "beta", "edge"} - if strutil.ListContains(channelRisks, track) { - return fmt.Errorf(`%q channel selector must be a track name`, header) - } - return nil -} - func checkModel(headers map[string]interface{}) (string, error) { s, err := checkStringMatches(headers, "model", validModel) if err != nil { @@ -219,17 +509,12 @@ func checkOptionalSystemUserAuthority(headers map[string]interface{}, brandID st } var ( - modelMandatory = []string{"architecture", "gadget", "kernel"} - classicModelOptional = []string{"architecture", "gadget"} + modelMandatory = []string{"architecture", "gadget", "kernel"} + extendedCoreMandatory = []string{"architecture", "base"} + extendedSnapsConflicting = []string{"gadget", "kernel", "required-snaps"} + classicModelOptional = []string{"architecture", "gadget"} ) -func validateSnapName(name string, headerName string) error { - if err := naming.ValidateSnap(name); err != nil { - return fmt.Errorf("invalid snap name in %q header: %s", headerName, name) - } - return nil -} - func assembleModel(assert assertionBase) (Assertion, error) { err := checkAuthorityMatchesBrand(&assert) if err != nil { @@ -246,7 +531,20 @@ func assembleModel(assert assertionBase) (Assertion, error) { return nil, err } - if classic { + // Core 20 extended snaps header + extendedSnaps, extended := assert.headers["snaps"] + if extended { + if classic { + return nil, fmt.Errorf("cannot use extended snaps header for a classic model (yet)") + } + + for _, conflicting := range extendedSnapsConflicting { + if _, ok := assert.headers[conflicting]; ok { + return nil, fmt.Errorf("cannot specify separate %q header once using the extended snaps header", conflicting) + } + } + + } else if classic { if _, ok := assert.headers["kernel"]; ok { return nil, fmt.Errorf("cannot specify a kernel with a classic model") } @@ -257,7 +555,9 @@ func assembleModel(assert assertionBase) (Assertion, error) { checker := checkNotEmptyString toCheck := modelMandatory - if classic { + if extended { + toCheck = extendedCoreMandatory + } else if classic { checker = checkOptionalString toCheck = classicModelOptional } @@ -268,46 +568,80 @@ func assembleModel(assert assertionBase) (Assertion, error) { } } - // kernel/gadget must be valid snap names and can have (optional) tracks - // - validate those - if err := checkSnapWithTrackHeader("kernel", assert.headers); err != nil { - return nil, err - } - if err := checkSnapWithTrackHeader("gadget", assert.headers); err != nil { - return nil, err + if !extended { } + // base, if provided, must be a valid snap name too + var baseSnap *ModelSnap base, err := checkOptionalString(assert.headers, "base") if err != nil { return nil, err } if base != "" { - if err := validateSnapName(base, "base"); err != nil { + baseSnap, err = checkRequiredSnap(base, "base", "base") + if err != nil { return nil, err } } // store is optional but must be a string, defaults to the ubuntu store - _, err = checkOptionalString(assert.headers, "store") - if err != nil { + if _, err = checkOptionalString(assert.headers, "store"); err != nil { return nil, err } // display-name is optional but must be a string - _, err = checkOptionalString(assert.headers, "display-name") - if err != nil { + if _, err = checkOptionalString(assert.headers, "display-name"); err != nil { return nil, err } - // required snap must be valid snap names - reqSnaps, err := checkStringList(assert.headers, "required-snaps") - if err != nil { - return nil, err - } - for _, name := range reqSnaps { - if err := validateSnapName(name, "required-snaps"); err != nil { + var modSnaps *modelSnaps + if extended { + // TODO: support and consider grade! + modSnaps, err = checkExtendSnaps(extendedSnaps, base) + if err != nil { return nil, err } + if modSnaps.Gadget == nil { + return nil, fmt.Errorf(`one "snaps" header entry must specify the model gadget`) + } + if modSnaps.Kernel == nil { + return nil, fmt.Errorf(`one "snaps" header entry must specify the model kernel`) + } + + if modSnaps.Base == nil { + // complete with defaults, + // the assumption is that base names are very stable + // essentially fixed + modSnaps.Base = baseSnap + modSnaps.Base.Modes = bootSnapModes + } + } else { + modSnaps = &modelSnaps{ + Base: baseSnap, + } + // kernel/gadget must be valid snap names and can have (optional) tracks + // - validate those + modSnaps.Kernel, err = checkSnapWithTrack(assert.headers, "kernel") + if err != nil { + return nil, err + } + modSnaps.Gadget, err = checkSnapWithTrack(assert.headers, "gadget") + if err != nil { + return nil, err + } + + // required snap must be valid snap names + reqSnaps, err := checkStringList(assert.headers, "required-snaps") + if err != nil { + return nil, err + } + for _, name := range reqSnaps { + reqSnap, err := checkRequiredSnap(name, "required-snaps", "") + if err != nil { + return nil, err + } + modSnaps.Snaps = append(modSnaps.Snaps, reqSnap) + } } sysUserAuthority, err := checkOptionalSystemUserAuthority(assert.headers, assert.HeaderString("brand-id")) @@ -320,6 +654,8 @@ func assembleModel(assert assertionBase) (Assertion, error) { return nil, err } + allSnaps, allRequiredSnaps, numBootSnaps := modSnaps.list() + // NB: // * core is not supported at this time, it defaults to ubuntu-core // in prepare-image until rename and/or introduction of the header. @@ -331,7 +667,12 @@ func assembleModel(assert assertionBase) (Assertion, error) { return &Model{ assertionBase: assert, classic: classic, - requiredSnaps: reqSnaps, + baseSnap: modSnaps.Base, + gadgetSnap: modSnaps.Gadget, + kernelSnap: modSnaps.Kernel, + allSnaps: allSnaps, + allRequiredSnaps: allRequiredSnaps, + numBootSnaps: numBootSnaps, sysUserAuthority: sysUserAuthority, timestamp: timestamp, }, nil diff --git a/asserts/model_test.go b/asserts/model_test.go index 3707a66d0a8..5d8e70cbeca 100644 --- a/asserts/model_test.go +++ b/asserts/model_test.go @@ -84,6 +84,57 @@ const ( "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + "\n\n" + "AXNpZw==" + + core20ModelExample = `type: model +authority-id: brand-id1 +series: 16 +brand-id: brand-id1 +model: baz-3000 +display-name: Baz 3000 +architecture: amd64 +system-user-authority: * +base: core20 +store: brand-store +snaps: + - + name: baz-linux + id: bazlinuxidididididididididididid + type: kernel + track: 20 + - + name: brand-gadget + id: brandgadgetdidididididididididid + type: gadget + - + name: other-base + id: otherbasedididididididididididid + type: base + modes: + - run + presence: required + - + name: nm + id: nmididididididididididididididid + modes: + - ephemeral + - run + default-channel: 1.0 + - + name: myapp + id: myappdididididididididididididid + type: app + default-channel: 2.0 + - + name: myappopt + id: myappoptidididididididididididid + type: app + presence: optional +OTHERgrade: stable +` + "TSLINE" + + "body-length: 0\n" + + "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" + + "\n\n" + + "AXNpZw==" ) func (mods *modelSuite) TestDecodeOK(c *C) { @@ -99,13 +150,60 @@ func (mods *modelSuite) TestDecodeOK(c *C) { c.Check(model.Model(), Equals, "baz-3000") c.Check(model.DisplayName(), Equals, "Baz 3000") c.Check(model.Architecture(), Equals, "amd64") + c.Check(model.GadgetSnap(), DeepEquals, &asserts.ModelSnap{ + Name: "brand-gadget", + SnapType: "gadget", + Modes: []string{"run"}, + DefaultChannel: "stable", + Presence: "required", + }) c.Check(model.Gadget(), Equals, "brand-gadget") c.Check(model.GadgetTrack(), Equals, "") + c.Check(model.KernelSnap(), DeepEquals, &asserts.ModelSnap{ + Name: "baz-linux", + SnapType: "kernel", + Modes: []string{"run"}, + DefaultChannel: "stable", + Presence: "required", + }) c.Check(model.Kernel(), Equals, "baz-linux") c.Check(model.KernelTrack(), Equals, "") c.Check(model.Base(), Equals, "core18") + c.Check(model.BaseSnap(), DeepEquals, &asserts.ModelSnap{ + Name: "core18", + SnapType: "base", + Modes: []string{"run"}, + DefaultChannel: "stable", + Presence: "required", + }) c.Check(model.Store(), Equals, "brand-store") - c.Check(model.RequiredSnaps(), DeepEquals, []string{"foo", "bar"}) + allSnaps := model.AllSnaps() + c.Check(allSnaps, DeepEquals, []*asserts.ModelSnap{ + model.BaseSnap(), + model.GadgetSnap(), + model.KernelSnap(), + { + Name: "foo", + Modes: []string{"run"}, + DefaultChannel: "stable", + Presence: "required", + }, + { + Name: "bar", + Modes: []string{"run"}, + DefaultChannel: "stable", + Presence: "required", + }, + }) + // boot snaps included + reqSnaps := model.AllRequiredSnaps() + c.Check(reqSnaps, HasLen, len(allSnaps)) + for i, r := range reqSnaps { + c.Check(r.SnapName(), Equals, allSnaps[i].Name) + c.Check(r.ID(), Equals, "") + } + // boot snaps excluded + c.Check(model.RequiredSnaps(), DeepEquals, reqSnaps[3:]) c.Check(model.SystemUserAuthority(), HasLen, 0) } @@ -137,6 +235,7 @@ func (mods *modelSuite) TestDecodeBaseIsOptional(c *C) { c.Assert(err, IsNil) model = a.(*asserts.Model) c.Check(model.Base(), Equals, "") + c.Check(model.BaseSnap(), IsNil) } func (mods *modelSuite) TestDecodeDisplayNameIsOptional(c *C) { @@ -275,6 +374,13 @@ func (mods *modelSuite) TestDecodeKernelTrack(c *C) { a, err := asserts.Decode([]byte(encoded)) c.Assert(err, IsNil) model := a.(*asserts.Model) + c.Check(model.KernelSnap(), DeepEquals, &asserts.ModelSnap{ + Name: "baz-linux", + SnapType: "kernel", + Modes: []string{"run"}, + Track: "18", + Presence: "required", + }) c.Check(model.Kernel(), Equals, "baz-linux") c.Check(model.KernelTrack(), Equals, "18") } @@ -285,6 +391,13 @@ func (mods *modelSuite) TestDecodeGadgetTrack(c *C) { a, err := asserts.Decode([]byte(encoded)) c.Assert(err, IsNil) model := a.(*asserts.Model) + c.Check(model.GadgetSnap(), DeepEquals, &asserts.ModelSnap{ + Name: "brand-gadget", + SnapType: "gadget", + Modes: []string{"run"}, + Track: "18", + Presence: "required", + }) c.Check(model.Gadget(), Equals, "brand-gadget") c.Check(model.GadgetTrack(), Equals, "18") } @@ -388,11 +501,45 @@ func (mods *modelSuite) TestClassicDecodeOK(c *C) { c.Check(model.DisplayName(), Equals, "Baz 3000") c.Check(model.Classic(), Equals, true) c.Check(model.Architecture(), Equals, "amd64") + c.Check(model.GadgetSnap(), DeepEquals, &asserts.ModelSnap{ + Name: "brand-gadget", + SnapType: "gadget", + Modes: []string{"run"}, + DefaultChannel: "stable", + Presence: "required", + }) c.Check(model.Gadget(), Equals, "brand-gadget") + c.Check(model.KernelSnap(), IsNil) c.Check(model.Kernel(), Equals, "") c.Check(model.KernelTrack(), Equals, "") + c.Check(model.Base(), Equals, "") + c.Check(model.BaseSnap(), IsNil) c.Check(model.Store(), Equals, "brand-store") - c.Check(model.RequiredSnaps(), DeepEquals, []string{"foo", "bar"}) + allSnaps := model.AllSnaps() + c.Check(allSnaps, DeepEquals, []*asserts.ModelSnap{ + model.GadgetSnap(), + { + Name: "foo", + Modes: []string{"run"}, + DefaultChannel: "stable", + Presence: "required", + }, + { + Name: "bar", + Modes: []string{"run"}, + DefaultChannel: "stable", + Presence: "required", + }, + }) + // gadget included + reqSnaps := model.AllRequiredSnaps() + c.Check(reqSnaps, HasLen, len(allSnaps)) + for i, r := range reqSnaps { + c.Check(r.SnapName(), Equals, allSnaps[i].Name) + c.Check(r.ID(), Equals, "") + } + // gadget excluded + c.Check(model.RequiredSnaps(), DeepEquals, reqSnaps[1:]) } func (mods *modelSuite) TestClassicDecodeInvalid(c *C) { @@ -424,5 +571,161 @@ func (mods *modelSuite) TestClassicDecodeGadgetAndArchOptional(c *C) { model := a.(*asserts.Model) c.Check(model.Classic(), Equals, true) c.Check(model.Architecture(), Equals, "") + c.Check(model.GadgetSnap(), IsNil) c.Check(model.Gadget(), Equals, "") + c.Check(model.GadgetTrack(), Equals, "") +} + +func (mods *modelSuite) TestCore20DecodeOK(c *C) { + encoded := strings.Replace(core20ModelExample, "TSLINE", mods.tsLine, 1) + encoded = strings.Replace(encoded, "OTHER", "", 1) + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.ModelType) + model := a.(*asserts.Model) + c.Check(model.AuthorityID(), Equals, "brand-id1") + c.Check(model.Timestamp(), Equals, mods.ts) + c.Check(model.Series(), Equals, "16") + c.Check(model.BrandID(), Equals, "brand-id1") + c.Check(model.Model(), Equals, "baz-3000") + c.Check(model.DisplayName(), Equals, "Baz 3000") + c.Check(model.Architecture(), Equals, "amd64") + c.Check(model.GadgetSnap(), DeepEquals, &asserts.ModelSnap{ + Name: "brand-gadget", + SnapID: "brandgadgetdidididididididididid", + SnapType: "gadget", + Modes: []string{"run", "ephemeral"}, + DefaultChannel: "stable", + Presence: "required", + }) + c.Check(model.Gadget(), Equals, "brand-gadget") + c.Check(model.GadgetTrack(), Equals, "") + c.Check(model.KernelSnap(), DeepEquals, &asserts.ModelSnap{ + Name: "baz-linux", + SnapID: "bazlinuxidididididididididididid", + SnapType: "kernel", + Modes: []string{"run", "ephemeral"}, + Track: "20", + Presence: "required", + }) + c.Check(model.Kernel(), Equals, "baz-linux") + c.Check(model.KernelTrack(), Equals, "20") + c.Check(model.Base(), Equals, "core20") + c.Check(model.BaseSnap(), DeepEquals, &asserts.ModelSnap{ + Name: "core20", + SnapType: "base", + Modes: []string{"run", "ephemeral"}, + DefaultChannel: "stable", + Presence: "required", + }) + c.Check(model.Store(), Equals, "brand-store") + allSnaps := model.AllSnaps() + c.Check(allSnaps, DeepEquals, []*asserts.ModelSnap{ + model.BaseSnap(), + model.GadgetSnap(), + model.KernelSnap(), + { + Name: "other-base", + SnapID: "otherbasedididididididididididid", + SnapType: "base", + Modes: []string{"run"}, + DefaultChannel: "stable", + Presence: "required", + }, + { + Name: "nm", + SnapID: "nmididididididididididididididid", + SnapType: "app", + Modes: []string{"ephemeral", "run"}, + DefaultChannel: "1.0", + Presence: "required", + }, + { + Name: "myapp", + SnapID: "myappdididididididididididididid", + SnapType: "app", + Modes: []string{"run"}, + DefaultChannel: "2.0", + Presence: "required", + }, + { + Name: "myappopt", + SnapID: "myappoptidididididididididididid", + SnapType: "app", + Modes: []string{"run"}, + DefaultChannel: "stable", + Presence: "optional", + }, + }) + // boot snaps included + reqSnaps := model.AllRequiredSnaps() + c.Check(reqSnaps, HasLen, len(allSnaps)-1) + for i, r := range reqSnaps { + c.Check(r.SnapName(), Equals, allSnaps[i].Name) + c.Check(r.ID(), Equals, allSnaps[i].SnapID) + } + // boot snaps excluded + c.Check(model.RequiredSnaps(), DeepEquals, reqSnaps[3:]) + c.Check(model.SystemUserAuthority(), HasLen, 0) +} + +func (mods *modelSuite) TestCore20ExplictBootBase(c *C) { + encoded := strings.Replace(core20ModelExample, "TSLINE", mods.tsLine, 1) + encoded = strings.Replace(encoded, "OTHER", ` - + name: core20 + id: core20ididididididididididididid + type: base + default-channel: candidate +`, 1) + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.ModelType) + // model := a.(*asserts.Model) +} + +func (mods *modelSuite) TestCore20DecodeInvalid(c *C) { + encoded := strings.Replace(core20ModelExample, "TSLINE", mods.tsLine, 1) + + snapsStanza := encoded[strings.Index(encoded, "snaps:"):strings.Index(encoded, "grade:")] + + invalidTests := []struct{ original, invalid, expectedErr string }{ + {"base: core20\n", "", `"base" header is mandatory`}, + {"OTHER", "classic: true\n", `cannot use extended snaps header for a classic model \(yet\)`}, + {snapsStanza, "snaps: snap\n", `"snaps" header must be a list of maps`}, + {snapsStanza, "snaps:\n - snap\n", `"snaps" header must be a list of maps`}, + {"name: myapp\n", "other: 1\n", `"name" of snap is mandatory`}, + {"name: myapp\n", "name: myapp_2\n", `invalid snap name "myapp_2"`}, + {"id: myappdididididididididididididid\n", "id: 2\n", `"id" of snap "myapp" contains invalid characters: "2"`}, + {"type: gadget\n", "type:\n - g\n", `"type" of snap "brand-gadget" must be a string`}, + {"type: app\n", "type: thing\n", `"type" of snap "myappopt" must be one of must be one of app|base|gadget|kernel|core`}, + {"modes:\n - run\n", "modes: run\n", `"modes" of snap "other-base" must be a list of strings`}, + {"track: 20\n", "track:\n - x\n", `"track" of snap "baz-linux" must be a string`}, + {"track: 20\n", "track: 20/edge\n", `invalid locked track for snap \"baz-linux\": 20/edge`}, + {"track: 20\n", "track: 20////\n", `invalid locked track for snap \"baz-linux\": 20////`}, + {"default-channel: 2.0\n", "default-channel:\n - x\n", `"default-channel" of snap "myapp" must be a string`}, + {"default-channel: 2.0\n", "default-channel: 2.0/xyz/z\n", `invalid default channel for snap "myapp": invalid risk in channel name: 2.0/xyz/z`}, + {"track: 20\n", "track: 20\n default-channel: 20/foo\n", `snap "baz-linux" cannot specify both default channel and locked track`}, + {"presence: optional\n", "presence:\n - opt\n", `"presence" of snap "myappopt" must be a string`}, + {"presence: optional\n", "presence: no\n", `"presence" of snap "myappopt" must be one of must be one of required|optional`}, + {"OTHER", " -\n name: myapp\n id: myappdididididididididididididid\n", `cannot list the same snap "myapp" multiple times`}, + {"OTHER", " -\n name: myapp2\n id: myappdididididididididididididid\n", `cannot specify the same snap id "myappdididididididididididididid" multiple times, specified for snaps "myapp" and "myapp2"`}, + {"OTHER", " -\n name: kernel2\n id: kernel2didididididididididididid\n type: kernel\n", `cannot specify multiple kernel snaps: "baz-linux" and "kernel2"`}, + {"OTHER", " -\n name: gadget2\n id: gadget2didididididididididididid\n type: gadget\n", `cannot specify multiple gadget snaps: "brand-gadget" and "gadget2"`}, + {"type: gadget\n", "type: gadget\n presence: required\n", `boot snaps are always available, cannot specify modes or presence for snap "brand-gadget"`}, + {"type: gadget\n", "type: gadget\n modes:\n - run\n", `boot snaps are always available, cannot specify modes or presence for snap "brand-gadget"`}, + {"type: kernel\n", "type: kernel\n presence: required\n", `boot snaps are always available, cannot specify modes or presence for snap "baz-linux"`}, + {"OTHER", " -\n name: core20\n id: core20ididididididididididididid\n type: base\n presence: optional\n", `boot snaps are always available, cannot specify modes or presence for snap "core20"`}, + {"type: gadget\n", "type: app\n", `one "snaps" header entry must specify the model gadget`}, + {"type: kernel\n", "type: app\n", `one "snaps" header entry must specify the model kernel`}, + {"OTHER", " -\n name: core20\n id: core20ididididididididididididid\n type: app\n", `boot base "core20" must specify type "base", not "app"`}, + {"OTHER", "kernel: foo\n", `cannot specify separate "kernel" header once using the extended snaps header`}, + {"OTHER", "gadget: foo\n", `cannot specify separate "gadget" header once using the extended snaps header`}, + {"OTHER", "required-snaps:\n - foo\n", `cannot specify separate "required-snaps" header once using the extended snaps header`}, + } + for _, test := range invalidTests { + invalid := strings.Replace(encoded, test.original, test.invalid, 1) + invalid = strings.Replace(invalid, "OTHER", "", 1) + _, err := asserts.Decode([]byte(invalid)) + c.Check(err, ErrorMatches, modelErrPrefix+test.expectedErr) + } } diff --git a/image/image.go b/image/image.go index 0bb50b99e03..9eedd1de1e4 100644 --- a/image/image.go +++ b/image/image.go @@ -473,7 +473,13 @@ func setupSeed(tsto *ToolingStore, model *asserts.Model, opts *Options, local *l } } - basesAndApps = append(basesAndApps, model.RequiredSnaps()...) + // TODO|XXX: later use the refs directly and simplify + reqSnaps := make([]string, 0, len(model.RequiredSnaps())) + for _, reqRef := range model.RequiredSnaps() { + reqSnaps = append(reqSnaps, reqRef.SnapName()) + } + + basesAndApps = append(basesAndApps, reqSnaps...) basesAndApps = append(basesAndApps, opts.Snaps...) // TODO: required snaps should get their base from required // snaps (mentioned in the model); additional snaps could @@ -513,7 +519,7 @@ func setupSeed(tsto *ToolingStore, model *asserts.Model, opts *Options, local *l } // then required and the user requested stuff - snaps = append(snaps, model.RequiredSnaps()...) + snaps = append(snaps, reqSnaps...) snaps = append(snaps, opts.Snaps...) for _, snapName := range snaps { diff --git a/overlord/devicestate/devicestate.go b/overlord/devicestate/devicestate.go index d889e67d310..ebf10e7dc7c 100644 --- a/overlord/devicestate/devicestate.go +++ b/overlord/devicestate/devicestate.go @@ -39,6 +39,7 @@ import ( "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/release" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/naming" ) var ( @@ -289,26 +290,9 @@ func CanManageRefreshes(st *state.State) bool { return false } -func getAllRequiredSnapsForModel(model *asserts.Model) map[string]bool { - reqSnaps := model.RequiredSnaps() - // +4 for (snapd, base, gadget, kernel) - required := make(map[string]bool, len(reqSnaps)+4) - for _, snap := range reqSnaps { - required[snap] = true - } - if model.Base() != "" { - required["snapd"] = true - required[model.Base()] = true - } else { - required["core"] = true - } - if model.Kernel() != "" { - required[model.Kernel()] = true - } - if model.Gadget() != "" { - required[model.Gadget()] = true - } - return required +func getAllRequiredSnapsForModel(model *asserts.Model) *naming.SnapSet { + reqSnaps := model.AllRequiredSnaps() + return naming.NewSnapSet(reqSnaps) } // extractDownloadInstallEdgesFromTs extracts the first, last download @@ -348,11 +332,13 @@ func remodelTasks(ctx context.Context, st *state.State, current, new *asserts.Mo } // add new required-snaps, no longer required snaps will be cleaned // in "set-model" - for _, snapName := range new.RequiredSnaps() { - _, err := snapstate.CurrentInfo(st, snapName) + for _, snapRef := range new.RequiredSnaps() { + // TODO|XXX: have methods that take refs directly + // to respect the snap ids + _, err := snapstate.CurrentInfo(st, snapRef.SnapName()) // If the snap is not installed we need to install it now. if _, ok := err.(*snap.NotInstalledError); ok { - ts, err := snapstateInstallWithDeviceContext(ctx, st, snapName, nil, userID, snapstate.Flags{Required: true}, deviceCtx, fromChange) + ts, err := snapstateInstallWithDeviceContext(ctx, st, snapRef.SnapName(), nil, userID, snapstate.Flags{Required: true}, deviceCtx, fromChange) if err != nil { return nil, err } diff --git a/overlord/devicestate/firstboot.go b/overlord/devicestate/firstboot.go index e63525f4cd8..5f9cf623901 100644 --- a/overlord/devicestate/firstboot.go +++ b/overlord/devicestate/firstboot.go @@ -38,6 +38,7 @@ import ( "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/release" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/naming" "github.com/snapcore/snapd/timings" ) @@ -242,7 +243,8 @@ func populateStateFromSeedImpl(st *state.State, tm timings.Measurer) ([]*state.T } var flags snapstate.Flags - if required[sn.Name] { + // TODO|XXX: turn SeedSnap into a SnapRef + if required.Contains(naming.Snap(sn.Name)) { flags.Required = true } diff --git a/overlord/devicestate/handlers.go b/overlord/devicestate/handlers.go index 8376ef8d9cc..237aa5de71a 100644 --- a/overlord/devicestate/handlers.go +++ b/overlord/devicestate/handlers.go @@ -49,6 +49,7 @@ import ( "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/release" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/naming" "github.com/snapcore/snapd/timings" ) @@ -92,6 +93,7 @@ func (m *DeviceManager) doSetModel(t *state.Task, _ *tomb.Tomb) error { // unmark no-longer required snaps requiredSnaps := getAllRequiredSnapsForModel(new) + // TODO:XXX: have AllByRef snapStates, err := snapstate.All(st) if err != nil { return err @@ -108,7 +110,7 @@ func (m *DeviceManager) doSetModel(t *state.Task, _ *tomb.Tomb) error { continue } // clean required flag if no-longer needed - if snapst.Flags.Required && !requiredSnaps[snapName] { + if snapst.Flags.Required && !requiredSnaps.Contains(naming.Snap(snapName)) { snapst.Flags.Required = false snapstate.Set(st, snapName, snapst) }