Skip to content

Commit

Permalink
tests/lib/fakestore: add support for tracking channels (#14840)
Browse files Browse the repository at this point in the history
* tests/lib/fakestore: add support for tracking channels

* tests/lib/tools/store-state: add add-to-channel subcommand

* tests/lib/fakestore/store: add tests for channel feature
  • Loading branch information
valentindavid authored Dec 18, 2024
1 parent 2d3d2a1 commit 1f677f1
Show file tree
Hide file tree
Showing 3 changed files with 219 additions and 14 deletions.
112 changes: 98 additions & 14 deletions tests/lib/fakestore/store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
package store

import (
"bufio"
"context"
"encoding/base64"
"encoding/json"
Expand Down Expand Up @@ -68,6 +69,8 @@ type Store struct {
fallback *store.Store

srv *http.Server

channelRepository *ChannelRepository
}

// NewStore creates a new store server serving snaps from the given top directory and assertions from topDir/asserts. If assertFallback is true missing assertions are looked up in the main online store.
Expand All @@ -90,13 +93,20 @@ func NewStore(topDir, addr string, assertFallback bool) *Store {
Addr: addr,
Handler: mux,
},
channelRepository: &ChannelRepository{
rootDir: filepath.Join(topDir, "channels"),
},
}

mux.HandleFunc("/", rootEndpoint)
mux.HandleFunc("/api/v1/snaps/search", store.searchEndpoint)
mux.HandleFunc("/api/v1/snaps/details/", store.detailsEndpoint)
mux.HandleFunc("/api/v1/snaps/metadata", store.bulkEndpoint)
mux.Handle("/download/", http.StripPrefix("/download/", http.FileServer(http.Dir(topDir))))

mux.HandleFunc("/api/v1/snaps/auth/nonces", store.nonceEndpoint)
mux.HandleFunc("/api/v1/snaps/auth/sessions", store.sessionEndpoint)

// v2
mux.HandleFunc("/v2/assertions/", store.assertionsEndpoint)
mux.HandleFunc("/v2/snaps/refresh", store.snapActionEndpoint)
Expand All @@ -111,6 +121,14 @@ func (s *Store) URL() string {
return s.url
}

func (s *Store) RealURL(req *http.Request) string {
if req.Host == "" {
return s.url
} else {
return fmt.Sprintf("http://%s", req.Host)
}
}

func (s *Store) SnapsDir() string {
return s.blobDir
}
Expand Down Expand Up @@ -179,7 +197,7 @@ type essentialInfo struct {
Base string
}

func snapEssentialInfo(fn, snapID string, bs asserts.Backstore) (*essentialInfo, error) {
func snapEssentialInfo(fn, snapID string, bs asserts.Backstore, cs *ChannelRepository) (*essentialInfo, error) {
f, err := snapfile.Open(fn)
if err != nil {
return nil, fmt.Errorf("cannot read: %v: %v", fn, err)
Expand Down Expand Up @@ -356,7 +374,7 @@ func (s *Store) detailsEndpoint(w http.ResponseWriter, req *http.Request) {

fn := set.getLatest()

essInfo, err := snapEssentialInfo(fn, "", bs)
essInfo, err := snapEssentialInfo(fn, "", bs, s.channelRepository)
if err != nil {
http.Error(w, err.Error(), 400)
return
Expand All @@ -368,8 +386,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.URL(), filepath.Base(fn)),
DownloadURL: fmt.Sprintf("%s/download/%s", s.URL(), 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(fn)),
Version: essInfo.Version,
Revision: essInfo.Revision,
DownloadDigest: hexify(essInfo.Digest),
Expand Down Expand Up @@ -436,7 +454,7 @@ func (s *Store) collectSnaps(bs asserts.Backstore) (map[string]*revisionSet, err
// we only care about the revision here, so we can get away without
// setting the id
const snapID = ""
info, err := snapEssentialInfo(fn, snapID, bs)
info, err := snapEssentialInfo(fn, snapID, bs, s.channelRepository)
if err != nil {
return nil, err
}
Expand All @@ -447,6 +465,18 @@ func (s *Store) collectSnaps(bs asserts.Backstore) (map[string]*revisionSet, err

snaps[info.Name].add(snap.R(info.Revision), fn)

channels, err := s.channelRepository.findSnapChannels(info.Digest)
if err != nil {
return nil, err
}
for _, channel := range channels {
compositeName := fmt.Sprintf("%s|%s", info.Name, channel)
if _, ok := snaps[compositeName]; !ok {
snaps[compositeName] = &revisionSet{}
}
snaps[compositeName].add(snap.R(info.Revision), fn)
}

logger.Debugf("found snap %q (revision %d) at %v", info.Name, info.Revision, fn)
}

Expand Down Expand Up @@ -537,7 +567,7 @@ func (s *Store) bulkEndpoint(w http.ResponseWriter, req *http.Request) {

fn := set.getLatest()

essInfo, err := snapEssentialInfo(fn, pkg.SnapID, bs)
essInfo, err := snapEssentialInfo(fn, pkg.SnapID, bs, s.channelRepository)
if err != nil {
http.Error(w, err.Error(), 400)
return
Expand All @@ -549,8 +579,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.URL(), filepath.Base(fn)),
AnonDownloadURL: fmt.Sprintf("%s/download/%s", s.URL(), 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(fn)),
Version: essInfo.Version,
Revision: essInfo.Revision,
DownloadDigest: hexify(essInfo.Digest),
Expand Down Expand Up @@ -610,8 +640,9 @@ func (s *Store) collectAssertions() (asserts.Backstore, error) {
}

type currentSnap struct {
SnapID string `json:"snap-id"`
InstanceKey string `json:"instance-key"`
SnapID string `json:"snap-id"`
InstanceKey string `json:"instance-key"`
TrackingChannel string `json:"tracking-channel"`
}

type snapAction struct {
Expand All @@ -620,6 +651,7 @@ type snapAction struct {
SnapID string `json:"snap-id"`
Name string `json:"name"`
Revision int `json:"revision,omitempty"`
Channel string `json:"channel,omitempty"`
}

type snapActionRequest struct {
Expand Down Expand Up @@ -702,6 +734,7 @@ func (s *Store) snapActionEndpoint(w http.ResponseWriter, req *http.Request) {
Action: "refresh",
SnapID: s.SnapID,
InstanceKey: s.InstanceKey,
Channel: s.TrackingChannel,
}
}
}
Expand All @@ -723,8 +756,21 @@ func (s *Store) snapActionEndpoint(w http.ResponseWriter, req *http.Request) {
return
}

set, ok := snaps[name]
if !ok {
var set *revisionSet
var foundSnap bool
if a.Channel != "" {
set, foundSnap = snaps[fmt.Sprintf("%s|%s", name, a.Channel)]
}
if !foundSnap {
// FIXME: It is possible that many tests do
// not use channels correctly. So we have to
// fallback to searching for snaps by just
// name, without channel. Maybe we should
// remove that, and fix all the tests instead.
set, foundSnap = snaps[name]
}

if !foundSnap {
continue
}

Expand All @@ -735,7 +781,7 @@ func (s *Store) snapActionEndpoint(w http.ResponseWriter, req *http.Request) {
continue
}

essInfo, err := snapEssentialInfo(fn, snapID, bs)
essInfo, err := snapEssentialInfo(fn, snapID, bs, s.channelRepository)
if err != nil {
http.Error(w, err.Error(), 400)
return
Expand All @@ -760,7 +806,7 @@ func (s *Store) snapActionEndpoint(w http.ResponseWriter, req *http.Request) {
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.URL(), filepath.Base(fn))
res.Snap.Download.URL = fmt.Sprintf("%s/download/%s", s.RealURL(req), filepath.Base(fn))
res.Snap.Download.Sha3_384 = hexify(essInfo.Digest)
res.Snap.Download.Size = essInfo.Size
replyData.Results = append(replyData.Results, res)
Expand Down Expand Up @@ -920,3 +966,41 @@ func findSnapRevision(snapDigest string, bs asserts.Backstore) (*asserts.SnapRev

return snapRev, devAcct, nil
}

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 {
rootDir string
}

func (cr *ChannelRepository) findSnapChannels(snapDigest string) ([]string, error) {
dataPath := filepath.Join(cr.rootDir, snapDigest)
fd, err := os.Open(dataPath)
if err != nil {
if os.IsNotExist(err) {
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
}
}
111 changes: 111 additions & 0 deletions tests/lib/fakestore/store/store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ func (s *storeTestSuite) SetUpTest(c *C) {
topdir := c.MkDir()
err := os.Mkdir(filepath.Join(topdir, "asserts"), 0755)
c.Assert(err, IsNil)
err = os.Mkdir(filepath.Join(topdir, "channels"), 0755)
c.Assert(err, IsNil)
s.store = NewStore(topdir, "localhost:0", false)
err = s.store.Start()
c.Assert(err, IsNil)
Expand Down Expand Up @@ -423,6 +425,17 @@ func (s *storeTestSuite) makeAssertions(c *C, snapFn, name, snapID, develName, d
c.Assert(err, IsNil)
}

func (s *storeTestSuite) addToChannel(c *C, snapFn, channel string) {
dgst, _, err := asserts.SnapFileSHA3_384(snapFn)
c.Assert(err, IsNil)

f, err := os.OpenFile(filepath.Join(s.store.blobDir, "channels", dgst), os.O_CREATE|os.O_WRONLY, 0644)
c.Assert(err, IsNil)
defer f.Close()

fmt.Fprintf(f, "%s\n", channel)
}

func (s *storeTestSuite) TestMakeTestSnap(c *C) {
snapFn := s.makeTestSnap(c, "name: foo\nversion: 1")
c.Assert(osutil.FileExists(snapFn), Equals, true)
Expand Down Expand Up @@ -693,6 +706,104 @@ func (s *storeTestSuite) TestSnapActionEndpointUsesLatest(c *C) {
})
}

func (s *storeTestSuite) TestSnapActionEndpointChannel(c *C) {
snapFn := s.makeTestSnap(c, "name: test-snapd-tools\nversion: 1")
s.makeAssertions(c, snapFn, "test-snapd-tools", "eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw", "canonical", "canonical", 1)
s.addToChannel(c, snapFn, "latest/stable")

snapFnEdge := s.makeTestSnap(c, "name: test-snapd-tools\nversion: 2")
s.makeAssertions(c, snapFnEdge, "test-snapd-tools", "eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw", "canonical", "canonical", 2)
s.addToChannel(c, snapFnEdge, "latest/edge")

resp, err := s.StorePostJSON("/v2/snaps/refresh", []byte(`{
"context": [{"instance-key":"eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw","snap-id":"eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw","tracking-channel":"latest/stable","revision":1}],
"actions": [{"action":"refresh","instance-key":"eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw","snap-id":"eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw","channel":"latest/stable"}]
}`))
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(snapFn)
c.Check(body.Results[0], DeepEquals, 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/test-snapd-tools_1_all.snap",
"sha3-384": sha3_384,
"size": float64(size),
},
"version": "1",
"revision": float64(1),
"confinement": "strict",
"type": "app",
},
})
}

func (s *storeTestSuite) TestSnapActionEndpointChannelRefreshAll(c *C) {
snapFn := s.makeTestSnap(c, "name: test-snapd-tools\nversion: 1")
s.makeAssertions(c, snapFn, "test-snapd-tools", "eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw", "canonical", "canonical", 1)
s.addToChannel(c, snapFn, "latest/stable")

snapFnEdge := s.makeTestSnap(c, "name: test-snapd-tools\nversion: 2")
s.makeAssertions(c, snapFnEdge, "test-snapd-tools", "eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw", "canonical", "canonical", 2)
s.addToChannel(c, snapFnEdge, "latest/edge")

resp, err := s.StorePostJSON("/v2/snaps/refresh", []byte(`{
"context": [{"instance-key":"eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw","snap-id":"eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw","tracking-channel":"latest/stable","revision":1}],
"actions": [{"action":"refresh-all"}]
}`))
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(snapFn)
c.Check(body.Results[0], DeepEquals, 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/test-snapd-tools_1_all.snap",
"sha3-384": sha3_384,
"size": float64(size),
},
"version": "1",
"revision": float64(1),
"confinement": "strict",
"type": "app",
},
})
}

func (s *storeTestSuite) TestSnapActionEndpointAssertedWithRevision(c *C) {
oldFn := s.makeTestSnap(c, "name: test-snapd-tools\nversion: 1")
s.makeAssertions(c, oldFn, "test-snapd-tools", "eFe8BTR5L5V9F7yHeMAPxkEr2NdUXMtw", "canonical", "canonical", 5)
Expand Down
10 changes: 10 additions & 0 deletions tests/lib/tools/store-state
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ show_help() {
echo " store-state teardown-staging-store"
echo " store-state make-snap-installable [--noack ] [--extra-decl-json FILE] <DIR> <SNAP_PATH> [SNAP_ID]"
echo " store-state init-fake-refreshes <DIR>"
echo " store-state add-to-channel <DIR> <FILENAME> <CHANNEL>"
}

_configure_store_backends(){
Expand Down Expand Up @@ -182,6 +183,15 @@ teardown_fake_store(){
fi
}

add_to_channel() {
local BLOB_DIR=$1
local FILENAME=$2
local CHANNEL=$3
SUM="$(snap info --verbose "$(realpath "${FILENAME}")" | sed '/^sha3-384: */{;s///;q;};d')"
mkdir -p "${BLOB_DIR}/channels"
echo "${CHANNEL}" >>"${BLOB_DIR}/channels/${SUM}"
}

main() {
if [ $# -eq 0 ]; then
show_help
Expand Down

0 comments on commit 1f677f1

Please sign in to comment.