diff --git a/daemon/api_sideload_n_try_test.go b/daemon/api_sideload_n_try_test.go index 542f8a9a157..b1288718443 100644 --- a/daemon/api_sideload_n_try_test.go +++ b/daemon/api_sideload_n_try_test.go @@ -2026,7 +2026,7 @@ func (s *sideloadSuite) TestSideloadManyOnlyComponents(c *check.C) { st.Lock() defer st.Unlock() - expectedFileNames := []string{"one+comp-one.comp.comp", "one+comp-two.comp.comp", "one+comp-three.comp.comp", "one+comp-four.comp.comp"} + expectedFileNames := []string{"one+comp-one.comp", "one+comp-two.comp", "one+comp-three.comp", "one+comp-four.comp"} fullComponentNames := make([]string, len(components)) for i, c := range components { diff --git a/snap/snaptest/snaptest.go b/snap/snaptest/snaptest.go index a9917fd29a4..3355f0dea73 100644 --- a/snap/snaptest/snaptest.go +++ b/snap/snaptest/snaptest.go @@ -274,7 +274,7 @@ func MakeTestComponentWithFiles(c *check.C, componentName, componentYaml string, func MakeTestComponent(c *check.C, compYaml string) string { compInfo, err := snap.InfoFromComponentYaml([]byte(compYaml)) c.Assert(err, check.IsNil) - return MakeTestComponentWithFiles(c, compInfo.FullName()+".comp", compYaml, nil) + return MakeTestComponentWithFiles(c, compInfo.FullName(), compYaml, nil) } func populateContainer(c *check.C, yamlFile, yamlContent string, files [][]string) string { diff --git a/tests/lib/fakestore/cmd/fakestore/cmd_new_snap_decl.go b/tests/lib/fakestore/cmd/fakestore/cmd_new_snap_decl.go index 110b5b91e02..4330a61ccd6 100644 --- a/tests/lib/fakestore/cmd/fakestore/cmd_new_snap_decl.go +++ b/tests/lib/fakestore/cmd/fakestore/cmd_new_snap_decl.go @@ -29,7 +29,7 @@ import ( type cmdNewSnapDeclaration struct { Positional struct { - Snap string `description:"Snap file"` + Snap string `description:"Path to a snap file"` } `positional-args:"yes"` TopDir string `long:"dir" description:"Directory to be used by the store to keep and serve snaps, /asserts is used for assertions"` diff --git a/tests/lib/fakestore/cmd/fakestore/cmd_new_snap_resource_pair.go b/tests/lib/fakestore/cmd/fakestore/cmd_new_snap_resource_pair.go new file mode 100644 index 00000000000..3ce11d5421e --- /dev/null +++ b/tests/lib/fakestore/cmd/fakestore/cmd_new_snap_resource_pair.go @@ -0,0 +1,51 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/snapcore/snapd/tests/lib/fakestore/refresh" +) + +type cmdNewSnapResourcePair struct { + Positional struct { + Component string `description:"Path to a component blob file"` + SnapResourcePairJSONPath string `description:"Path to a json encoded snap resource pair revision subset"` + } `positional-args:"yes" required:"yes"` + + TopDir string `long:"dir" description:"Directory to be used by the store to keep and serve snaps, /asserts is used for assertions"` +} + +func (x *cmdNewSnapResourcePair) Execute(args []string) error { + content, err := os.ReadFile(x.Positional.SnapResourcePairJSONPath) + if err != nil { + return err + } + + headers := make(map[string]interface{}) + if err := json.Unmarshal(content, &headers); err != nil { + return err + } + + p, err := refresh.NewSnapResourcePair(x.TopDir, x.Positional.Component, headers) + if err != nil { + return err + } + fmt.Println(p) + return nil +} + +var shortNewSnapResourcePairHelp = "Make a new snap resource pair" + +var longNewSnapResourcePairHelp = ` +Generate a new snap resource pair signed with test keys. Snap ID, snap revision, +and component revision must be provided in the given JSON file. All other +headers are either derived from the component file or optional, but can be +overridden via the given JSON file. +` + +func init() { + parser.AddCommand("new-snap-resource-pair", shortNewSnapResourcePairHelp, longNewSnapResourcePairHelp, + &cmdNewSnapResourcePair{}) +} diff --git a/tests/lib/fakestore/cmd/fakestore/cmd_new_snap_resource_revision.go b/tests/lib/fakestore/cmd/fakestore/cmd_new_snap_resource_revision.go new file mode 100644 index 00000000000..890bb9cf928 --- /dev/null +++ b/tests/lib/fakestore/cmd/fakestore/cmd_new_snap_resource_revision.go @@ -0,0 +1,51 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/snapcore/snapd/tests/lib/fakestore/refresh" +) + +type cmdNewSnapResourceRevision struct { + Positional struct { + Component string `description:"Path to a component blob file"` + SnapResourceRevJsonPath string `description:"Path to a json encoded snap resource revision subset"` + } `positional-args:"yes" required:"yes"` + + TopDir string `long:"dir" description:"Directory to be used by the store to keep and serve snaps, /asserts is used for assertions"` +} + +func (x *cmdNewSnapResourceRevision) Execute(args []string) error { + content, err := os.ReadFile(x.Positional.SnapResourceRevJsonPath) + if err != nil { + return err + } + + headers := make(map[string]interface{}) + if err := json.Unmarshal(content, &headers); err != nil { + return err + } + + p, err := refresh.NewSnapResourceRevision(x.TopDir, x.Positional.Component, headers) + if err != nil { + return err + } + fmt.Println(p) + return nil +} + +var shortNewSnapResourceRevisionHelp = "Make a new snap resource revision" + +var longNewSnapResourceRevisionHelp = ` +Generate a new snap resource revision signed with test keys. Snap ID and +revision must be provided in the given JSON file. All other headers are either +derived from the component file or optional, but can be overridden via the given +JSON file. +` + +func init() { + parser.AddCommand("new-snap-resource-revision", shortNewSnapResourceRevisionHelp, longNewSnapResourceRevisionHelp, + &cmdNewSnapResourceRevision{}) +} diff --git a/tests/lib/fakestore/cmd/fakestore/cmd_new_snap_rev.go b/tests/lib/fakestore/cmd/fakestore/cmd_new_snap_rev.go index 82bb653e8c0..1085f815742 100644 --- a/tests/lib/fakestore/cmd/fakestore/cmd_new_snap_rev.go +++ b/tests/lib/fakestore/cmd/fakestore/cmd_new_snap_rev.go @@ -29,7 +29,7 @@ import ( type cmdNewSnapRevision struct { Positional struct { - Snap string `description:"Snap file"` + Snap string `description:"Path to a snap file"` } `positional-args:"yes"` TopDir string `long:"dir" description:"Directory to be used by the store to keep and serve snaps, /asserts is used for assertions"` diff --git a/tests/lib/fakestore/refresh/snap_asserts.go b/tests/lib/fakestore/refresh/snap_asserts.go index cc03b6b87f7..21132025905 100644 --- a/tests/lib/fakestore/refresh/snap_asserts.go +++ b/tests/lib/fakestore/refresh/snap_asserts.go @@ -28,6 +28,8 @@ import ( "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/asserts/systestkeys" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snapfile" ) func snapNameFromPath(snapPath string) string { @@ -68,6 +70,102 @@ func NewSnapRevision(targetDir string, snap string, headers map[string]interface return writeAssert(a, targetDir) } +func NewSnapResourceRevision(targetDir string, compPath string, headers map[string]interface{}) (string, error) { + db, err := newAssertsDB(systestkeys.TestStorePrivKey) + if err != nil { + return "", err + } + digest, size, err := asserts.SnapFileSHA3_384(compPath) + if err != nil { + return "", err + } + + container, err := snapfile.Open(compPath) + if err != nil { + return "", err + } + + ci, err := snap.ReadComponentInfoFromContainer(container, nil, nil) + if err != nil { + return "", err + } + + required := []string{"snap-id", "resource-revision"} + for _, r := range required { + if _, ok := headers[r]; !ok { + return "", fmt.Errorf("missing required header %q", r) + } + } + + defaults := map[string]interface{}{ + "type": "snap-resource-revision", + "authority-id": "testrootorg", + "developer-id": "testrootorg", + "resource-name": ci.Component.ComponentName, + "timestamp": time.Now().Format(time.RFC3339), + "resource-size": fmt.Sprintf("%d", size), + "resource-sha3-384": digest, + } + for k, v := range defaults { + if _, ok := headers[k]; !ok { + headers[k] = v + } + } + headers["authority-id"] = "testrootorg" + headers["snap-sha3-384"] = digest + headers["snap-size"] = fmt.Sprintf("%d", size) + headers["timestamp"] = time.Now().Format(time.RFC3339) + + a, err := db.Sign(asserts.SnapResourceRevisionType, headers, nil, systestkeys.TestStoreKeyID) + if err != nil { + return "", err + } + return writeAssert(a, targetDir) +} + +func NewSnapResourcePair(targetDir string, compPath string, headers map[string]interface{}) (string, error) { + db, err := newAssertsDB(systestkeys.TestStorePrivKey) + if err != nil { + return "", err + } + + container, err := snapfile.Open(compPath) + if err != nil { + return "", err + } + + ci, err := snap.ReadComponentInfoFromContainer(container, nil, nil) + if err != nil { + return "", err + } + + required := []string{"snap-id", "resource-revision", "snap-revision"} + for _, r := range required { + if _, ok := headers[r]; !ok { + return "", fmt.Errorf("missing required header %q", r) + } + } + + defaults := map[string]interface{}{ + "type": "snap-resource-pair", + "authority-id": "testrootorg", + "developer-id": "testrootorg", + "resource-name": ci.Component.ComponentName, + "timestamp": time.Now().Format(time.RFC3339), + } + for k, v := range defaults { + if _, ok := headers[k]; !ok { + headers[k] = v + } + } + + a, err := db.Sign(asserts.SnapResourcePairType, headers, nil, systestkeys.TestStoreKeyID) + if err != nil { + return "", err + } + return writeAssert(a, targetDir) +} + func NewSnapDeclaration(targetDir string, snap string, headers map[string]interface{}) (string, error) { db, err := newAssertsDB(systestkeys.TestStorePrivKey) if err != nil { diff --git a/tests/lib/fakestore/store/store.go b/tests/lib/fakestore/store/store.go index dcd04d74503..5f6424b75e7 100644 --- a/tests/lib/fakestore/store/store.go +++ b/tests/lib/fakestore/store/store.go @@ -30,6 +30,7 @@ import ( "net/http" "net/url" "os" + "path" "path/filepath" "regexp" "strconv" @@ -197,7 +198,7 @@ type essentialInfo struct { Base string } -func snapEssentialInfo(fn, snapID string, bs asserts.Backstore, cs *ChannelRepository) (*essentialInfo, error) { +func snapEssentialInfo(fn, snapID string, bs asserts.Backstore) (*essentialInfo, error) { f, err := snapfile.Open(fn) if err != nil { return nil, fmt.Errorf("cannot read: %v: %v", fn, err) @@ -250,6 +251,79 @@ func snapEssentialInfo(fn, snapID string, bs asserts.Backstore, cs *ChannelRepos }, nil } +func addComponentBlobToRevisionSet(snaps map[string]*revisionSet, snapIDs map[string]string, fn string, bs asserts.Backstore) error { + f, err := snapfile.Open(fn) + if err != nil { + return fmt.Errorf("cannot read: %v: %v", fn, err) + } + + info, err := snap.ReadComponentInfoFromContainer(f, nil, nil) + if err != nil { + return fmt.Errorf("cannot get info for: %v: %v", fn, err) + } + + compName := info.Component.ComponentName + snapName := info.Component.SnapName + + digest, _, err := asserts.SnapFileSHA3_384(fn) + if err != nil { + return fmt.Errorf("cannot get digest for: %v: %v", fn, err) + } + + set, ok := snaps[snapName] + if !ok { + return fmt.Errorf("cannot find snap %q for component: %q", snapName, info.Component) + } + + snapID, ok := snapIDs[snapName] + if !ok { + return fmt.Errorf("cannot find snap id for snap %q", snapName) + } + + pk, err := asserts.PrimaryKeyFromHeaders(asserts.SnapResourceRevisionType, map[string]string{ + "snap-id": snapID, + "resource-name": compName, + "resource-sha3-384": digest, + }) + if err != nil { + return err + } + + a, err := bs.Get(asserts.SnapResourceRevisionType, pk, asserts.SnapResourceRevisionType.MaxSupportedFormat()) + if err != nil { + return err + } + compRev := snap.R(a.(*asserts.SnapResourceRevision).ResourceRevision()) + + for snapRev := range set.revisions { + pk, err := asserts.PrimaryKeyFromHeaders(asserts.SnapResourcePairType, map[string]string{ + "resource-name": compName, + "snap-id": snapID, + "resource-revision": compRev.String(), + "snap-revision": snapRev.String(), + }) + if err != nil { + return err + } + + _, err = bs.Get(asserts.SnapResourcePairType, pk, asserts.SnapResourcePairType.MaxSupportedFormat()) + if err != nil { + // no pair assertion for this snap revision, so this one isn't + // associated with this snap revision + if errors.Is(err, &asserts.NotFoundError{}) { + continue + } + return err + } + + if err := set.addComponent(compName, compRev, fn, snapRev); err != nil { + return err + } + } + + return nil +} + type detailsReplyJSON struct { Architectures []string `json:"architecture"` SnapID string `json:"snap_id"` @@ -372,9 +446,9 @@ func (s *Store) detailsEndpoint(w http.ResponseWriter, req *http.Request) { return } - fn := set.getLatest() + sn := set.getLatest() - essInfo, err := snapEssentialInfo(fn, "", bs, s.channelRepository) + essInfo, err := snapEssentialInfo(sn.path, "", bs) if err != nil { http.Error(w, err.Error(), 400) return @@ -386,8 +460,8 @@ func (s *Store) detailsEndpoint(w http.ResponseWriter, req *http.Request) { PackageName: essInfo.Name, Developer: essInfo.DevelName, DeveloperID: essInfo.DeveloperID, - AnonDownloadURL: fmt.Sprintf("%s/download/%s", s.RealURL(req), filepath.Base(fn)), - DownloadURL: fmt.Sprintf("%s/download/%s", s.RealURL(req), filepath.Base(fn)), + AnonDownloadURL: fmt.Sprintf("%s/download/%s", s.RealURL(req), filepath.Base(sn.path)), + DownloadURL: fmt.Sprintf("%s/download/%s", s.RealURL(req), filepath.Base(sn.path)), Version: essInfo.Version, Revision: essInfo.Revision, DownloadDigest: hexify(essInfo.Digest), @@ -407,37 +481,58 @@ func (s *Store) detailsEndpoint(w http.ResponseWriter, req *http.Request) { } type revisionSet struct { - latest snap.Revision - containers map[snap.Revision]string + latest snap.Revision + revisions map[snap.Revision]availableSnap +} + +type availableSnap struct { + path string + components map[string]availableComponent +} + +type availableComponent struct { + path string + revision snap.Revision } -func (rs *revisionSet) get(rev snap.Revision) (string, bool) { +func (rs *revisionSet) get(rev snap.Revision) (availableSnap, bool) { if rev.Unset() { rev = rs.latest } - path, ok := rs.containers[rev] - return path, ok + sn, ok := rs.revisions[rev] + return sn, ok } -func (rs *revisionSet) getLatest() string { - path, ok := rs.containers[rs.latest] +func (rs *revisionSet) getLatest() availableSnap { + sn, ok := rs.revisions[rs.latest] if !ok { panic("internal error: revision set should always contain latest revision") } - return path + return sn } func (rs *revisionSet) add(rev snap.Revision, path string) { - if rs.containers == nil { - rs.containers = make(map[snap.Revision]string) + if rs.revisions == nil { + rs.revisions = make(map[snap.Revision]availableSnap) } if rs.latest.N < rev.N { rs.latest = rev } - rs.containers[rev] = path + rs.revisions[rev] = availableSnap{path: path, components: make(map[string]availableComponent)} +} + +func (rs *revisionSet) addComponent(name string, compRev snap.Revision, path string, snapRev snap.Revision) error { + sn, ok := rs.revisions[snapRev] + if !ok { + return fmt.Errorf("cannot find snap revision %q", snapRev) + } + + sn.components[name] = availableComponent{path: path, revision: compRev} + + return nil } func (s *Store) collectSnaps(bs asserts.Backstore) (map[string]*revisionSet, error) { @@ -450,11 +545,12 @@ func (s *Store) collectSnaps(bs asserts.Backstore) (map[string]*revisionSet, err defer restoreSanitize() snaps := make(map[string]*revisionSet) + snapNamesToID := make(map[string]string, len(snapFns)) for _, fn := range snapFns { - // we only care about the revision here, so we can get away without - // setting the id + // if the snap is asserted, then the returned info will contain the ID + // taken from the database const snapID = "" - info, err := snapEssentialInfo(fn, snapID, bs, s.channelRepository) + info, err := snapEssentialInfo(fn, snapID, bs) if err != nil { return nil, err } @@ -469,6 +565,7 @@ func (s *Store) collectSnaps(bs asserts.Backstore) (map[string]*revisionSet, err if err != nil { return nil, err } + for _, channel := range channels { compositeName := fmt.Sprintf("%s|%s", info.Name, channel) if _, ok := snaps[compositeName]; !ok { @@ -477,9 +574,24 @@ func (s *Store) collectSnaps(bs asserts.Backstore) (map[string]*revisionSet, err snaps[compositeName].add(snap.R(info.Revision), fn) } + if info.SnapID != "" { + snapNamesToID[info.Name] = info.SnapID + } + logger.Debugf("found snap %q (revision %d) at %v", info.Name, info.Revision, fn) } + compFns, err := filepath.Glob(filepath.Join(s.blobDir, "*.comp")) + if err != nil { + return nil, err + } + + for _, fn := range compFns { + if err := addComponentBlobToRevisionSet(snaps, snapNamesToID, fn, bs); err != nil { + return nil, err + } + } + return snaps, err } @@ -565,9 +677,9 @@ func (s *Store) bulkEndpoint(w http.ResponseWriter, req *http.Request) { continue } - fn := set.getLatest() + sn := set.getLatest() - essInfo, err := snapEssentialInfo(fn, pkg.SnapID, bs, s.channelRepository) + essInfo, err := snapEssentialInfo(sn.path, pkg.SnapID, bs) if err != nil { http.Error(w, err.Error(), 400) return @@ -579,8 +691,8 @@ func (s *Store) bulkEndpoint(w http.ResponseWriter, req *http.Request) { PackageName: essInfo.Name, Developer: essInfo.DevelName, DeveloperID: essInfo.DeveloperID, - DownloadURL: fmt.Sprintf("%s/download/%s", s.RealURL(req), filepath.Base(fn)), - AnonDownloadURL: fmt.Sprintf("%s/download/%s", s.RealURL(req), filepath.Base(fn)), + DownloadURL: fmt.Sprintf("%s/download/%s", s.RealURL(req), filepath.Base(sn.path)), + AnonDownloadURL: fmt.Sprintf("%s/download/%s", s.RealURL(req), filepath.Base(sn.path)), Version: essInfo.Version, Revision: essInfo.Revision, DownloadDigest: hexify(essInfo.Digest), @@ -681,15 +793,26 @@ type detailsResultV2 struct { ID string `json:"id"` Username string `json:"username"` } `json:"publisher"` - Download struct { - URL string `json:"url"` - Sha3_384 string `json:"sha3-384"` - Size uint64 `json:"size"` - } `json:"download"` - Version string `json:"version"` - Revision int `json:"revision"` - Confinement string `json:"confinement"` - Type string `json:"type"` + Download downloadInfo `json:"download"` + Version string `json:"version"` + Revision int `json:"revision"` + Confinement string `json:"confinement"` + Type string `json:"type"` + Resources []snapResourceResult `json:"resources,omitempty"` +} + +type downloadInfo struct { + URL string `json:"url"` + Sha3_384 string `json:"sha3-384"` + Size uint64 `json:"size"` +} + +type snapResourceResult struct { + Download downloadInfo `json:"download"` + Type string `json:"type"` + Name string `json:"name"` + Revision int `json:"revision"` + Version string `json:"version"` } func (s *Store) snapActionEndpoint(w http.ResponseWriter, req *http.Request) { @@ -774,39 +897,78 @@ func (s *Store) snapActionEndpoint(w http.ResponseWriter, req *http.Request) { continue } - fn, ok := set.get(snap.R(a.Revision)) + sn, ok := set.get(snap.R(a.Revision)) if !ok { // TODO: this should send back some error? continue } - essInfo, err := snapEssentialInfo(fn, snapID, bs, s.channelRepository) + essInfo, err := snapEssentialInfo(sn.path, snapID, bs) if err != nil { http.Error(w, err.Error(), 400) return } + resources := make([]snapResourceResult, 0, len(sn.components)) + for compName, comp := range sn.components { + f, err := snapfile.Open(path.Join(comp.path)) + if err != nil { + http.Error(w, fmt.Sprintf("cannot read: %v: %v", compName, err), 400) + return + } + + digest, size, err := asserts.SnapFileSHA3_384(comp.path) + if err != nil { + http.Error(w, fmt.Sprintf("cannot get digest for: %v: %v", compName, err), 400) + return + } + + compInfo, err := snap.ReadComponentInfoFromContainer(f, nil, nil) + if err != nil { + http.Error(w, fmt.Sprintf("cannot get info for: %v: %v", compName, err), 400) + return + } + + resources = append(resources, snapResourceResult{ + Name: compName, + Revision: comp.revision.N, + Type: fmt.Sprintf("component/%s", compInfo.Type), + Version: compInfo.Version(essInfo.Version), + Download: downloadInfo{ + URL: fmt.Sprintf("%s/download/%s", s.RealURL(req), filepath.Base(comp.path)), + Sha3_384: hexify(digest), + Size: size, + }, + }) + } + + details := detailsResultV2{ + Architectures: []string{"all"}, + SnapID: essInfo.SnapID, + Name: essInfo.Name, + Version: essInfo.Version, + Revision: essInfo.Revision, + Confinement: essInfo.Confinement, + Type: essInfo.Type, + Base: essInfo.Base, + } + if len(resources) > 0 { + details.Resources = resources + } + res := &snapActionResult{ Result: a.Action, InstanceKey: a.InstanceKey, SnapID: essInfo.SnapID, Name: essInfo.Name, - Snap: detailsResultV2{ - Architectures: []string{"all"}, - SnapID: essInfo.SnapID, - Name: essInfo.Name, - Version: essInfo.Version, - Revision: essInfo.Revision, - Confinement: essInfo.Confinement, - Type: essInfo.Type, - Base: essInfo.Base, - }, + Snap: details, } + logger.Debugf("requested snap %q revision %d", essInfo.Name, a.Revision) res.Snap.Publisher.ID = essInfo.DeveloperID res.Snap.Publisher.Username = essInfo.DevelName - res.Snap.Download.URL = fmt.Sprintf("%s/download/%s", s.RealURL(req), filepath.Base(fn)) + res.Snap.Download.URL = fmt.Sprintf("%s/download/%s", s.RealURL(req), filepath.Base(sn.path)) res.Snap.Download.Sha3_384 = hexify(essInfo.Digest) res.Snap.Download.Size = essInfo.Size replyData.Results = append(replyData.Results, res) @@ -971,14 +1133,12 @@ func (s *Store) nonceEndpoint(w http.ResponseWriter, req *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) w.Write([]byte(`{"nonce": "blah"}`)) - return } func (s *Store) sessionEndpoint(w http.ResponseWriter, req *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) w.Write([]byte(`{"macaroon": "blahblah"}`)) - return } type ChannelRepository struct { @@ -987,20 +1147,19 @@ type ChannelRepository struct { func (cr *ChannelRepository) findSnapChannels(snapDigest string) ([]string, error) { dataPath := filepath.Join(cr.rootDir, snapDigest) - fd, err := os.Open(dataPath) + f, err := os.Open(dataPath) if err != nil { - if os.IsNotExist(err) { + if errors.Is(err, os.ErrNotExist) { return nil, nil - } else { - return nil, err } - } else { - defer fd.Close() - sc := bufio.NewScanner(fd) - var lines []string - for sc.Scan() { - lines = append(lines, sc.Text()) - } - return lines, nil + return nil, err + } + defer f.Close() + + sc := bufio.NewScanner(f) + var lines []string + for sc.Scan() { + lines = append(lines, sc.Text()) } + return lines, nil } diff --git a/tests/lib/fakestore/store/store_test.go b/tests/lib/fakestore/store/store_test.go index c173e4ce88c..1e1357391ba 100644 --- a/tests/lib/fakestore/store/store_test.go +++ b/tests/lib/fakestore/store/store_test.go @@ -358,6 +358,14 @@ func (s *storeTestSuite) makeTestSnap(c *C, snapYamlContent string) string { return dst } +func (s *storeTestSuite) makeTestComponent(c *C, yaml string) string { + fn := snaptest.MakeTestComponent(c, yaml) + dst := filepath.Join(s.store.blobDir, filepath.Base(fn)) + err := osutil.CopyFile(fn, dst, 0) + c.Assert(err, IsNil) + return dst +} + var ( tSnapDecl = template.Must(template.New("snap-decl").Parse(`type: snap-declaration authority-id: testrootorg @@ -391,6 +399,32 @@ validation: unproven timestamp: 2016-08-19T19:19:19Z sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij +AXNpZw= +`)) + tResourceRevision = template.Must(template.New("resource-revision").Parse(`type: snap-resource-revision +authority-id: testrootorg +snap-id: {{.SnapID}} +resource-name: {{.Name}} +resource-size: {{.Size}} +resource-sha3-384: {{.Digest}} +resource-revision: {{.Revision}} +developer-id: {{.DeveloperID}} +snap-name: {{.Name}} +timestamp: 2016-08-19T19:19:19Z +sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij + +AXNpZw= +`)) + tResourcePair = template.Must(template.New("resource-pair").Parse(`type: snap-resource-pair +authority-id: testrootorg +snap-id: {{.SnapID}} +resource-name: {{.Name}} +resource-revision: {{.Revision}} +snap-revision: {{.SnapRevision}} +developer-id: {{.DeveloperID}} +timestamp: 2016-08-19T19:19:19Z +sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij + AXNpZw= `)) ) @@ -436,6 +470,41 @@ func (s *storeTestSuite) addToChannel(c *C, snapFn, channel string) { fmt.Fprintf(f, "%s\n", channel) } +func (s *storeTestSuite) makeComponentAssertions(c *C, fn, name, snapID, develID string, compRev, snapRev int) { + type essentialComponentInfo struct { + Name string + SnapID string + DeveloperID string + Revision int + SnapRevision int + Digest string + Size uint64 + } + + digest, size, err := asserts.SnapFileSHA3_384(fn) + c.Assert(err, IsNil) + + info := essentialComponentInfo{ + Name: name, + SnapID: snapID, + DeveloperID: develID, + Revision: compRev, + SnapRevision: snapRev, + Digest: digest, + Size: size, + } + + f, err := os.OpenFile(filepath.Join(s.store.assertDir, fmt.Sprintf("%s+%s.fake.snap-resource-revison", snapID, name)), os.O_CREATE|os.O_WRONLY, 0644) + c.Assert(err, IsNil) + err = tResourceRevision.Execute(f, info) + c.Assert(err, IsNil) + + f, err = os.OpenFile(filepath.Join(s.store.assertDir, fmt.Sprintf("%s+%s+%d.fake.snap-resource-pair", snapID, name, snapRev)), os.O_CREATE|os.O_WRONLY, 0644) + c.Assert(err, IsNil) + err = tResourcePair.Execute(f, info) + c.Assert(err, IsNil) +} + func (s *storeTestSuite) TestMakeTestSnap(c *C) { snapFn := s.makeTestSnap(c, "name: foo\nversion: 1") c.Assert(osutil.FileExists(snapFn), Equals, true) @@ -452,6 +521,12 @@ func (s *storeTestSuite) TestCollectSnaps(c *C) { fn = s.makeTestSnap(c, "name: bar\nversion: 3") s.makeAssertions(c, fn, "bar", snaptest.AssertedSnapID("bar"), "devel", "devel-id", 7) + fn = s.makeTestComponent(c, "component: foo+comp1\nversion: 4\ntype: standard") + + // same component is shared across two snap revisions + s.makeComponentAssertions(c, fn, "comp1", snaptest.AssertedSnapID("foo"), "devel-id", 8, 5) + s.makeComponentAssertions(c, fn, "comp1", snaptest.AssertedSnapID("foo"), "devel-id", 8, 6) + bs, err := s.store.collectAssertions() c.Assert(err, IsNil) @@ -460,15 +535,34 @@ func (s *storeTestSuite) TestCollectSnaps(c *C) { c.Assert(snaps, DeepEquals, map[string]*revisionSet{ "foo": { latest: snap.R(6), - containers: map[snap.Revision]string{ - snap.R(5): filepath.Join(s.store.blobDir, "foo_1_all.snap"), - snap.R(6): filepath.Join(s.store.blobDir, "foo_2_all.snap"), + revisions: map[snap.Revision]availableSnap{ + snap.R(5): { + path: filepath.Join(s.store.blobDir, "foo_1_all.snap"), + components: map[string]availableComponent{ + "comp1": { + path: filepath.Join(s.store.blobDir, "foo+comp1.comp"), + revision: snap.R(8), + }, + }, + }, + snap.R(6): { + path: filepath.Join(s.store.blobDir, "foo_2_all.snap"), + components: map[string]availableComponent{ + "comp1": { + path: filepath.Join(s.store.blobDir, "foo+comp1.comp"), + revision: snap.R(8), + }, + }, + }, }, }, "bar": { latest: snap.R(7), - containers: map[snap.Revision]string{ - snap.R(7): filepath.Join(s.store.blobDir, "bar_3_all.snap"), + revisions: map[snap.Revision]availableSnap{ + snap.R(7): { + path: filepath.Join(s.store.blobDir, "bar_3_all.snap"), + components: make(map[string]availableComponent), + }, }, }, }) @@ -858,6 +952,104 @@ func (s *storeTestSuite) TestSnapActionEndpointAssertedWithRevision(c *C) { request(snap.R(6), "2", latestFn) } +func (s *storeTestSuite) TestSnapActionEndpointAssertedWithComponents(c *C) { + snapWithoutComp := s.makeTestSnap(c, "name: test-snapd-tools\nversion: 1") + s.makeAssertions(c, snapWithoutComp, "test-snapd-tools", "eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw", "canonical", "canonical", 5) + + snapWithcomp := s.makeTestSnap(c, "name: test-snapd-tools\nversion: 2") + s.makeAssertions(c, snapWithcomp, "test-snapd-tools", "eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw", "canonical", "canonical", 6) + + componentFn := s.makeTestComponent(c, "component: test-snapd-tools+comp1\nversion: 4\ntype: standard") + s.makeComponentAssertions(c, componentFn, "comp1", "eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw", "canonical", 8, 6) + + compDigest, compSize, err := asserts.SnapFileSHA3_384(componentFn) + c.Assert(err, IsNil) + + type availableComponent struct { + path string + digest string + size uint64 + revision snap.Revision + version string + } + + request := func(rev snap.Revision, version string, path string, comps map[string]availableComponent) { + post := fmt.Sprintf(`{ + "context": [{"instance-key":"eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw","snap-id":"eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw","tracking-channel":"stable","revision":1}], + "actions": [{"action":"refresh","instance-key":"eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw","snap-id":"eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw", "revision":%d}] + }`, rev.N) + + resp, err := s.StorePostJSON("/v2/snaps/refresh", []byte(post)) + c.Assert(err, IsNil) + defer resp.Body.Close() + + c.Assert(resp.StatusCode, Equals, 200) + var body struct { + Results []map[string]interface{} + } + c.Assert(json.NewDecoder(resp.Body).Decode(&body), IsNil) + c.Check(body.Results, HasLen, 1) + sha3_384, size := getSha(path) + + payload := map[string]interface{}{ + "result": "refresh", + "instance-key": "eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw", + "snap-id": "eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw", + "name": "test-snapd-tools", + "snap": map[string]interface{}{ + "architectures": []interface{}{"all"}, + "snap-id": "eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw", + "name": "test-snapd-tools", + "publisher": map[string]interface{}{ + "username": "canonical", + "id": "canonical", + }, + "download": map[string]interface{}{ + "url": s.store.URL() + "/download/" + filepath.Base(path), + "sha3-384": sha3_384, + "size": float64(size), + }, + "version": version, + "revision": float64(rev.N), + "confinement": "strict", + "type": "app", + }, + } + + var resources []interface{} + for name, comp := range comps { + resources = append(resources, map[string]interface{}{ + "download": map[string]interface{}{ + "url": s.store.URL() + "/download/" + filepath.Base(comp.path), + "sha3-384": comp.digest, + "size": float64(comp.size), + }, + "type": "component/standard", + "name": name, + "revision": float64(comp.revision.N), + "version": comp.version, + }) + } + + if len(resources) > 0 { + payload["snap"].(map[string]interface{})["resources"] = resources + } + + c.Check(body.Results[0], DeepEquals, payload) + } + + request(snap.R(5), "1", snapWithoutComp, map[string]availableComponent{}) + request(snap.R(6), "2", snapWithcomp, map[string]availableComponent{ + "comp1": { + path: componentFn, + digest: hexify(compDigest), + size: compSize, + revision: snap.R(8), + version: "4", + }, + }) +} + func (s *storeTestSuite) TestSnapActionEndpointWithAssertions(c *C) { snapFn := s.makeTestSnap(c, "name: foo\nversion: 10") s.makeAssertions(c, snapFn, "foo", "xidididididididididididididididid", "foo-devel", "foo-devel-id", 99) diff --git a/tests/lib/tools/store-state b/tests/lib/tools/store-state index 05c077e76b7..66aca2c6f69 100755 --- a/tests/lib/tools/store-state +++ b/tests/lib/tools/store-state @@ -10,6 +10,7 @@ show_help() { echo " store-state setup-staging-store" echo " store-state teardown-staging-store" echo " store-state make-snap-installable [--noack ] [--extra-decl-json FILE] [SNAP_ID]" + echo " store-state make-component-installable --snap-id --component-revision --snap-revision [--noack ] " echo " store-state init-fake-refreshes " echo " store-state add-to-channel " } @@ -124,6 +125,85 @@ EOF rm -f /tmp/snap-decl.json /tmp/snap-rev.json } + +make_component_installable(){ + local ack=true + local component_rev=""; + local snap_rev=""; + local snap_id=""; + while [ $# -gt 0 ]; do + case "$1" in + (--component-revision) + component_rev="$2" + shift 2 + ;; + (--snap-id) + snap_id="$2" + shift 2 + ;; + (--snap-revision) + snap_rev="$2" + shift 2 + ;; + (--noack) + ack=false + shift + ;; + (*) + break + ;; + esac + done + + if [ -z "${snap_id}" ]; then + echo "snap-id must be provided" + return 1 + fi + + if [ -z "${component_rev}" ]; then + echo "component-revision must be provided" + return 1 + fi + + if [ -z "${snap_rev}" ]; then + echo "snap-revision must be provided" + return 1 + fi + + local dir="$1" + local path="$2" + + work=$(mktemp -d) + + cat > "/${work}/snap-resource-revision.json" << EOF +{ + "snap-id": "${snap_id}", + "publisher-id": "developer1", + "resource-revision": "${component_rev}" +} +EOF + + cat > "/${work}/snap-resource-pair.json" << EOF +{ + "snap-id": "${snap_id}", + "publisher-id": "developer1", + "resource-revision": "${component_rev}", + "snap-revision": "${snap_rev}" +} +EOF + + resource_rev_assert=$(fakestore new-snap-resource-revision --dir "${dir}" "${path}" "/${work}/snap-resource-revision.json") + resource_pair_assert=$(fakestore new-snap-resource-pair --dir "${dir}" "${path}" "/${work}/snap-resource-pair.json") + + if [ "${ack}" = "true" ]; then + snap ack "${resource_rev_assert}" + snap ack "${resource_pair_assert}" + fi + + cp -av "${path}" "${dir}/" + rm -rf "${work}" +} + setup_fake_store(){ local top_dir=$1