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