Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tests/lib/fakestore: add support for tracking channels #14840

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing a defer fd.Close() in here.

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing a f.Close here.

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
Loading