diff --git a/image/image_test.go b/image/image_test.go index cb823102fbb..5330b2cdcb0 100644 --- a/image/image_test.go +++ b/image/image_test.go @@ -53,6 +53,7 @@ import ( "github.com/snapcore/snapd/seed/seedtest" "github.com/snapcore/snapd/seed/seedwriter" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/naming" "github.com/snapcore/snapd/snap/snaptest" "github.com/snapcore/snapd/store" "github.com/snapcore/snapd/store/tooling" @@ -3343,11 +3344,20 @@ func (s *imageSuite) testSetupSeedCore20Grub(c *C, kernelContent [][]string, exp Channel: channel, }) } + // comp2 is optional in our model so it has not been included + // as it was not in the options either + cref1 := naming.NewComponentRef("required20", "comp1") c.Check(runSnaps[0], DeepEquals, &seed.Snap{ Path: filepath.Join(seedsnapsdir, "required20_21.snap"), SideInfo: &s.AssertedSnapInfo("required20").SideInfo, Required: true, Channel: stableChannel, + Components: []seed.Component{ + { + Path: filepath.Join(seedsnapsdir, "required20+comp1_22.comp"), + CompSideInfo: *snap.NewComponentSideInfo(cref1, snap.R(22)), + }, + }, }) c.Check(runSnaps[0].Path, testutil.FilePresent) @@ -4072,11 +4082,23 @@ func (s *imageSuite) TestSetupSeedSnapRevisionsDownloadHappy(c *C) { Channel: channel, }) } + cref1 := naming.NewComponentRef("required20", "comp1") + cref2 := naming.NewComponentRef("required20", "comp2") c.Check(runSnaps[0], DeepEquals, &seed.Snap{ Path: filepath.Join(seedsnapsdir, "required20_59.snap"), SideInfo: &s.AssertedSnapInfo("required20").SideInfo, Required: true, Channel: stableChannel, + Components: []seed.Component{ + { + Path: filepath.Join(seedsnapsdir, "required20+comp1_22.comp"), + CompSideInfo: *snap.NewComponentSideInfo(cref1, snap.R(22)), + }, + { + Path: filepath.Join(seedsnapsdir, "required20+comp2_33.comp"), + CompSideInfo: *snap.NewComponentSideInfo(cref2, snap.R(33)), + }, + }, }) c.Check(runSnaps[0].Path, testutil.FilePresent) @@ -4421,6 +4443,8 @@ func (s *imageSuite) TestLocalSnapWithCompsRevisionMatchingStoreRevision(c *C) { Channel: essChannel[i], }) } + cref1 := naming.NewComponentRef("required20", "comp1") + cref2 := naming.NewComponentRef("required20", "comp2") c.Check(runSnaps[0], DeepEquals, &seed.Snap{ Path: filepath.Join(seedsnapsdir, "required20_21.snap"), Required: true, @@ -4430,6 +4454,16 @@ func (s *imageSuite) TestLocalSnapWithCompsRevisionMatchingStoreRevision(c *C) { Revision: snap.R(21), }, Channel: "latest/stable", + Components: []seed.Component{ + { + Path: filepath.Join(seedsnapsdir, "required20+comp1_22.comp"), + CompSideInfo: *snap.NewComponentSideInfo(cref1, snap.R(22)), + }, + { + Path: filepath.Join(seedsnapsdir, "required20+comp2_33.comp"), + CompSideInfo: *snap.NewComponentSideInfo(cref2, snap.R(33)), + }, + }, }) c.Check(runSnaps[0].Path, testutil.FilePresent) // Check components exist @@ -5138,17 +5172,27 @@ func (s *imageSuite) TestSetupSeedLocalComponents(c *C) { } expectedLabel := image.MakeLabel(time.Now()) extraSnapsDir := filepath.Join(seeddir, "systems", expectedLabel, "snaps") + cref1 := naming.NewComponentRef("required20", "comp1") + cref2 := naming.NewComponentRef("required20", "comp2") c.Check(runSnaps[0], DeepEquals, &seed.Snap{ Path: filepath.Join(extraSnapsDir, "required20_1.0.snap"), SideInfo: &snap.SideInfo{ RealName: "required20", }, Required: true, + Components: []seed.Component{ + { + Path: filepath.Join(extraSnapsDir, "required20+comp1_1.0.comp"), + CompSideInfo: *snap.NewComponentSideInfo(cref1, snap.R(0)), + }, + { + Path: filepath.Join(extraSnapsDir, "required20+comp2_2.0.comp"), + CompSideInfo: *snap.NewComponentSideInfo(cref2, snap.R(0)), + }, + }, }) c.Check(runSnaps[0].Path, testutil.FilePresent) - // TODO these files will be loaded when opening the seed, but that is - // not implemented yet c.Check(osutil.FileExists(filepath.Join(extraSnapsDir, "required20+comp1_1.0.comp")), Equals, true) c.Check(osutil.FileExists(filepath.Join(extraSnapsDir, "required20+comp2_2.0.comp")), diff --git a/overlord/managers_test.go b/overlord/managers_test.go index 130e6209634..3ec083a0ec5 100644 --- a/overlord/managers_test.go +++ b/overlord/managers_test.go @@ -7483,7 +7483,7 @@ func (s *mgrsSuiteCore) testRemodelUC20WithRecoverySystem(c *C, encrypted bool) {Path: coreInfo.MountFile()}, {Path: snapdInfo.MountFile()}, {Path: bazInfo.MountFile()}, - }) + }, nil) // create a new model newModel := s.brands.Model("can0nical", "my-model", uc20ModelDefaults, map[string]interface{}{ @@ -7876,7 +7876,7 @@ func (s *mgrsSuiteCore) testRemodelUC20WithRecoverySystemSimpleSetUp(c *C, model {Path: pcKernelInfo.MountFile()}, {Path: coreInfo.MountFile()}, {Path: snapdInfo.MountFile()}, - }) + }, nil) // mock the modeenv file m := &boot.Modeenv{ diff --git a/seed/helpers.go b/seed/helpers.go index f65228b5f0d..ed5aed4bfda 100644 --- a/seed/helpers.go +++ b/seed/helpers.go @@ -104,6 +104,14 @@ func readInfo(snapPath string, si *snap.SideInfo) (*snap.Info, error) { return snap.ReadInfoFromSnapFile(snapf, si) } +func readComponentInfo(compPath string, info *snap.Info, csi *snap.ComponentSideInfo) (*snap.ComponentInfo, error) { + compf, err := snapfile.Open(compPath) + if err != nil { + return nil, err + } + return snap.ReadComponentInfoFromContainer(compf, info, csi) +} + func snapTypeFromModel(modSnap *asserts.ModelSnap) snap.Type { switch modSnap.SnapType { case "base": diff --git a/seed/internal/options20.go b/seed/internal/options20.go index 886a41555ce..1d23cd70b75 100644 --- a/seed/internal/options20.go +++ b/seed/internal/options20.go @@ -31,8 +31,8 @@ import ( "github.com/snapcore/snapd/snap/naming" ) -// Component contains the options for components for grade: dangerous. -type Component struct { +// Component20 contains the options for components for grade: dangerous. +type Component20 struct { // Name is the component name Name string `yaml:"name"` // Unasserted has the filename for an unasserted local component @@ -56,7 +56,7 @@ type Snap20 struct { // Components is a list of component options. It is only valid to add a // list of unasserted local components when we are using an unasserted // local snap. - Components []Component `yaml:"components,omitempty"` + Components []Component20 `yaml:"components,omitempty"` } // SnapName implements naming.SnapRef. @@ -114,17 +114,16 @@ func ReadOptions20(optionsFn string) (*Options20, error) { return nil, fmt.Errorf("%s: %q must be a filename, not a path", errPrefix, sn.Unasserted) } if len(sn.Components) > 0 { - if sn.Unasserted == "" { - return nil, fmt.Errorf("%s: local components specified for non-local snap %q", errPrefix, sn.Name) - } - for _, comp := range sn.Components { if err := naming.ValidateSnap(comp.Name); err != nil { return nil, fmt.Errorf("%s: %v", errPrefix, err) } - if comp.Unasserted == "" { + if comp.Unasserted == "" && sn.Unasserted != "" { return nil, fmt.Errorf("%s: no file specified for unasserted component %q", errPrefix, comp.Name) } + if comp.Unasserted != "" && sn.Unasserted == "" { + return nil, fmt.Errorf("%s: unasserted component specified for asserted snap %q", errPrefix, sn.Name) + } if strings.Contains(comp.Unasserted, "/") { return nil, fmt.Errorf("%s: %q must be a filename, not a path", errPrefix, comp.Unasserted) } diff --git a/seed/internal/options20_test.go b/seed/internal/options20_test.go index 072849986f4..9bd183ee349 100644 --- a/seed/internal/options20_test.go +++ b/seed/internal/options20_test.go @@ -196,7 +196,7 @@ snaps: c.Assert(options20.Snaps[0], DeepEquals, &internal.Snap20{ Name: "foo", Unasserted: "bar.snap", - Components: []internal.Component{ + Components: []internal.Component20{ {Name: "comp1", Unasserted: "comp1_1.comp"}, {Name: "comp2", Unasserted: "file.comp"}, }, @@ -217,7 +217,30 @@ snaps: c.Assert(err, IsNil) options20, err := internal.ReadOptions20(fn) c.Assert(options20, IsNil) - c.Assert(err, ErrorMatches, `cannot read grade dangerous options yaml: local components specified for non-local snap "foo"`) + c.Assert(err, ErrorMatches, `cannot read grade dangerous options yaml: unasserted component specified for asserted snap "foo"`) +} + +func (s *options20Suite) TestWithComponentsAssertedSnapAssertedComp(c *C) { + fn := filepath.Join(c.MkDir(), "options.yaml") + err := os.WriteFile(fn, []byte(` +snaps: + - name: foo + id: snapidsnapidsnapidsnapidsnapidsn + components: + - name: comp1 +`), 0644) + + c.Assert(err, IsNil) + options20, err := internal.ReadOptions20(fn) + c.Assert(err, IsNil) + c.Assert(options20.Snaps, HasLen, 1) + c.Assert(options20.Snaps[0], DeepEquals, &internal.Snap20{ + Name: "foo", + SnapID: "snapidsnapidsnapidsnapidsnapidsn", + Components: []internal.Component20{ + {Name: "comp1"}, + }, + }) } func (s *options20Suite) TestWithComponentsBadCompName(c *C) { diff --git a/seed/seed.go b/seed/seed.go index 9ca5d1ed599..9f8cb52a6c1 100644 --- a/seed/seed.go +++ b/seed/seed.go @@ -39,6 +39,12 @@ var ( open = Open ) +// Component holds the details of a component in a seed. +type Component struct { + Path string + CompSideInfo snap.ComponentSideInfo +} + // Snap holds the details of a snap in a seed. type Snap struct { Path string @@ -56,6 +62,9 @@ type Snap struct { Channel string DevMode bool Classic bool + + // Components for the snap + Components []Component } func (s *Snap) SnapName() string { diff --git a/seed/seed20.go b/seed/seed20.go index 0104c49fe38..0324833ce41 100644 --- a/seed/seed20.go +++ b/seed/seed20.go @@ -36,6 +36,7 @@ import ( "io/fs" "os" "path/filepath" + "sort" "strings" "sync" @@ -49,6 +50,14 @@ import ( "github.com/snapcore/snapd/timings" ) +// resourceKey is used in maps of resource assertions. +type resourceKey struct { + // snapID is the snap ID + snapID string + // name is the resource name + name string +} + type seed20 struct { systemDir string @@ -62,6 +71,9 @@ type seed20 struct { snapRevsByID map[string]*asserts.SnapRevision + resPairByResKey map[resourceKey]*asserts.SnapResourcePair + resRevByResKey map[resourceKey]*asserts.SnapResourceRevision + nLoadMetaJobs int optSnaps []*internal.Snap20 @@ -176,8 +188,8 @@ func (s *seed20) LoadAssertions(db asserts.RODatabase, commitTo func(*asserts.Ba assertsDir := filepath.Join(s.systemDir, "assertions") // collect assertions that are not the model - var declRefs []*asserts.Ref - var revRefs []*asserts.Ref + var declRefs, revRefs []*asserts.Ref + var resRevRefs, resPairRefs []*asserts.Ref checkAssertion := func(ref *asserts.Ref) error { switch ref.Type { case asserts.ModelType: @@ -186,6 +198,10 @@ func (s *seed20) LoadAssertions(db asserts.RODatabase, commitTo func(*asserts.Ba declRefs = append(declRefs, ref) case asserts.SnapRevisionType: revRefs = append(revRefs, ref) + case asserts.SnapResourceRevisionType: + resRevRefs = append(resRevRefs, ref) + case asserts.SnapResourcePairType: + resPairRefs = append(resPairRefs, ref) } return nil } @@ -207,6 +223,9 @@ func (s *seed20) LoadAssertions(db asserts.RODatabase, commitTo func(*asserts.Ba if len(declRefs) != len(revRefs) { return fmt.Errorf("system unexpectedly holds a different number of snap-declaration than snap-revision assertions") } + if len(resRevRefs) != len(resPairRefs) { + return fmt.Errorf("system unexpectedly holds a different number of snap-snap-resource-revision than snap-resource-pair assertions") + } // this also verifies the consistency of all of them if err := commitTo(batch); err != nil { @@ -236,10 +255,10 @@ func (s *seed20) LoadAssertions(db asserts.RODatabase, commitTo func(*asserts.Ba return err } snapDecl := a.(*asserts.SnapDeclaration) - snapDeclsByID[snapDecl.SnapID()] = snapDecl if snapDecl1 := snapDeclsByName[snapDecl.SnapName()]; snapDecl1 != nil { return fmt.Errorf("cannot have multiple snap-declarations for the same snap-name: %s", snapDecl.SnapName()) } + snapDeclsByID[snapDecl.SnapID()] = snapDecl snapDeclsByName[snapDecl.SnapName()] = snapDecl } @@ -261,6 +280,59 @@ func (s *seed20) LoadAssertions(db asserts.RODatabase, commitTo func(*asserts.Ba } } + s.resRevByResKey = make(map[resourceKey]*asserts.SnapResourceRevision, len(resRevRefs)) + for _, resRevRef := range resRevRefs { + a, err := find(resRevRef) + if err != nil { + return err + } + resRev := a.(*asserts.SnapResourceRevision) + snapID := resRev.SnapID() + if _, ok := snapDeclsByID[snapID]; !ok { + // Unidentified IDs are checked previously + return fmt.Errorf("internal error: snap ID %s in resource revision assertion for %s not in known snap declarations", snapID, resRev.ResourceName()) + } + resKey := resourceKey{snapID: snapID, name: resRev.ResourceName()} + if _, ok := s.resRevByResKey[resKey]; ok { + return fmt.Errorf("cannot have multiple resource revisions for the same component %s (snap %s)", resRev.ResourceName(), snapID) + } + s.resRevByResKey[resKey] = resRev + } + + s.resPairByResKey = make(map[resourceKey]*asserts.SnapResourcePair, len(resPairRefs)) + for _, resPairRef := range resPairRefs { + a, err := find(resPairRef) + if err != nil { + return err + } + resPair := a.(*asserts.SnapResourcePair) + snapID := resPair.SnapID() + resKey := resourceKey{snapID: snapID, name: resPair.ResourceName()} + resRev, ok := s.resRevByResKey[resKey] + if !ok { + return fmt.Errorf("resource pair for %s (%s) does not have a matching resource revision", resPair.ResourceName(), resPair.SnapID()) + } + snapRev, ok := snapRevsByID[snapID] + if !ok { + // This should have been detected by previous checks + return fmt.Errorf("internal error, no snap revision for %s", + snapID) + } + // Check that we have matching snap-resource revisions as specified + // by the resource pair. + if resRev.ResourceRevision() != resPair.ResourceRevision() || + snapRev.SnapRevision() != resPair.SnapRevision() { + return fmt.Errorf("resource pair %s for %s does not match (snap revision, resource revision): (%d, %d)", + resPair.ResourceName(), snapRev.SnapID(), snapRev.SnapRevision(), resPair.ResourceRevision()) + } + + if _, ok := s.resPairByResKey[resKey]; ok { + // This should be detected in previous similar check for resource-revision + return fmt.Errorf("internal error: cannot have multiple resource pairs for the same component %s (snap %s)", resPair.ResourceName(), snapID) + } + s.resPairByResKey[resKey] = resPair + } + // remember db for later use s.db = db // remember commitTo for LoadPreseedAssertion @@ -353,6 +425,92 @@ func (e *noSnapDeclarationError) Error() string { return fmt.Sprintf("cannot find snap-declaration for snap name: %s", e.snapRef.SnapName()) } +type errorComponentNotInSeed struct { + error +} + +func (s *seed20) lookupVerifiedComponent(cref naming.ComponentRef, snapRev snap.Revision, snapID, snapProvenance string, snapsDir string, handler ContainerHandler, tm timings.Measurer) (Component, error) { + snapName := cref.SnapName + compName := cref.ComponentName + + resKey := resourceKey{snapID: snapID, name: compName} + resRev, ok := s.resRevByResKey[resKey] + if !ok { + // No assertions might be ok if the component is optional, the + // caller should check for this error type in that case. + return Component{}, errorComponentNotInSeed{ + fmt.Errorf("resource revision assertion not found for %s", compName)} + } + resPair, ok := s.resPairByResKey[resKey] + if !ok { + // should actually be catched by the previous check + return Component{}, + fmt.Errorf("internal error: resource pair assertion not found for %s", compName) + } + + compPath := filepath.Join(s.systemDir, snapsDir, + fmt.Sprintf("%s_%d.comp", cref.String(), resRev.ResourceRevision())) + _, err := os.Stat(compPath) + if err != nil { + // error should be of type *PathError + return Component{}, errorComponentNotInSeed{err} + } + + // Checks + + // Note that the check for matching revisions in resource-revision / + // resource-pair is already done in LoadAssertions + if resPair.SnapRevision() != snapRev.N { + return Component{}, fmt.Errorf( + "resource %s pair revision does not match snap revision: %d != %d", + compName, resPair.SnapRevision(), snapRev.N) + } + + if resRev.Provenance() != snapProvenance { + return Component{}, fmt.Errorf( + "resource revision provenance for %s does not match snap provenance: %s != %s", + compName, resRev.Provenance(), snapProvenance) + } + if resPair.Provenance() != snapProvenance { + return Component{}, fmt.Errorf( + "resource pair provenance for %s does not match snap provenance: %s != %s", + compName, resPair.Provenance(), snapProvenance) + } + + cpi := snap.MinimalComponentContainerPlaceInfo(compName, snap.R(resRev.Revision()), snapName) + newPath, snapSHA3_384, resSize, err := handler.HandleAndDigestAssertedContainer( + cpi, compPath, tm) + if err != nil { + return Component{}, err + } + if newPath != "" { + compPath = newPath + } + if resRev.ResourceSize() != resSize { + return Component{}, fmt.Errorf( + "resource %s size does not match size in resource revision: %d != %d", + compName, resSize, resRev.ResourceSize()) + } + if snapSHA3_384 != resRev.ResourceSHA3_384() { + return Component{}, fmt.Errorf( + "cannot validate resource %s, hash mismatch with snap-resource-revision", + compName) + } + + if err := snapasserts.CheckComponentProvenanceWithVerifiedRevision(compPath, resRev); err != nil { + return Component{}, err + } + + csi := snap.ComponentSideInfo{ + Component: cref, + Revision: snap.R(resRev.ResourceRevision()), + } + return Component{ + Path: compPath, + CompSideInfo: csi, + }, nil +} + func (s *seed20) lookupVerifiedRevision(snapRef naming.SnapRef, handler ContainerHandler, snapsDir string, tm timings.Measurer) (snapPath string, snapRev *asserts.SnapRevision, snapDecl *asserts.SnapDeclaration, err error) { snapID := snapRef.ID() if snapID != "" { @@ -397,7 +555,6 @@ func (s *seed20) lookupVerifiedRevision(snapRef naming.SnapRef, handler Containe if snapSHA3_384 != snapRev.SnapSHA3_384() { return "", nil, nil, fmt.Errorf("cannot validate %q for snap %q (snap-id %q), hash mismatch with snap-revision", snapPath, snapName, snapID) - } if newPath != "" { @@ -418,19 +575,106 @@ func (s *seed20) lookupVerifiedRevision(snapRef naming.SnapRef, handler Containe return snapPath, snapRev, snapDecl, nil } -func (s *seed20) lookupSnap(snapRef naming.SnapRef, optSnap *internal.Snap20, channel string, handler ContainerHandler, snapsDir string, tm timings.Measurer) (*Snap, error) { +func (s *seed20) lookupUnassertedComponent(comp20 internal.Component20, info *snap.Info, handler ContainerHandler, tm timings.Measurer) (Component, error) { + compPath := filepath.Join(s.systemDir, "snaps", comp20.Unasserted) + cinfo, err := readComponentInfo(compPath, info, nil) + if err != nil { + return Component{}, fmt.Errorf("cannot read unasserted component: %v", err) + } + compName := cinfo.Component.ComponentName + cref := naming.NewComponentRef(info.SnapName(), compName) + csi := snap.NewComponentSideInfo(cref, snap.R(0)) + cpi := snap.MinimalComponentContainerPlaceInfo( + compName, snap.R(-1), info.SnapName()) + newCompPath, err := handler.HandleUnassertedContainer(cpi, compPath, tm) + if err != nil { + return Component{}, err + } + if newCompPath != "" { + compPath = newCompPath + } + return Component{ + Path: compPath, + CompSideInfo: *csi, + }, nil +} + +func (s *seed20) deriveSideInfo(snapRef naming.SnapRef, modelSnap *asserts.ModelSnap, optSnap *internal.Snap20, handler ContainerHandler, snapsDir string, tm timings.Measurer) (snapPath string, sideInfo *snap.SideInfo, seedComps []Component, err error) { + var snapRev *asserts.SnapRevision + var snapDecl *asserts.SnapDeclaration + snapPath, snapRev, snapDecl, err = s.lookupVerifiedRevision(snapRef, handler, snapsDir, tm) + if err != nil { + return "", nil, nil, err + } + sideInfo = snapasserts.SideInfoFromSnapAssertions(snapDecl, snapRev) + + if modelSnap != nil { + seedComps = make([]Component, 0, len(modelSnap.Components)) + for comp, modelComp := range modelSnap.Components { + seedComp, err := s.lookupVerifiedComponent( + naming.NewComponentRef(snapDecl.SnapName(), comp), + snap.R(snapRev.SnapRevision()), snapDecl.SnapID(), + snapRev.Provenance(), snapsDir, handler, tm) + if err != nil { + var notInSeed errorComponentNotInSeed + if errors.As(err, ¬InSeed) { + // component not in seed + if modelComp.Presence == "required" { + err = fmt.Errorf("component %s required in the model but is not in the seed: %v", comp, err) + return "", nil, nil, err + } + // ignore if optional and not in seed + continue + } + return "", nil, nil, err + } + seedComps = append(seedComps, seedComp) + } + // Order for test reproducibility + sort.Slice(seedComps, func(i, j int) bool { + return seedComps[i].CompSideInfo.Component.ComponentName < + seedComps[j].CompSideInfo.Component.ComponentName + }) + } else { + // Asserted option snap + for _, comp := range optSnap.Components { + seedComp, err := s.lookupVerifiedComponent( + naming.NewComponentRef(snapDecl.SnapName(), comp.Name), + snap.R(snapRev.SnapRevision()), snapDecl.SnapID(), + snapRev.Provenance(), snapsDir, handler, tm) + if err != nil { + return "", nil, nil, err + } + seedComps = append(seedComps, seedComp) + } + } + + return snapPath, sideInfo, seedComps, nil +} + +func (s *seed20) lookupSnap(snapRef naming.SnapRef, modelSnap *asserts.ModelSnap, optSnap *internal.Snap20, channel string, handler ContainerHandler, snapsDir string, tm timings.Measurer) (*Snap, error) { if optSnap != nil && optSnap.Channel != "" { channel = optSnap.Channel } var path string var sideInfo *snap.SideInfo + var seedComps []Component if optSnap != nil && optSnap.Unasserted != "" { path = filepath.Join(s.systemDir, "snaps", optSnap.Unasserted) info, err := readInfo(path, nil) if err != nil { return nil, fmt.Errorf("cannot read unasserted snap: %v", err) } + // Read unasserted components + seedComps = make([]Component, 0, len(optSnap.Components)) + for _, comp20 := range optSnap.Components { + comp, err := s.lookupUnassertedComponent(comp20, info, handler, tm) + if err != nil { + return nil, err + } + seedComps = append(seedComps, comp) + } pinfo := snap.MinimalSnapContainerPlaceInfo(info.SnapName(), snap.Revision{N: -1}) newPath, err := handler.HandleUnassertedContainer(pinfo, path, tm) @@ -446,12 +690,8 @@ func (s *seed20) lookupSnap(snapRef naming.SnapRef, optSnap *internal.Snap20, ch } else { var err error timings.Run(tm, "derive-side-info", fmt.Sprintf("hash and derive side info for snap %q", snapRef.SnapName()), func(nested timings.Measurer) { - var snapRev *asserts.SnapRevision - var snapDecl *asserts.SnapDeclaration - path, snapRev, snapDecl, err = s.lookupVerifiedRevision(snapRef, handler, snapsDir, tm) - if err == nil { - sideInfo = snapasserts.SideInfoFromSnapAssertions(snapDecl, snapRev) - } + path, sideInfo, seedComps, err = s.deriveSideInfo( + snapRef, modelSnap, optSnap, handler, snapsDir, tm) }) if err != nil { return nil, err @@ -466,12 +706,16 @@ func (s *seed20) lookupSnap(snapRef naming.SnapRef, optSnap *internal.Snap20, ch sideInfo.LegacyEditedContact = auxInfo.Contact } + // TODO this is to avoid changing tests, fix tests instead + var comps []Component + if len(seedComps) > 0 { + comps = seedComps + } return &Snap{ - Path: path, - - SideInfo: sideInfo, - - Channel: channel, + Path: path, + SideInfo: sideInfo, + Channel: channel, + Components: comps, }, nil } @@ -510,7 +754,7 @@ func (s *seed20) doLoadMetaOne(sntoc *snapToConsider, handler ContainerHandler, channel = "latest/stable" snapsDir = "snaps" } - seedSnap, err := s.lookupSnap(snapRef, sntoc.optSnap, channel, handler, snapsDir, tm) + seedSnap, err := s.lookupSnap(snapRef, sntoc.modelSnap, sntoc.optSnap, channel, handler, snapsDir, tm) if err != nil { if _, ok := err.(*noSnapDeclarationError); ok && !required { // skipped optional snap is ok diff --git a/seed/seed20_test.go b/seed/seed20_test.go index 35ecbfc3281..2f913eebbab 100644 --- a/seed/seed20_test.go +++ b/seed/seed20_test.go @@ -39,6 +39,7 @@ import ( "github.com/snapcore/snapd/seed/seedtest" "github.com/snapcore/snapd/seed/seedwriter" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/naming" "github.com/snapcore/snapd/snap/snaptest" "github.com/snapcore/snapd/testutil" "github.com/snapcore/snapd/timings" @@ -331,13 +332,13 @@ func (s *seed20Suite) TestLoadAssertionsInvalidModelAssertFile(c *C) { c.Check(err, ErrorMatches, `system model assertion file must contain exactly the model assertion`) } -func (s *seed20Suite) massageAssertions(c *C, fn string, filter func(asserts.Assertion) asserts.Assertion) { +func (s *seed20Suite) massageAssertions(c *C, fn string, filter func(asserts.Assertion) []asserts.Assertion) { assertions := seedtest.ReadAssertions(c, fn) filtered := make([]asserts.Assertion, 0, len(assertions)) for _, a := range assertions { a1 := filter(a) if a1 != nil { - filtered = append(filtered, a1) + filtered = append(filtered, a1...) } } seedtest.WriteAssertions(fn, filtered...) @@ -347,11 +348,11 @@ func (s *seed20Suite) TestLoadAssertionsUnbalancedDeclsAndRevs(c *C) { sysLabel := "20191031" sysDir := s.makeCore20MinimalSeed(c, sysLabel) - s.massageAssertions(c, filepath.Join(sysDir, "assertions", "snaps"), func(a asserts.Assertion) asserts.Assertion { + s.massageAssertions(c, filepath.Join(sysDir, "assertions", "snaps"), func(a asserts.Assertion) []asserts.Assertion { if a.Type() == asserts.SnapRevisionType && a.HeaderString("snap-id") == s.AssertedSnapID("core20") { return nil } - return a + return []asserts.Assertion{a} }) seed20, err := seed.Open(s.SeedDir, sysLabel) @@ -374,11 +375,11 @@ func (s *seed20Suite) TestLoadAssertionsMultiSnapRev(c *C) { }, nil, "") c.Assert(err, IsNil) - s.massageAssertions(c, filepath.Join(sysDir, "assertions", "snaps"), func(a asserts.Assertion) asserts.Assertion { + s.massageAssertions(c, filepath.Join(sysDir, "assertions", "snaps"), func(a asserts.Assertion) []asserts.Assertion { if a.Type() == asserts.SnapRevisionType && a.HeaderString("snap-id") == s.AssertedSnapID("snapd") { - return spuriousRev + return []asserts.Assertion{spuriousRev} } - return a + return []asserts.Assertion{a} }) seed20, err := seed.Open(s.SeedDir, sysLabel) @@ -410,14 +411,14 @@ func (s *seed20Suite) TestLoadAssertionsMultiSnapDecl(c *C) { }, nil, "") c.Assert(err, IsNil) - s.massageAssertions(c, filepath.Join(sysDir, "assertions", "snaps"), func(a asserts.Assertion) asserts.Assertion { + s.massageAssertions(c, filepath.Join(sysDir, "assertions", "snaps"), func(a asserts.Assertion) []asserts.Assertion { if a.Type() == asserts.SnapDeclarationType && a.HeaderString("snap-name") == "snapd" { - return spuriousDecl + return []asserts.Assertion{spuriousDecl} } if a.Type() == asserts.SnapRevisionType && a.HeaderString("snap-id") == s.AssertedSnapID("snapd") { - return spuriousRev + return []asserts.Assertion{spuriousRev} } - return a + return []asserts.Assertion{a} }) seed20, err := seed.Open(s.SeedDir, sysLabel) @@ -480,14 +481,14 @@ func (s *seed20Suite) TestLoadMetaMissingSnapDeclByName(c *C) { }, nil, "") c.Assert(err, IsNil) - s.massageAssertions(c, filepath.Join(sysDir, "assertions", "snaps"), func(a asserts.Assertion) asserts.Assertion { + s.massageAssertions(c, filepath.Join(sysDir, "assertions", "snaps"), func(a asserts.Assertion) []asserts.Assertion { if a.Type() == asserts.SnapDeclarationType && a.HeaderString("snap-name") == "core20" { - return wrongDecl + return []asserts.Assertion{wrongDecl} } if a.Type() == asserts.SnapRevisionType && a.HeaderString("snap-id") == s.AssertedSnapID("core20") { - return wrongRev + return []asserts.Assertion{wrongRev} } - return a + return []asserts.Assertion{a} }) seed20, err := seed.Open(s.SeedDir, sysLabel) @@ -523,14 +524,14 @@ func (s *seed20Suite) TestLoadMetaMissingSnapDeclByID(c *C) { }, nil, "") c.Assert(err, IsNil) - s.massageAssertions(c, filepath.Join(sysDir, "assertions", "snaps"), func(a asserts.Assertion) asserts.Assertion { + s.massageAssertions(c, filepath.Join(sysDir, "assertions", "snaps"), func(a asserts.Assertion) []asserts.Assertion { if a.Type() == asserts.SnapDeclarationType && a.HeaderString("snap-name") == "pc" { - return wrongDecl + return []asserts.Assertion{wrongDecl} } if a.Type() == asserts.SnapRevisionType && a.HeaderString("snap-id") == s.AssertedSnapID("pc") { - return wrongRev + return []asserts.Assertion{wrongRev} } - return a + return []asserts.Assertion{a} }) seed20, err := seed.Open(s.SeedDir, sysLabel) @@ -592,11 +593,11 @@ func (s *seed20Suite) TestLoadMetaWrongHashSnap(c *C) { }, nil, "") c.Assert(err, IsNil) - s.massageAssertions(c, filepath.Join(sysDir, "assertions", "snaps"), func(a asserts.Assertion) asserts.Assertion { + s.massageAssertions(c, filepath.Join(sysDir, "assertions", "snaps"), func(a asserts.Assertion) []asserts.Assertion { if a.Type() == asserts.SnapRevisionType && a.HeaderString("snap-id") == s.AssertedSnapID("pc") { - return wrongRev + return []asserts.Assertion{wrongRev} } - return a + return []asserts.Assertion{a} }) seed20, err := seed.Open(s.SeedDir, sysLabel) @@ -617,14 +618,14 @@ func (s *seed20Suite) TestLoadMetaWrongGadgetBase(c *C) { pc18Decl, pc18Rev := s.MakeAssertedSnap(c, snapYaml["pc=18"], nil, snap.R(2), "canonical") err := os.Rename(s.AssertedSnap("pc"), filepath.Join(s.SeedDir, "snaps", "pc_2.snap")) c.Assert(err, IsNil) - s.massageAssertions(c, filepath.Join(sysDir, "assertions", "snaps"), func(a asserts.Assertion) asserts.Assertion { + s.massageAssertions(c, filepath.Join(sysDir, "assertions", "snaps"), func(a asserts.Assertion) []asserts.Assertion { if a.Type() == asserts.SnapDeclarationType && a.HeaderString("snap-name") == "pc" { - return pc18Decl + return []asserts.Assertion{pc18Decl} } if a.Type() == asserts.SnapRevisionType && a.HeaderString("snap-id") == s.AssertedSnapID("pc") { - return pc18Rev + return []asserts.Assertion{pc18Rev} } - return a + return []asserts.Assertion{a} }) seed20, err := seed.Open(s.SeedDir, sysLabel) @@ -3015,11 +3016,11 @@ func (s *seed20Suite) TestLoadMetaWrongHashSnapParallelism2(c *C) { }, nil, "") c.Assert(err, IsNil) - s.massageAssertions(c, filepath.Join(sysDir, "assertions", "snaps"), func(a asserts.Assertion) asserts.Assertion { + s.massageAssertions(c, filepath.Join(sysDir, "assertions", "snaps"), func(a asserts.Assertion) []asserts.Assertion { if a.Type() == asserts.SnapRevisionType && a.HeaderString("snap-id") == s.AssertedSnapID("pc-kernel") { - return wrongRev + return []asserts.Assertion{wrongRev} } - return a + return []asserts.Assertion{a} }) seed20, err := seed.Open(s.SeedDir, sysLabel) @@ -3707,3 +3708,780 @@ func compareDirs(c *C, expected, got string) { c.Check(gotCount, Equals, expectedCount) } + +type seedOpts struct { + delegated bool +} + +func (s *seed20Suite) makeCore20SeedWithComps(c *C, sysLabel string, opts seedOpts) string { + s.makeSnap(c, "snapd", "") + s.makeSnap(c, "core20", "") + s.makeSnap(c, "pc-kernel=20", "") + s.makeSnap(c, "pc=20", "") + compRevs := map[string]snap.Revision{ + "comp1": snap.R(22), + "comp2": snap.R(33), + } + if opts.delegated { + ra := map[string]interface{}{ + "account-id": "my-brand", + "provenance": []interface{}{"delegated-prov", "other-prov"}, + } + s.MakeAssertedDelegatedSnapWithComps(c, + snapYaml["required20"]+"\nprovenance: delegated-prov\n", + nil, snap.R(1), compRevs, "developerid", "my-brand", + "delegated-prov", ra, s.StoreSigning.Database) + } else { + s.MakeAssertedSnapWithComps(c, seedtest.SampleSnapYaml["required20"], nil, + snap.R(11), compRevs, "canonical", s.StoreSigning.Database) + } + + s.MakeSeed(c, sysLabel, "my-brand", "my-model", map[string]interface{}{ + "display-name": "my model", + "architecture": "amd64", + "base": "core20", + "snaps": []interface{}{ + map[string]interface{}{ + "name": "pc-kernel", + "id": s.AssertedSnapID("pc-kernel"), + "type": "kernel", + "default-channel": "20", + }, + map[string]interface{}{ + "name": "pc", + "id": s.AssertedSnapID("pc"), + "type": "gadget", + "default-channel": "20", + }, + map[string]interface{}{ + "name": "required20", + "id": s.AssertedSnapID("required20"), + "type": "app", + "components": map[string]interface{}{ + "comp1": "required", + "comp2": "required", + }, + }, + }, + }, nil) + + return filepath.Join(s.SeedDir, "systems", sysLabel) +} + +func (s *seed20Suite) TestLoadMetaWithComponents(c *C) { + sysLabel := "20240805" + s.makeCore20SeedWithComps(c, sysLabel, seedOpts{delegated: false}) + + seed20, err := seed.Open(s.SeedDir, sysLabel) + c.Assert(err, IsNil) + + err = seed20.LoadAssertions(s.db, s.commitTo) + c.Assert(err, IsNil) + + err = seed20.LoadMeta(seed.AllModes, nil, s.perfTimings) + c.Assert(err, IsNil) + + c.Check(seed20.UsesSnapdSnap(), Equals, true) + + essSnaps := seed20.EssentialSnaps() + c.Check(essSnaps, HasLen, 4) + + c.Check(essSnaps, DeepEquals, []*seed.Snap{ + { + Path: s.expectedPath("snapd"), + SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, + EssentialType: snap.TypeSnapd, + Essential: true, + Required: true, + Channel: "latest/stable", + }, { + Path: s.expectedPath("pc-kernel"), + SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, + EssentialType: snap.TypeKernel, + Essential: true, + Required: true, + Channel: "20", + }, { + Path: s.expectedPath("core20"), + SideInfo: &s.AssertedSnapInfo("core20").SideInfo, + EssentialType: snap.TypeBase, + Essential: true, + Required: true, + Channel: "latest/stable", + }, { + Path: s.expectedPath("pc"), + SideInfo: &s.AssertedSnapInfo("pc").SideInfo, + EssentialType: snap.TypeGadget, + Essential: true, + Required: true, + Channel: "20", + }, + }) + + // check that PlaceInfo method works + pi := essSnaps[0].PlaceInfo() + c.Check(pi.Filename(), Equals, "snapd_1.snap") + pi = essSnaps[1].PlaceInfo() + c.Check(pi.Filename(), Equals, "pc-kernel_1.snap") + pi = essSnaps[2].PlaceInfo() + c.Check(pi.Filename(), Equals, "core20_1.snap") + pi = essSnaps[3].PlaceInfo() + c.Check(pi.Filename(), Equals, "pc_1.snap") + + runSnaps, err := seed20.ModeSnaps("run") + c.Assert(err, IsNil) + c.Check(runSnaps, HasLen, 1) + req20sn := runSnaps[0] + c.Check(req20sn.SnapName(), Equals, "required20") + c.Check(len(req20sn.Components), Equals, 2) + checked := make([]bool, 2) + for _, comp := range req20sn.Components { + switch comp.CompSideInfo.Component.ComponentName { + case "comp1": + c.Check(comp, DeepEquals, seed.Component{ + Path: filepath.Join(s.SeedDir, "snaps", "required20+comp1_22.comp"), + CompSideInfo: snap.ComponentSideInfo{ + Component: naming.NewComponentRef("required20", "comp1"), + Revision: snap.R(22), + }, + }) + checked[0] = true + case "comp2": + c.Check(comp, DeepEquals, seed.Component{ + Path: filepath.Join(s.SeedDir, "snaps", "required20+comp2_33.comp"), + CompSideInfo: snap.ComponentSideInfo{ + Component: naming.NewComponentRef("required20", "comp2"), + Revision: snap.R(33), + }, + }) + checked[1] = true + } + } + c.Check(checked, DeepEquals, []bool{true, true}) + + c.Check(seed20.NumSnaps(), Equals, 5) +} + +func (s *seed20Suite) TestLoadMetaWithComponentsNoAssertForReqComp(c *C) { + sysLabel := "20240805" + sysDir := s.makeCore20SeedWithComps(c, sysLabel, seedOpts{delegated: false}) + + // Remove all assertions for comp2 + s.massageAssertions(c, filepath.Join(sysDir, "assertions", "snaps"), + func(a asserts.Assertion) []asserts.Assertion { + if a.HeaderString("snap-id") == s.AssertedSnapID("required20") && + a.HeaderString("resource-name") == "comp2" { + return []asserts.Assertion{} + } + return []asserts.Assertion{a} + }) + + seed20, err := seed.Open(s.SeedDir, sysLabel) + c.Assert(err, IsNil) + + err = seed20.LoadAssertions(s.db, s.commitTo) + c.Assert(err, IsNil) + + err = seed20.LoadMeta(seed.AllModes, nil, s.perfTimings) + c.Assert(err, ErrorMatches, "component comp2 required in the model but is not in the seed: resource revision assertion not found for comp2") +} + +func (s *seed20Suite) TestLoadMetaWithComponentsReqNotPresent(c *C) { + sysLabel := "20240805" + s.makeCore20SeedWithComps(c, sysLabel, seedOpts{delegated: false}) + + // sneakly remove one of the components from the seed + c.Assert(os.Remove(filepath.Join(s.SeedDir, "snaps", "required20+comp2_33.comp")), IsNil) + + seed20, err := seed.Open(s.SeedDir, sysLabel) + c.Assert(err, IsNil) + + err = seed20.LoadAssertions(s.db, s.commitTo) + c.Assert(err, IsNil) + + err = seed20.LoadMeta(seed.AllModes, nil, s.perfTimings) + c.Assert(err, ErrorMatches, "component comp2 required in the model but is not in the seed: .*no such file or directory") +} + +func (s *seed20Suite) TestLoadMetaWithComponentsBadSize(c *C) { + sysLabel := "20240805" + sysDir := s.makeCore20SeedWithComps(c, sysLabel, seedOpts{delegated: false}) + + finfo, err := os.Stat(filepath.Join(s.SeedDir, "snaps", "required20+comp1_22.comp")) + c.Assert(err, IsNil) + spuriousRev, err := s.StoreSigning.Sign(asserts.SnapResourceRevisionType, map[string]interface{}{ + "authority-id": "canonical", + "snap-id": s.AssertedSnapID("required20"), + "resource-name": "comp1", + "resource-sha3-384": strings.Repeat("B", 64), + "resource-size": fmt.Sprint(finfo.Size() + 4096), + "resource-revision": "22", + "snap-revision": "11", + "developer-id": "canonical", + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + + s.massageAssertions(c, filepath.Join(sysDir, "assertions", "snaps"), + func(a asserts.Assertion) []asserts.Assertion { + if a.Type() == asserts.SnapResourceRevisionType && + a.HeaderString("snap-id") == s.AssertedSnapID("required20") && + a.HeaderString("resource-name") == "comp1" { + return []asserts.Assertion{spuriousRev} + } + return []asserts.Assertion{a} + }) + + seed20, err := seed.Open(s.SeedDir, sysLabel) + c.Assert(err, IsNil) + + err = seed20.LoadAssertions(s.db, s.commitTo) + c.Assert(err, IsNil) + + err = seed20.LoadMeta(seed.AllModes, nil, s.perfTimings) + c.Assert(err, ErrorMatches, `resource comp1 size does not match size in resource revision: .*`) +} + +func (s *seed20Suite) TestLoadMetaWithComponentsBadHash(c *C) { + sysLabel := "20240805" + sysDir := s.makeCore20SeedWithComps(c, sysLabel, seedOpts{delegated: false}) + + finfo, err := os.Stat(filepath.Join(s.SeedDir, "snaps", "required20+comp1_22.comp")) + c.Assert(err, IsNil) + spuriousRev, err := s.StoreSigning.Sign(asserts.SnapResourceRevisionType, map[string]interface{}{ + "authority-id": "canonical", + "snap-id": s.AssertedSnapID("required20"), + "resource-name": "comp1", + "resource-sha3-384": strings.Repeat("B", 64), + "resource-size": fmt.Sprint(finfo.Size()), + "resource-revision": "22", + "snap-revision": "11", + "developer-id": "canonical", + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + + s.massageAssertions(c, filepath.Join(sysDir, "assertions", "snaps"), + func(a asserts.Assertion) []asserts.Assertion { + if a.Type() == asserts.SnapResourceRevisionType && + a.HeaderString("snap-id") == s.AssertedSnapID("required20") && + a.HeaderString("resource-name") == "comp1" { + return []asserts.Assertion{spuriousRev} + } + return []asserts.Assertion{a} + }) + + seed20, err := seed.Open(s.SeedDir, sysLabel) + c.Assert(err, IsNil) + + err = seed20.LoadAssertions(s.db, s.commitTo) + c.Assert(err, IsNil) + + err = seed20.LoadMeta(seed.AllModes, nil, s.perfTimings) + c.Assert(err, ErrorMatches, `cannot validate resource comp1, hash mismatch with snap-resource-revision`) +} + +func (s *seed20Suite) TestLoadMetaWithComponentsUnmatchedProvenanceInResRev(c *C) { + assertstest.AddMany(s.StoreSigning, s.Brands.AccountsAndKeys("my-brand")...) + + sysLabel := "20240805" + sysDir := s.makeCore20SeedWithComps(c, sysLabel, seedOpts{delegated: true}) + + myBrandSigner := s.Brands.Signing("my-brand") + + snapSHA3_384_1, size1, err := asserts.SnapFileSHA3_384( + filepath.Join(s.SeedDir, "snaps", "required20+comp1_22.comp")) + c.Assert(err, IsNil) + resRev1, err := myBrandSigner.Sign(asserts.SnapResourceRevisionType, map[string]interface{}{ + "authority-id": "my-brand", + "snap-id": s.AssertedSnapID("required20"), + "resource-name": "comp1", + "resource-sha3-384": snapSHA3_384_1, + "resource-size": fmt.Sprint(size1), + "resource-revision": "22", + "snap-revision": "11", + "developer-id": "canonical", + "provenance": "other-prov", + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + snapSHA3_384_2, size2, err := asserts.SnapFileSHA3_384( + filepath.Join(s.SeedDir, "snaps", "required20+comp2_33.comp")) + c.Assert(err, IsNil) + resRev2, err := myBrandSigner.Sign(asserts.SnapResourceRevisionType, map[string]interface{}{ + "authority-id": "my-brand", + "snap-id": s.AssertedSnapID("required20"), + "resource-name": "comp2", + "resource-sha3-384": snapSHA3_384_2, + "resource-size": fmt.Sprint(size2), + "resource-revision": "33", + "snap-revision": "11", + "developer-id": "canonical", + "provenance": "other-prov", + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + + s.massageAssertions(c, filepath.Join(sysDir, "assertions", "snaps"), + func(a asserts.Assertion) []asserts.Assertion { + if a.Type() == asserts.SnapResourceRevisionType && + a.HeaderString("snap-id") == s.AssertedSnapID("required20") { + if a.HeaderString("resource-name") == "comp1" { + return []asserts.Assertion{resRev1} + } else { + return []asserts.Assertion{resRev2} + } + } + return []asserts.Assertion{a} + }) + + seed20, err := seed.Open(s.SeedDir, sysLabel) + c.Assert(err, IsNil) + + err = seed20.LoadAssertions(s.db, s.commitTo) + c.Assert(err, IsNil) + + err = seed20.LoadMeta(seed.AllModes, nil, s.perfTimings) + c.Assert(err, ErrorMatches, `resource revision provenance for comp[12] does not match snap provenance: other-prov != delegated-prov`) +} + +func (s *seed20Suite) TestLoadMetaWithComponentsUnmatchedProvenanceInResPair(c *C) { + assertstest.AddMany(s.StoreSigning, s.Brands.AccountsAndKeys("my-brand")...) + + sysLabel := "20240805" + sysDir := s.makeCore20SeedWithComps(c, sysLabel, seedOpts{delegated: true}) + + myBrandSigner := s.Brands.Signing("my-brand") + pairRev1, err := myBrandSigner.Sign(asserts.SnapResourcePairType, map[string]interface{}{ + "authority-id": "my-brand", + "snap-id": s.AssertedSnapID("required20"), + "resource-name": "comp1", + "resource-revision": "22", + "snap-revision": "1", + "developer-id": "canonical", + "provenance": "other-prov", + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + pairRev2, err := myBrandSigner.Sign(asserts.SnapResourcePairType, map[string]interface{}{ + "authority-id": "my-brand", + "snap-id": s.AssertedSnapID("required20"), + "resource-name": "comp2", + "resource-revision": "33", + "snap-revision": "1", + "developer-id": "canonical", + "provenance": "other-prov", + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + + s.massageAssertions(c, filepath.Join(sysDir, "assertions", "snaps"), + func(a asserts.Assertion) []asserts.Assertion { + if a.Type() == asserts.SnapResourcePairType && + a.HeaderString("snap-id") == s.AssertedSnapID("required20") { + if a.HeaderString("resource-name") == "comp1" { + return []asserts.Assertion{pairRev1} + } else { + return []asserts.Assertion{pairRev2} + } + } + return []asserts.Assertion{a} + }) + + seed20, err := seed.Open(s.SeedDir, sysLabel) + c.Assert(err, IsNil) + + err = seed20.LoadAssertions(s.db, s.commitTo) + c.Assert(err, IsNil) + + err = seed20.LoadMeta(seed.AllModes, nil, s.perfTimings) + c.Assert(err, ErrorMatches, `resource pair provenance for comp[12] does not match snap provenance: other-prov != delegated-prov`) +} + +func (s *seed20Suite) TestLoadMetaWithComponentsUnmatchedProvenanceInMetadata(c *C) { + assertstest.AddMany(s.StoreSigning, s.Brands.AccountsAndKeys("my-brand")...) + + sysLabel := "20240805" + s.makeCore20SeedWithComps(c, sysLabel, seedOpts{delegated: true}) + + seed20, err := seed.Open(s.SeedDir, sysLabel) + c.Assert(err, IsNil) + + err = seed20.LoadAssertions(s.db, s.commitTo) + c.Assert(err, IsNil) + + err = seed20.LoadMeta(seed.AllModes, nil, s.perfTimings) + c.Assert(err, ErrorMatches, `component ".*required20\+comp.*\.comp" has been signed under provenance "delegated-prov" different from the metadata one: "global-upload"`) +} + +func (s *seed20Suite) TestLoadAssertionsUnbalancedResRevsAndPairs(c *C) { + sysLabel := "20241031" + sysDir := s.makeCore20SeedWithComps(c, sysLabel, seedOpts{delegated: false}) + + s.massageAssertions(c, filepath.Join(sysDir, "assertions", "snaps"), + func(a asserts.Assertion) []asserts.Assertion { + if a.Type() == asserts.SnapResourcePairType && + a.HeaderString("snap-id") == s.AssertedSnapID("required20") { + return nil + } + return []asserts.Assertion{a} + }) + + seed20, err := seed.Open(s.SeedDir, sysLabel) + c.Assert(err, IsNil) + err = seed20.LoadAssertions(s.db, s.commitTo) + c.Check(err, ErrorMatches, `system unexpectedly holds a different number of snap-snap-resource-revision than snap-resource-pair assertions`) +} + +func (s *seed20Suite) TestLoadAssertionsNoMatchingPair(c *C) { + sysLabel := "20241031" + sysDir := s.makeCore20SeedWithComps(c, sysLabel, seedOpts{delegated: false}) + + pairRev, err := s.StoreSigning.Sign(asserts.SnapResourcePairType, map[string]interface{}{ + "authority-id": "canonical", + "snap-id": s.AssertedSnapID("required20"), + "resource-name": "comp1", + "resource-revision": "101", + "snap-revision": "101", + "developer-id": "canonical", + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + + s.massageAssertions(c, filepath.Join(sysDir, "assertions", "snaps"), + func(a asserts.Assertion) []asserts.Assertion { + if a.Type() == asserts.SnapResourcePairType && + a.HeaderString("snap-id") == s.AssertedSnapID("required20") && + a.HeaderString("resource-name") == "comp1" { + return []asserts.Assertion{pairRev} + } + return []asserts.Assertion{a} + }) + + seed20, err := seed.Open(s.SeedDir, sysLabel) + c.Assert(err, IsNil) + err = seed20.LoadAssertions(s.db, s.commitTo) + c.Check(err, ErrorMatches, fmt.Sprintf(`resource pair comp1 for %s does not match \(snap revision, resource revision\): \(11, 101\)`, s.AssertedSnapID("required20"))) +} + +func (s *seed20Suite) TestLoadAssertionsMultipleResRevForComp(c *C) { + sysLabel := "20241031" + sysDir := s.makeCore20SeedWithComps(c, sysLabel, seedOpts{delegated: false}) + + resRev, err := s.StoreSigning.Sign(asserts.SnapResourceRevisionType, map[string]interface{}{ + "authority-id": "canonical", + "snap-id": s.AssertedSnapID("required20"), + "resource-name": "comp1", + "resource-sha3-384": strings.Repeat("B", 64), + "resource-size": "1024", + "resource-revision": "101", + "snap-revision": "101", + "developer-id": "canonical", + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + pairRev, err := s.StoreSigning.Sign(asserts.SnapResourcePairType, map[string]interface{}{ + "authority-id": "canonical", + "snap-id": s.AssertedSnapID("required20"), + "resource-name": "comp1", + "resource-revision": "101", + "snap-revision": "101", + "developer-id": "canonical", + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + + s.massageAssertions(c, filepath.Join(sysDir, "assertions", "snaps"), + func(a asserts.Assertion) []asserts.Assertion { + if a.Type() == asserts.SnapResourceRevisionType && + a.HeaderString("snap-id") == s.AssertedSnapID("required20") && + a.HeaderString("resource-name") == "comp1" { + return []asserts.Assertion{a, resRev, pairRev} + } + return []asserts.Assertion{a} + }) + + seed20, err := seed.Open(s.SeedDir, sysLabel) + c.Assert(err, IsNil) + err = seed20.LoadAssertions(s.db, s.commitTo) + c.Check(err, ErrorMatches, fmt.Sprintf(`cannot have multiple resource revisions for the same component comp1 \(snap %s\)`, s.AssertedSnapID("required20"))) +} + +func (s *seed20Suite) TestLoadAssertionsNoMatchingResRevForResPair(c *C) { + sysLabel := "20241031" + sysDir := s.makeCore20SeedWithComps(c, sysLabel, seedOpts{delegated: false}) + + spuriousRev, err := s.StoreSigning.Sign(asserts.SnapResourceRevisionType, map[string]interface{}{ + "authority-id": "canonical", + "snap-id": s.AssertedSnapID("core20"), + "resource-name": "comp1", + "resource-sha3-384": strings.Repeat("B", 64), + "resource-size": "1024", + "resource-revision": "101", + "snap-revision": "101", + "developer-id": "canonical", + "timestamp": time.Now().UTC().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + + s.massageAssertions(c, filepath.Join(sysDir, "assertions", "snaps"), + func(a asserts.Assertion) []asserts.Assertion { + if a.Type() == asserts.SnapResourceRevisionType && + a.HeaderString("snap-id") == s.AssertedSnapID("required20") && + a.HeaderString("resource-name") == "comp1" { + return []asserts.Assertion{spuriousRev} + } + return []asserts.Assertion{a} + }) + + seed20, err := seed.Open(s.SeedDir, sysLabel) + c.Assert(err, IsNil) + err = seed20.LoadAssertions(s.db, s.commitTo) + c.Check(err, ErrorMatches, fmt.Sprintf(`resource pair for comp1 \(%s\) does not have a matching resource revision`, s.AssertedSnapID("required20"))) +} + +func (s *seed20Suite) TestLoadMetaWithLocalComponents(c *C) { + s.makeSnap(c, "snapd", "") + s.makeSnap(c, "core20", "") + s.makeSnap(c, "pc-kernel=20", "") + s.makeSnap(c, "pc=20", "") + localSnapPath := s.makeLocalSnap(c, "required20") + localComp1Path := snaptest.MakeTestComponent(c, seedtest.SampleSnapYaml["required20+comp1"]) + localComp2Path := snaptest.MakeTestComponent(c, seedtest.SampleSnapYaml["required20+comp2"]) + + sysLabel := "20240805" + model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{ + "display-name": "my model", + "architecture": "amd64", + "base": "core20", + "grade": "dangerous", + "snaps": []interface{}{ + map[string]interface{}{ + "name": "pc-kernel", + "id": s.AssertedSnapID("pc-kernel"), + "type": "kernel", + "default-channel": "20", + }, + map[string]interface{}{ + "name": "pc", + "id": s.AssertedSnapID("pc"), + "type": "gadget", + "default-channel": "20", + }, + map[string]interface{}{ + "name": "required20", + "id": s.AssertedSnapID("required20"), + "type": "app", + "components": map[string]interface{}{ + "comp1": "required", + "comp2": "required", + }, + }, + }, + }) + assertstest.AddMany(s.StoreSigning, s.Brands.AccountsAndKeys("my-brand")...) + s.MakeSeedWithModel(c, sysLabel, model, + []*seedwriter.OptionsSnap{{Path: localSnapPath}}, + map[string][]string{"required20": {localComp1Path, localComp2Path}}) + + seed20, err := seed.Open(s.SeedDir, sysLabel) + c.Assert(err, IsNil) + + err = seed20.LoadAssertions(s.db, s.commitTo) + c.Assert(err, IsNil) + + err = seed20.LoadMeta(seed.AllModes, nil, s.perfTimings) + c.Assert(err, IsNil) + + c.Check(seed20.UsesSnapdSnap(), Equals, true) + + essSnaps := seed20.EssentialSnaps() + c.Check(essSnaps, HasLen, 4) + + c.Check(essSnaps, DeepEquals, []*seed.Snap{ + { + Path: s.expectedPath("snapd"), + SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, + EssentialType: snap.TypeSnapd, + Essential: true, + Required: true, + Channel: "latest/stable", + }, { + Path: s.expectedPath("pc-kernel"), + SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, + EssentialType: snap.TypeKernel, + Essential: true, + Required: true, + Channel: "20", + }, { + Path: s.expectedPath("core20"), + SideInfo: &s.AssertedSnapInfo("core20").SideInfo, + EssentialType: snap.TypeBase, + Essential: true, + Required: true, + Channel: "latest/stable", + }, { + Path: s.expectedPath("pc"), + SideInfo: &s.AssertedSnapInfo("pc").SideInfo, + EssentialType: snap.TypeGadget, + Essential: true, + Required: true, + Channel: "20", + }, + }) + + // check that PlaceInfo method works + pi := essSnaps[0].PlaceInfo() + c.Check(pi.Filename(), Equals, "snapd_1.snap") + pi = essSnaps[1].PlaceInfo() + c.Check(pi.Filename(), Equals, "pc-kernel_1.snap") + pi = essSnaps[2].PlaceInfo() + c.Check(pi.Filename(), Equals, "core20_1.snap") + pi = essSnaps[3].PlaceInfo() + c.Check(pi.Filename(), Equals, "pc_1.snap") + + runSnaps, err := seed20.ModeSnaps("run") + c.Assert(err, IsNil) + c.Check(runSnaps, HasLen, 1) + req20sn := runSnaps[0] + c.Check(req20sn.SnapName(), Equals, "required20") + c.Check(len(req20sn.Components), Equals, 2) + checked := make([]bool, 2) + for _, comp := range req20sn.Components { + switch comp.CompSideInfo.Component.ComponentName { + case "comp1": + c.Check(comp, DeepEquals, seed.Component{ + Path: filepath.Join(s.SeedDir, "systems", sysLabel, + "snaps", "required20+comp1_1.0.comp"), + CompSideInfo: snap.ComponentSideInfo{ + Component: naming.NewComponentRef("required20", "comp1"), + }, + }) + checked[0] = true + case "comp2": + c.Check(comp, DeepEquals, seed.Component{ + Path: filepath.Join(s.SeedDir, "systems", sysLabel, + "snaps", "required20+comp2_2.0.comp"), + CompSideInfo: snap.ComponentSideInfo{ + Component: naming.NewComponentRef("required20", "comp2"), + }, + }) + checked[1] = true + } + } + c.Check(checked, DeepEquals, []bool{true, true}) + + c.Check(seed20.NumSnaps(), Equals, 5) +} + +func (s *seed20Suite) TestLoadMetaCore20ExtraSnapsWithComps(c *C) { + s.makeSnap(c, "snapd", "") + s.makeSnap(c, "core20", "") + s.makeSnap(c, "pc-kernel=20", "") + s.makeSnap(c, "pc=20", "") + comRevs := map[string]snap.Revision{ + "comp1": snap.R(22), + "comp2": snap.R(33), + } + s.MakeAssertedSnapWithComps(c, seedtest.SampleSnapYaml["required20"], nil, + snap.R(11), comRevs, "canonical", s.StoreSigning.Database) + + sysLabel := "20251122" + s.MakeSeed(c, sysLabel, "my-brand", "my-model", map[string]interface{}{ + "display-name": "my model", + "architecture": "amd64", + "base": "core20", + "grade": "dangerous", + "snaps": []interface{}{ + map[string]interface{}{ + "name": "pc-kernel", + "id": s.AssertedSnapID("pc-kernel"), + "type": "kernel", + "default-channel": "20", + }, + map[string]interface{}{ + "name": "pc", + "id": s.AssertedSnapID("pc"), + "type": "gadget", + "default-channel": "20", + }}, + }, []*seedwriter.OptionsSnap{ + {Name: "required20", Components: []seedwriter.OptionsComponent{ + {Name: "comp1"}, {Name: "comp2"}}}, + }) + + seed20, err := seed.Open(s.SeedDir, sysLabel) + c.Assert(err, IsNil) + + err = seed20.LoadAssertions(s.db, s.commitTo) + c.Assert(err, IsNil) + + err = seed20.LoadMeta(seed.AllModes, nil, s.perfTimings) + c.Assert(err, IsNil) + + c.Check(seed20.UsesSnapdSnap(), Equals, true) + + essSnaps := seed20.EssentialSnaps() + c.Check(essSnaps, HasLen, 4) + + c.Check(essSnaps, DeepEquals, []*seed.Snap{ + { + Path: s.expectedPath("snapd"), + SideInfo: &s.AssertedSnapInfo("snapd").SideInfo, + EssentialType: snap.TypeSnapd, + Essential: true, + Required: true, + Channel: "latest/stable", + }, { + Path: s.expectedPath("pc-kernel"), + SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo, + EssentialType: snap.TypeKernel, + Essential: true, + Required: true, + Channel: "20", + }, { + Path: s.expectedPath("core20"), + SideInfo: &s.AssertedSnapInfo("core20").SideInfo, + EssentialType: snap.TypeBase, + Essential: true, + Required: true, + Channel: "latest/stable", + }, { + Path: s.expectedPath("pc"), + SideInfo: &s.AssertedSnapInfo("pc").SideInfo, + EssentialType: snap.TypeGadget, + Essential: true, + Required: true, + Channel: "20", + }, + }) + + sysSnapsDir := filepath.Join(s.SeedDir, "systems", sysLabel, "snaps") + + runSnaps, err := seed20.ModeSnaps("run") + c.Assert(err, IsNil) + c.Check(runSnaps, HasLen, 1) + c.Check(runSnaps, DeepEquals, []*seed.Snap{ + { + Path: filepath.Join(sysSnapsDir, "required20_11.snap"), + SideInfo: &s.AssertedSnapInfo("required20").SideInfo, + Channel: "latest/stable", + Components: []seed.Component{ + { + Path: filepath.Join(sysSnapsDir, "required20+comp1_22.comp"), + CompSideInfo: *snap.NewComponentSideInfo( + naming.NewComponentRef("required20", "comp1"), snap.R(22)), + }, + { + Path: filepath.Join(sysSnapsDir, "required20+comp2_33.comp"), + CompSideInfo: *snap.NewComponentSideInfo( + naming.NewComponentRef("required20", "comp2"), snap.R(33)), + }, + }, + }, + }) + + recoverSnaps, err := seed20.ModeSnaps("recover") + c.Assert(err, IsNil) + c.Check(recoverSnaps, HasLen, 0) +} diff --git a/seed/seedtest/seedtest.go b/seed/seedtest/seedtest.go index 5eac81aa722..904b95ce198 100644 --- a/seed/seedtest/seedtest.go +++ b/seed/seedtest/seedtest.go @@ -47,13 +47,16 @@ type SeedSnaps struct { StoreSigning *assertstest.StoreStack Brands *assertstest.SigningAccounts - snaps map[string]string - infos map[string]*snap.Info - comps map[string][]store.SnapResourceResult + snaps map[string]string + infos map[string]*snap.Info + compInfos map[string][]*snap.ComponentInfo + comps map[string][]store.SnapResourceResult snapAssertNow time.Time snapRevs map[string]*asserts.SnapRevision + resRevs map[string][]*asserts.SnapResourceRevision + resPairs map[string][]*asserts.SnapResourcePair } // SetupAssertSigning initializes StoreSigning for storeBrandID and Brands. @@ -89,6 +92,10 @@ func (ss *SeedSnaps) MakeAssertedSnapWithComps(c *C, snapYaml string, files [][] return ss.makeAssertedSnap(c, snapYaml, files, revision, compRevisions, developerID, ss.StoreSigning.SigningDB, "", nil, dbs...) } +func (ss *SeedSnaps) MakeAssertedDelegatedSnapWithComps(c *C, snapYaml string, files [][]string, revision snap.Revision, compRevisions map[string]snap.Revision, developerID, delegateID, revProvenance string, revisionAuthority map[string]interface{}, dbs ...*asserts.Database) (*asserts.SnapDeclaration, *asserts.SnapRevision) { + return ss.makeAssertedSnap(c, snapYaml, files, revision, compRevisions, developerID, ss.Brands.Signing(delegateID), revProvenance, revisionAuthority, dbs...) +} + func (ss *SeedSnaps) makeAssertedSnap(c *C, snapYaml string, files [][]string, revision snap.Revision, compRevisions map[string]snap.Revision, developerID string, revSigning *assertstest.SigningDB, revProvenance string, revisionAuthority map[string]interface{}, dbs ...*asserts.Database) (*asserts.SnapDeclaration, *asserts.SnapRevision) { info, err := snap.InfoFromSnapYaml([]byte(snapYaml)) c.Assert(err, IsNil) @@ -143,8 +150,11 @@ func (ss *SeedSnaps) makeAssertedSnap(c *C, snapYaml string, files [][]string, r if ss.snaps == nil { ss.snaps = make(map[string]string) ss.infos = make(map[string]*snap.Info) + ss.compInfos = make(map[string][]*snap.ComponentInfo) ss.comps = make(map[string][]store.SnapResourceResult) ss.snapRevs = make(map[string]*asserts.SnapRevision) + ss.resRevs = make(map[string][]*asserts.SnapResourceRevision) + ss.resPairs = make(map[string][]*asserts.SnapResourcePair) } ss.snaps[snapName] = snapFile @@ -164,6 +174,9 @@ func (ss *SeedSnaps) makeAssertedSnap(c *C, snapYaml string, files [][]string, r c.Assert(len(compRevisions), Equals, len(info.Components)) resResults := make([]store.SnapResourceResult, 0, len(info.Components)) + cinfos := make([]*snap.ComponentInfo, 0, len(info.Components)) + resRevs := make([]*asserts.SnapResourceRevision, 0, len(info.Components)) + resPairs := make([]*asserts.SnapResourcePair, 0, len(info.Components)) for _, comp := range info.Components { cref := naming.NewComponentRef(snapName, comp.Name) compFile := snaptest.MakeTestComponent(c, SampleSnapYaml[cref.String()]) @@ -189,6 +202,7 @@ func (ss *SeedSnaps) makeAssertedSnap(c *C, snapYaml string, files [][]string, r } resRev, err := revSigning.Sign(asserts.SnapResourceRevisionType, resRevHeads, nil, "") c.Assert(err, IsNil) + resRevs = append(resRevs, resRev.(*asserts.SnapResourceRevision)) // and the resource pair revision resPairHeads := map[string]interface{}{ @@ -205,6 +219,7 @@ func (ss *SeedSnaps) makeAssertedSnap(c *C, snapYaml string, files [][]string, r } resPair, err := revSigning.Sign(asserts.SnapResourcePairType, resPairHeads, nil, "") c.Assert(err, IsNil) + resPairs = append(resPairs, resPair.(*asserts.SnapResourcePair)) for _, db := range dbs { err := db.Add(resRev) @@ -222,8 +237,16 @@ func (ss *SeedSnaps) makeAssertedSnap(c *C, snapYaml string, files [][]string, r Name: comp.Name, Revision: compRev.N, }) + + cinfo, err := snap.InfoFromComponentYaml([]byte(SampleSnapYaml[cref.String()])) + c.Assert(err, IsNil) + cinfo.ComponentSideInfo = *snap.NewComponentSideInfo(cref, compRev) + cinfos = append(cinfos, cinfo) } + ss.compInfos[snapName] = cinfos ss.comps[snapName] = resResults + ss.resRevs[snapName] = resRevs + ss.resPairs[snapName] = resPairs return snapDecl, snapRev } @@ -248,6 +271,18 @@ func (ss *SeedSnaps) AssertedSnapRevision(snapName string) *asserts.SnapRevision return ss.snapRevs[snapName] } +func (ss *SeedSnaps) AssertedResourceRevision(snapName string) []*asserts.SnapResourceRevision { + return ss.resRevs[snapName] +} + +func (ss *SeedSnaps) AssertedResourcePair(snapName string) []*asserts.SnapResourcePair { + return ss.resPairs[snapName] +} + +func (ss *SeedSnaps) AssertedComponentInfos(snapName string) []*snap.ComponentInfo { + return ss.compInfos[snapName] +} + // TestingSeed16 helps setting up a populated Core 16/18 testing seed. type TestingSeed16 struct { SeedSnaps @@ -340,12 +375,12 @@ func (s *TestingSeed20) MakeSeed(c *C, label, brandID, modelID string, modelHead assertstest.AddMany(s.StoreSigning, s.Brands.AccountsAndKeys(brandID)...) - s.MakeSeedWithModel(c, label, model, optSnaps) + s.MakeSeedWithModel(c, label, model, optSnaps, nil) return model } // MakeSeedWithModel creates the seed with given label for a given model -func (s *TestingSeed20) MakeSeedWithModel(c *C, label string, model *asserts.Model, optSnaps []*seedwriter.OptionsSnap) { +func (s *TestingSeed20) MakeSeedWithModel(c *C, label string, model *asserts.Model, optSnaps []*seedwriter.OptionsSnap, compPathsBySnap map[string][]string) { db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ Backstore: asserts.NewMemoryBackstore(), Trusted: s.StoreSigning.Trusted, @@ -408,7 +443,34 @@ func (s *TestingSeed20) MakeSeedWithModel(c *C, label string, model *asserts.Mod c.Assert(err, IsNil) info, err := snap.ReadInfoFromSnapFile(f, si) c.Assert(err, IsNil) - w.SetInfo(sn, info, nil) + + // Add components from option paths + compPaths := compPathsBySnap[info.SnapName()] + seedComps := make(map[string]*seedwriter.SeedComponent, len(compPaths)) + for _, compPath := range compPaths { + compf, err := snapfile.Open(compPath) + c.Assert(err, IsNil) + cinfo, err := snap.ReadComponentInfoFromContainer(compf, nil, nil) + c.Assert(err, IsNil) + if si != nil { + // snap was asserted, components should be too + csi, cRefs, err := seedwriter.DeriveComponentSideInfo( + compPath, cinfo, info, model, sf, db) + if !errors.Is(err, &asserts.NotFoundError{}) { + c.Assert(err, IsNil) + cinfo.ComponentSideInfo = *csi + } + aRefs = append(aRefs, cRefs...) + } + + seedComps[cinfo.Component.ComponentName] = &seedwriter.SeedComponent{ + ComponentRef: cinfo.Component, + Path: compPath, + Info: cinfo, + } + } + + w.SetInfo(sn, info, seedComps) if aRefs != nil { localARefs[sn] = aRefs } @@ -425,6 +487,17 @@ func (s *TestingSeed20) MakeSeedWithModel(c *C, label string, model *asserts.Mod if err = sf.Save(s.snapRevs[sn.SnapName()]); err != nil { return nil, err } + // Components assertions + for _, resRev := range s.resRevs[sn.SnapName()] { + if err = sf.Save(resRev); err != nil { + return nil, err + } + } + for _, resPair := range s.resPairs[sn.SnapName()] { + if err = sf.Save(resPair); err != nil { + return nil, err + } + } return sf.Refs()[prev:], nil } @@ -437,7 +510,22 @@ func (s *TestingSeed20) MakeSeedWithModel(c *C, label string, model *asserts.Mod info := s.AssertedSnapInfo(name) c.Assert(info, NotNil, Commentf("no snap info for %q", name)) - err := w.SetInfo(sn, info, nil) + seedComps := make(map[string]*seedwriter.SeedComponent, len(sn.Components)) + cinfos := s.AssertedComponentInfos(name) + for _, cc := range sn.Components { + comp := cc + found := false + for _, ci := range cinfos { + if ci.Component.ComponentName == comp.ComponentName { + comp.Info = ci + found = true + break + } + } + c.Assert(found, Equals, true) + seedComps[comp.ComponentName] = &comp + } + err := w.SetInfo(sn, info, seedComps) c.Assert(err, IsNil) if _, err := os.Stat(sn.Path); err == nil { @@ -445,8 +533,13 @@ func (s *TestingSeed20) MakeSeedWithModel(c *C, label string, model *asserts.Mod continue } + // Put snaps/components containers in the seed err = os.Rename(s.AssertedSnap(name), sn.Path) c.Assert(err, IsNil) + for _, comp := range sn.Components { + err = os.Rename(s.AssertedSnap(comp.String()), comp.Path) + c.Assert(err, IsNil) + } } complete, err := w.Downloaded(fetchAsserts) diff --git a/seed/seedwriter/seed20.go b/seed/seedwriter/seed20.go index 26011eabe89..e7fb362d703 100644 --- a/seed/seedwriter/seed20.go +++ b/seed/seedwriter/seed20.go @@ -359,14 +359,14 @@ func (tr *tree20) writeAssertions(db asserts.RODatabase, modelRefs []*asserts.Re return nil } -func (tr *tree20) seedSnapComponents(sn *SeedSnap) []internal.Component { - compOpts := make([]internal.Component, len(sn.Components)) +func (tr *tree20) seedSnapComponents(sn *SeedSnap) []internal.Component20 { + compOpts := make([]internal.Component20, len(sn.Components)) for i, comp := range sn.Components { unassertedComp := "" if sn.Info.ID() == "" { unassertedComp = filepath.Base(comp.Path) } - compOpts[i] = internal.Component{ + compOpts[i] = internal.Component20{ Name: comp.ComponentName, Unasserted: unassertedComp, } diff --git a/seed/seedwriter/writer.go b/seed/seedwriter/writer.go index 1d2fd295ebd..0a2dc1f9fde 100644 --- a/seed/seedwriter/writer.go +++ b/seed/seedwriter/writer.go @@ -967,11 +967,18 @@ func (w *Writer) extraSnapToSeed(optSnap *OptionsSnap) (*SeedSnap, error) { sn := w.localSnaps[optSnap] if sn == nil { // not local, to download + seedComps := make([]SeedComponent, 0, len(optSnap.Components)) + for _, optComp := range optSnap.Components { + seedComps = append(seedComps, SeedComponent{ + ComponentRef: naming.NewComponentRef(optSnap.Name, optComp.Name), + }) + } sn = &SeedSnap{ SnapRef: optSnap, local: false, optionSnap: optSnap, + Components: seedComps, } } if sn.SnapName() == "" { @@ -1618,7 +1625,7 @@ func (w *Writer) SeedSnaps(copySnap func(name, src, dst string) error) error { return err } // copy components - for _, comp := range sn.Components { + for i, comp := range sn.Components { compDst, err := compPath(&comp) if err != nil { return err @@ -1626,8 +1633,10 @@ func (w *Writer) SeedSnaps(copySnap func(name, src, dst string) error) error { if err := copySnap(comp.ComponentRef.String(), comp.Path, compDst); err != nil { return err } + // record final destination path (for correct options.yaml) + sn.Components[i].Path = compDst } - // record final destination path + // record final destination path (for correct options.yaml) sn.Path = dst } if !info.Revision.Unset() { diff --git a/seed/seedwriter/writer_test.go b/seed/seedwriter/writer_test.go index 405c02f46a5..daa8bdeabe4 100644 --- a/seed/seedwriter/writer_test.go +++ b/seed/seedwriter/writer_test.go @@ -233,6 +233,18 @@ func (s *writerSuite) fetchAsserts(c *C) seedwriter.AssertsFetchFunc { if err != nil { return nil, err } + for _, a := range s.AssertedResourceRevision(sn.SnapName()) { + err := s.rf.Fetch(a.Ref()) + if err != nil { + return nil, err + } + } + for _, a := range s.AssertedResourcePair(sn.SnapName()) { + err := s.rf.Fetch(a.Ref()) + if err != nil { + return nil, err + } + } aRefs = s.rf.Refs()[prev:] s.aRefs[sn.SnapName()] = aRefs } @@ -242,8 +254,17 @@ func (s *writerSuite) fetchAsserts(c *C) seedwriter.AssertsFetchFunc { func (s *writerSuite) doFillMetaDownloadedSnap(c *C, w *seedwriter.Writer, sn *seedwriter.SeedSnap) *snap.Info { info := s.AssertedSnapInfo(sn.SnapName()) + cinfos := s.AssertedComponentInfos(sn.SnapName()) + seedComps := make(map[string]*seedwriter.SeedComponent, len(cinfos)) + for _, cinfo := range cinfos { + cref := naming.NewComponentRef(sn.SnapName(), cinfo.Component.ComponentName) + seedComps[cinfo.Component.ComponentName] = &seedwriter.SeedComponent{ + ComponentRef: cref, + Info: cinfo, + } + } c.Assert(info, NotNil, Commentf("%s not defined", sn.SnapName())) - err := w.SetInfo(sn, info, nil) + err := w.SetInfo(sn, info, seedComps) c.Assert(err, IsNil) return info } @@ -254,6 +275,10 @@ func (s *writerSuite) fillDownloadedSnap(c *C, w *seedwriter.Writer, sn *seedwri c.Assert(sn.Path, Equals, filepath.Join(s.opts.SeedDir, "snaps", info.Filename())) err := os.Rename(s.AssertedSnap(sn.SnapName()), sn.Path) c.Assert(err, IsNil) + for _, seedComp := range sn.Components { + err := os.Rename(s.AssertedSnap(seedComp.String()), seedComp.Path) + c.Assert(err, IsNil) + } } func (s *writerSuite) fillMetaDownloadedSnap(c *C, w *seedwriter.Writer, sn *seedwriter.SeedSnap) { @@ -3079,16 +3104,16 @@ func (s *writerSuite) testSeedSnapsWriteMetaCore20LocalSnaps(c *C, withComps boo options20, err := seedwriter.InternalReadOptions20(filepath.Join(systemDir, "options.yaml")) c.Assert(err, IsNil) - var compOpts []internal.Component + var compOpts []internal.Component20 if withComps { - compOpts = []internal.Component{ + compOpts = []internal.Component20{ { Name: "comp1", - Unasserted: filepath.Base(pathComp1), + Unasserted: "required20+comp1_1.0.comp", }, { Name: "comp2", - Unasserted: filepath.Base(pathComp2), + Unasserted: "required20+comp2_2.0.comp", }, } } @@ -3506,12 +3531,21 @@ func (s *writerSuite) TestSeedSnapsWriteMetaCore20ExtraSnaps(c *C) { s.makeSnap(c, "core18", "") s.makeSnap(c, "cont-producer", "developerid") contConsumerFn := s.makeLocalSnap(c, "cont-consumer") + comRevs := map[string]snap.Revision{ + "comp1": snap.R(22), + "comp2": snap.R(33), + } + s.SeedSnaps.MakeAssertedSnapWithComps(c, seedtest.SampleSnapYaml["required20"], nil, + snap.R(21), comRevs, "canonical", s.StoreSigning.Database) s.opts.Label = "20191122" w, err := seedwriter.New(model, s.opts) c.Assert(err, IsNil) - err = w.SetOptionsSnaps([]*seedwriter.OptionsSnap{{Name: "cont-producer", Channel: "edge"}, {Name: "core18"}, {Path: contConsumerFn}}) + err = w.SetOptionsSnaps([]*seedwriter.OptionsSnap{{Name: "cont-producer", Channel: "edge"}, + {Name: "core18"}, {Path: contConsumerFn}, + {Name: "required20", Components: []seedwriter.OptionsComponent{ + {Name: "comp1"}, {Name: "comp2"}}}}) c.Assert(err, IsNil) err = w.Start(s.db, s.rf) @@ -3554,9 +3588,10 @@ func (s *writerSuite) TestSeedSnapsWriteMetaCore20ExtraSnaps(c *C) { snaps, err = w.SnapsToDownload() c.Assert(err, IsNil) - c.Assert(snaps, HasLen, 2) + c.Assert(snaps, HasLen, 3) c.Check(snaps[0].SnapName(), Equals, "cont-producer") c.Check(snaps[1].SnapName(), Equals, "core18") + c.Check(snaps[2].SnapName(), Equals, "required20") for _, sn := range snaps { channel := "latest/stable" @@ -3571,6 +3606,10 @@ func (s *writerSuite) TestSeedSnapsWriteMetaCore20ExtraSnaps(c *C) { c.Assert(sn.Path, Equals, filepath.Join(s.opts.SeedDir, "systems", s.opts.Label, "snaps", info.Filename())) err := os.Rename(s.AssertedSnap(sn.SnapName()), sn.Path) c.Assert(err, IsNil) + for _, seedComp := range sn.Components { + err := os.Rename(s.AssertedSnap(seedComp.String()), seedComp.Path) + c.Assert(err, IsNil) + } } complete, err = w.Downloaded(s.fetchAsserts(c)) @@ -3595,10 +3634,13 @@ func (s *writerSuite) TestSeedSnapsWriteMetaCore20ExtraSnaps(c *C) { c.Assert(err, IsNil) c.Check(l, HasLen, 4) - // extra snaps were put in system snaps dir + // extra containers were put in system snaps dir c.Check(filepath.Join(systemDir, "snaps", "core18_1.snap"), testutil.FilePresent) c.Check(filepath.Join(systemDir, "snaps", "cont-producer_1.snap"), testutil.FilePresent) c.Check(filepath.Join(systemDir, "snaps", "cont-consumer_1.0.snap"), testutil.FilePresent) + c.Check(filepath.Join(systemDir, "snaps", "required20_21.snap"), testutil.FilePresent) + c.Check(filepath.Join(systemDir, "snaps", "required20+comp1_22.comp"), testutil.FilePresent) + c.Check(filepath.Join(systemDir, "snaps", "required20+comp2_33.comp"), testutil.FilePresent) // check extra-snaps in assertions snapAsserts := seedtest.ReadAssertions(c, filepath.Join(systemDir, "assertions", "extra-snaps")) @@ -3609,6 +3651,10 @@ func (s *writerSuite) TestSeedSnapsWriteMetaCore20ExtraSnaps(c *C) { if a.Type() == asserts.SnapRevisionType { rev := a.(*asserts.SnapRevision) uniq = fmt.Sprintf("%s@%d", rev.SnapID(), rev.SnapRevision()) + } else if a.Type() == asserts.SnapResourceRevisionType { + rev := a.(*asserts.SnapResourceRevision) + uniq = fmt.Sprintf("%s+%s@%d", rev.SnapID(), + rev.ResourceName(), rev.ResourceRevision()) } seen[uniq] = true } @@ -3616,16 +3662,25 @@ func (s *writerSuite) TestSeedSnapsWriteMetaCore20ExtraSnaps(c *C) { snapRevUniq := func(snapName string, revno int) string { return fmt.Sprintf("%s@%d", s.AssertedSnapID(snapName), revno) } + resRevUniq := func(snapName, compName string, resRev int) string { + return fmt.Sprintf("%s+%s@%d", s.AssertedSnapID(snapName), compName, resRev) + } snapDeclUniq := func(snapName string) string { return "snap-declaration/16/" + s.AssertedSnapID(snapName) } c.Check(seen, DeepEquals, map[string]bool{ - "account/developerid": true, - snapDeclUniq("core18"): true, - snapDeclUniq("cont-producer"): true, - snapRevUniq("core18", 1): true, - snapRevUniq("cont-producer", 1): true, + "account/developerid": true, + snapDeclUniq("core18"): true, + snapDeclUniq("cont-producer"): true, + snapDeclUniq("required20"): true, + snapRevUniq("core18", 1): true, + snapRevUniq("cont-producer", 1): true, + snapRevUniq("required20", 21): true, + resRevUniq("required20", "comp1", 22): true, + resRevUniq("required20", "comp2", 33): true, + "snap-resource-pair/required20ididididididididididid/comp1/22/21": true, + "snap-resource-pair/required20ididididididididididid/comp2/33/21": true, }) options20, err := seedwriter.InternalReadOptions20(filepath.Join(systemDir, "options.yaml")) @@ -3646,6 +3701,12 @@ func (s *writerSuite) TestSeedSnapsWriteMetaCore20ExtraSnaps(c *C) { Name: "cont-consumer", Unasserted: "cont-consumer_1.0.snap", }, + { + Name: "required20", + SnapID: s.AssertedSnapID("required20"), + Channel: "latest/stable", + Components: []internal.Component20{{Name: "comp1"}, {Name: "comp2"}}, + }, }) }