Merge lp:~chipaca/snappy/auth into lp:~snappy-dev/snappy/snappy-moved-to-github

Proposed by John Lenton on 2015-10-07
Status: Superseded
Proposed branch: lp:~chipaca/snappy/auth
Merge into: lp:~snappy-dev/snappy/snappy-moved-to-github
Diff against target: 3272 lines (+2280/-356)
22 files modified
daemon/api.go (+148/-162)
daemon/api_test.go (+114/-114)
daemon/crypt/bcrypt.go (+38/-0)
daemon/crypt/crypt.go (+112/-0)
daemon/crypt/crypt_test.go (+95/-0)
daemon/crypt/scrypt.go (+75/-0)
daemon/crypt/sha512crypt.go (+114/-0)
daemon/daemon.go (+22/-8)
daemon/daemon_test.go (+117/-0)
daemon/response.go (+16/-14)
pkg/husk/example_test.go (+54/-0)
pkg/husk/husk.go (+508/-0)
pkg/husk/husk_test.go (+412/-0)
pkg/husk/split_test.go (+61/-0)
pkg/remote/remote.go (+45/-0)
pkg/removed/removed.go (+156/-0)
pkg/removed/removed_test.go (+137/-0)
snappy/parts.go (+2/-2)
snappy/snapp.go (+12/-31)
snappy/sort_test.go (+5/-4)
snappy/systemimage.go (+32/-16)
snappy/systemimage_test.go (+5/-5)
To merge this branch: bzr merge lp:~chipaca/snappy/auth
Reviewer Review Type Date Requested Status
Ubuntu Security Team 2015-10-07 Pending
Snappy Developers 2015-10-07 Pending
Review via email: mp+273683@code.launchpad.net

This proposal has been superseded by a proposal from 2015-10-07.

To post a comment you must log in.
lp:~chipaca/snappy/auth updated on 2015-10-09
746. By John Lenton on 2015-10-07

Merged merged-list into auth.

747. By John Lenton on 2015-10-08

Merged merged-list into auth.

748. By John Lenton on 2015-10-08

Merged merged-list into auth.

749. By John Lenton on 2015-10-08

Merged merged-list into auth.

750. By John Lenton on 2015-10-09

restrict REST API calls w/auth

751. By John Lenton on 2015-10-09

fix for 1.3 not having basicAuth methods on http.Request. Also caught a nasty bug wrt null-termination

752. By John Lenton on 2015-10-09

annotated 1.3's basicAuth with return variable names

Unmerged revisions

752. By John Lenton on 2015-10-09

annotated 1.3's basicAuth with return variable names

751. By John Lenton on 2015-10-09

fix for 1.3 not having basicAuth methods on http.Request. Also caught a nasty bug wrt null-termination

750. By John Lenton on 2015-10-09

restrict REST API calls w/auth

749. By John Lenton on 2015-10-08

Merged merged-list into auth.

748. By John Lenton on 2015-10-08

Merged merged-list into auth.

747. By John Lenton on 2015-10-08

Merged merged-list into auth.

746. By John Lenton on 2015-10-07

Merged merged-list into auth.

745. By John Lenton on 2015-10-07

first pass at auth

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'daemon/api.go'
2--- daemon/api.go 2015-09-29 09:45:15 +0000
3+++ daemon/api.go 2015-10-07 12:49:07 +0000
4@@ -27,13 +27,15 @@
5 "mime/multipart"
6 "net/http"
7 "os"
8+ "path/filepath"
9 "sort"
10- "strconv"
11 "strings"
12
13 "github.com/gorilla/mux"
14
15+ "launchpad.net/snappy/dirs"
16 "launchpad.net/snappy/logger"
17+ "launchpad.net/snappy/pkg/husk"
18 "launchpad.net/snappy/progress"
19 "launchpad.net/snappy/release"
20 "launchpad.net/snappy/snappy"
21@@ -42,6 +44,8 @@
22 var api = []*Command{
23 rootCmd,
24 v1Cmd,
25+ metaIconCmd,
26+ appIconCmd,
27 packagesCmd,
28 packageCmd,
29 packageConfigCmd,
30@@ -62,6 +66,16 @@
31 GET: v1Get,
32 }
33
34+ metaIconCmd = &Command{
35+ Path: "/1.0/icons/{icon}",
36+ GET: metaIconGet,
37+ }
38+
39+ appIconCmd = &Command{
40+ Path: "/1.0/icons/{name}.{origin}/icon",
41+ GET: appIconGet,
42+ }
43+
44 packagesCmd = &Command{
45 Path: "/1.0/packages",
46 GET: getPackagesInfo,
47@@ -116,42 +130,32 @@
48 type metarepo interface {
49 Details(string, string) ([]snappy.Part, error)
50 All() ([]snappy.Part, error)
51-}
52-
53-var newRepo = func() metarepo {
54- return snappy.NewMetaRepository()
55-}
56-
57-var newLocalRepo = func() metarepo {
58- return snappy.NewMetaLocalRepository()
59+ Updates() ([]snappy.Part, error)
60 }
61
62 var newRemoteRepo = func() metarepo {
63 return snappy.NewMetaStoreRepository()
64 }
65
66+var newSystemRepo = func() metarepo {
67+ return snappy.NewSystemImageRepository()
68+}
69+
70 var muxVars = mux.Vars
71
72 func getPackageInfo(c *Command, r *http.Request) Response {
73 vars := muxVars(r)
74 name := vars["name"]
75 origin := vars["origin"]
76- if name == "" || origin == "" {
77- // can't happen, i think? mux won't let it
78- return BadRequest(nil, "missing name or origin")
79- }
80-
81- repo := newRepo()
82- found, err := repo.Details(name, origin)
83- if err != nil {
84- if err == snappy.ErrPackageNotFound {
85- return NotFound
86- }
87-
88- return InternalError(err, "unable to list packages: %v", err)
89- }
90-
91- if len(found) == 0 {
92+
93+ repo := newRemoteRepo()
94+ var part snappy.Part
95+ if parts, _ := repo.Details(name, origin); len(parts) > 0 {
96+ part = parts[0]
97+ }
98+
99+ hsk := husk.ByName(name, origin)
100+ if hsk == nil && part == nil {
101 return NotFound
102 }
103
104@@ -165,51 +169,37 @@
105 return InternalError(err, "route can't build URL for package %s.%s: %v", name, origin, err)
106 }
107
108- result := parts2map(found, url.String())
109+ result := webify(hsk.Map(part), url.String())
110
111 return SyncResponse(result)
112 }
113
114-// parts2map takes a slice of parts with the same name and returns a
115-// single map with that part's metadata (including rollback_available
116-// & etc).
117-func parts2map(parts []snappy.Part, resource string) map[string]string {
118- if len(parts) == 0 {
119- return nil
120- }
121-
122- // TODO: handle multiple results in parts; set rollback_available; set update_available
123- part := parts[0]
124- var status string
125- if part.IsInstalled() {
126- if part.IsActive() {
127- status = "active"
128- } else {
129- // can't really happen
130- status = "installed"
131+func webify(result map[string]string, resource string) map[string]string {
132+ result["resource"] = resource
133+
134+ icon := result["icon"]
135+ if icon == "" || strings.HasPrefix(icon, "http") {
136+ return result
137+ }
138+
139+ result["icon"] = ""
140+
141+ var route *mux.Route
142+ var args []string
143+
144+ if strings.HasPrefix(icon, dirs.SnapIconsDir) {
145+ route = metaIconCmd.d.router.Get(metaIconCmd.Path)
146+ args = []string{"icon", icon[len(dirs.SnapIconsDir)+1:]}
147+ } else {
148+ route = appIconCmd.d.router.Get(appIconCmd.Path)
149+ args = []string{"name", result["name"], "origin", result["origin"]}
150+ }
151+
152+ if route != nil {
153+ url, err := route.URL(args...)
154+ if err == nil {
155+ result["icon"] = url.String()
156 }
157- } else {
158- status = "not installed"
159- }
160- // TODO: check for removed and transients (extend the Part interface for removed; check ops for transients)
161-
162- icon := part.Icon()
163- if strings.HasPrefix(icon, iconPath) {
164- icon = iconPrefix + icon[len(iconPath):]
165- }
166-
167- result := map[string]string{
168- "icon": icon,
169- "name": part.Name(),
170- "origin": part.Origin(),
171- "resource": resource,
172- "status": status,
173- "type": string(part.Type()),
174- "vendor": part.Vendor(),
175- "version": part.Version(),
176- "description": part.Description(),
177- "installed_size": strconv.FormatInt(part.InstalledSize(), 10),
178- "download_size": strconv.FormatInt(part.DownloadSize(), 10),
179 }
180
181 return result
182@@ -223,17 +213,6 @@
183 return snappy.QualifiedName(ps[a]) < snappy.QualifiedName(ps[b])
184 }
185
186-func addPart(results map[string]map[string]string, current []snappy.Part, name string, origin string, route *mux.Route) error {
187- url, err := route.URL("name", name, "origin", origin)
188- if err != nil {
189- return err
190- }
191-
192- results[name+"."+origin] = parts2map(current, url.String())
193-
194- return nil
195-}
196-
197 // plural!
198 func getPackagesInfo(c *Command, r *http.Request) Response {
199 route := c.d.router.Get(packageCmd.Path)
200@@ -241,58 +220,58 @@
201 return InternalError(nil, "router can't find route for packages")
202 }
203
204- sources := r.URL.Query().Get("sources")
205- var repo metarepo
206- switch sources {
207- case "local":
208- repo = newLocalRepo()
209- case "remote":
210- repo = newRemoteRepo()
211- default:
212- repo = newRepo()
213- }
214-
215- found, err := repo.All()
216- if err != nil {
217- if err == snappy.ErrPackageNotFound {
218- return NotFound
219- }
220-
221- return InternalError(err, "unable to list packages: %v", err)
222- }
223-
224- if len(found) == 0 {
225- return NotFound
226- }
227+ sources := make([]string, 1, 3)
228+ sources[0] = "local"
229+ // we're not worried if the remote repos error out
230+ found, _ := newRemoteRepo().All()
231+ if len(found) > 0 {
232+ sources = append(sources, "store")
233+ }
234+
235+ upd, _ := newSystemRepo().Updates()
236+ if len(upd) > 0 {
237+ sources = append(sources, "system-image")
238+ }
239+
240+ found = append(found, upd...)
241
242 sort.Sort(byQN(found))
243
244+ husks := husk.All()
245+
246 results := make(map[string]map[string]string)
247- var current []snappy.Part
248- var oldName string
249- var oldOrigin string
250- for i := range found {
251- name := found[i].Name()
252- origin := found[i].Origin()
253- if (name != oldName || origin != oldOrigin) && len(current) > 0 {
254- if err := addPart(results, current, oldName, oldOrigin, route); err != nil {
255- return InternalError(err, "can't get details for %s.%s: %v", oldName, oldOrigin, err)
256- }
257- current = nil
258+ for _, part := range found {
259+ name := part.Name()
260+ origin := part.Origin()
261+
262+ url, err := route.URL("name", name, "origin", origin)
263+ if err != nil {
264+ return InternalError(err, "can't get route to details for %s.%s: %v", name, origin, err)
265 }
266- oldName = name
267- oldOrigin = origin
268- current = append(current, found[i])
269+
270+ fullname := name + "." + origin
271+ qn := snappy.QualifiedName(part)
272+ results[fullname] = webify(husks[qn].Map(part), url.String())
273+ delete(husks, qn)
274 }
275
276- if len(current) > 0 {
277- if err := addPart(results, current, oldName, oldOrigin, route); err != nil {
278- return InternalError(err, "can't get details for %s.%s: %v", oldName, oldOrigin, err)
279+ for _, v := range husks {
280+ m := v.Map(nil)
281+ name := m["name"]
282+ origin := m["origin"]
283+
284+ resource := "no resource URL for this resource"
285+ url, _ := route.URL("name", name, "origin", origin)
286+ if url != nil {
287+ resource = url.String()
288 }
289+
290+ results[name+"."+origin] = webify(m, resource)
291 }
292
293 return SyncResponse(map[string]interface{}{
294 "packages": results,
295+ "sources": sources,
296 "paging": map[string]interface{}{
297 "pages": 1,
298 "page": 1,
299@@ -301,25 +280,6 @@
300 })
301 }
302
303-func getActivePkg(fullname string) ([]snappy.Part, error) {
304- // TODO: below should be rolled into ActiveSnapByName
305- // (specifically: ActiveSnapByname should know about origins)
306- repo := newLocalRepo()
307- all, err := repo.All()
308- if err != nil {
309- return nil, err
310- }
311-
312- parts := all[:0]
313- for _, part := range snappy.FindSnapsByName(fullname, all) {
314- if part.IsActive() {
315- parts = append(parts, part)
316- }
317- }
318-
319- return parts, nil
320-}
321-
322 var findServices = snappy.FindServices
323
324 type svcDesc struct {
325@@ -362,23 +322,20 @@
326 return BadRequest(nil, "unknown action %s", action)
327 }
328
329- parts, err := getActivePkg(pkgName)
330+ h := husk.ByName(name, origin)
331+ idx := h.ActiveIndex()
332+ if idx < 0 {
333+ return NotFound
334+ }
335+
336+ ipart, err := h.Load(idx)
337 if err != nil {
338- return InternalError(err, "unable to get package list: %v", err)
339- }
340-
341- switch len(parts) {
342- default:
343- return InternalError(nil, "found %d active for %s", len(parts), pkgName)
344- case 0:
345- return NotFound
346- case 1:
347- // yay, or something
348- }
349-
350- part, ok := parts[0].(snappy.ServiceYamler)
351+ return InternalError(err, "unable to get load active package: %v", err)
352+ }
353+
354+ part, ok := ipart.(*snappy.SnapPart)
355 if !ok {
356- return InternalError(nil, "active package of type %T does not implement snappy.ServiceYamler", parts[0])
357+ return InternalError(nil, "active package is not a *snappy.SnapPart: %T", ipart)
358 }
359 svcs := part.ServiceYamls()
360
361@@ -460,18 +417,15 @@
362 }
363 pkgName := name + "." + origin
364
365- parts, err := getActivePkg(pkgName)
366+ h := husk.ByName(name, origin)
367+ idx := h.ActiveIndex()
368+ if idx < 0 {
369+ return NotFound
370+ }
371+
372+ part, err := h.Load(idx)
373 if err != nil {
374- return InternalError(err, "unable to get package list: %v", err)
375- }
376-
377- switch len(parts) {
378- default:
379- return InternalError(nil, "found %d active for %s", len(parts), pkgName)
380- case 0:
381- return NotFound
382- case 1:
383- // yay, or something
384+ return InternalError(err, "unable to get load active package: %v", err)
385 }
386
387 bs, err := ioutil.ReadAll(r.Body)
388@@ -479,7 +433,7 @@
389 return BadRequest(err, "reading config request body gave %v", err)
390 }
391
392- config, err := parts[0].Config(bs)
393+ config, err := part.Config(bs)
394 if err != nil {
395 return InternalError(err, "unable to retrieve config for %s: %v", pkgName, err)
396 }
397@@ -741,3 +695,35 @@
398
399 return SyncResponse(logs)
400 }
401+
402+func metaIconGet(c *Command, r *http.Request) Response {
403+ vars := muxVars(r)
404+ name := vars["icon"]
405+
406+ path := filepath.Join(dirs.SnapIconsDir, name)
407+
408+ return FileResponse(path)
409+}
410+
411+func appIconGet(c *Command, r *http.Request) Response {
412+ vars := muxVars(r)
413+ name := vars["name"]
414+ origin := vars["origin"]
415+
416+ hsk := husk.ByName(name, origin)
417+ if hsk == nil || len(hsk.Versions) == 0 {
418+ return NotFound
419+ }
420+
421+ part := hsk.LoadBest()
422+ if part == nil {
423+ return NotFound
424+ }
425+
426+ path := filepath.Clean(part.Icon())
427+ if !strings.HasPrefix(path, dirs.SnapAppsDir) && !strings.HasPrefix(path, dirs.SnapOemDir) {
428+ return BadRequest
429+ }
430+
431+ return FileResponse(path)
432+}
433
434=== modified file 'daemon/api_test.go'
435--- daemon/api_test.go 2015-09-15 12:55:09 +0000
436+++ daemon/api_test.go 2015-10-07 12:49:07 +0000
437@@ -32,11 +32,15 @@
438 "net/http/httptest"
439 "os"
440 "path/filepath"
441+ "strings"
442 "testing"
443 "time"
444
445 "gopkg.in/check.v1"
446
447+ "launchpad.net/snappy/dirs"
448+ "launchpad.net/snappy/pkg"
449+ "launchpad.net/snappy/pkg/husk"
450 "launchpad.net/snappy/progress"
451 "launchpad.net/snappy/release"
452 "launchpad.net/snappy/snappy"
453@@ -62,51 +66,81 @@
454 return s.parts, s.err
455 }
456
457+func (s *apiSuite) Updates() ([]snappy.Part, error) {
458+ return s.parts, s.err
459+}
460+
461 func (s *apiSuite) muxVars(*http.Request) map[string]string {
462 return s.vars
463 }
464
465 func (s *apiSuite) SetUpSuite(c *check.C) {
466- newRepo = func() metarepo {
467+ newRemoteRepo = func() metarepo {
468 return s
469 }
470- newLocalRepo = newRepo
471- newRemoteRepo = newRepo
472+ newSystemRepo = newRemoteRepo
473 muxVars = s.muxVars
474 }
475
476 func (s *apiSuite) TearDownSuite(c *check.C) {
477- newLocalRepo = nil
478 newRemoteRepo = nil
479- newRepo = nil
480+ newSystemRepo = nil
481 muxVars = nil
482 }
483
484 func (s *apiSuite) SetUpTest(c *check.C) {
485+ dirs.SetRootDir(c.MkDir())
486+
487 s.parts = nil
488 s.err = nil
489 s.vars = nil
490 }
491
492+func (s *apiSuite) mkInstalled(c *check.C, name, origin, version string, active bool, extraYaml string) {
493+ fullname := name + "." + origin
494+ c.Assert(os.MkdirAll(filepath.Join(dirs.SnapDataDir, fullname, version), 0755), check.IsNil)
495+
496+ metadir := filepath.Join(dirs.SnapAppsDir, fullname, version, "meta")
497+ c.Assert(os.MkdirAll(metadir, 0755), check.IsNil)
498+
499+ content := fmt.Sprintf(`
500+name: %s
501+version: %s
502+vendor: a vendor
503+%s`, name, version, extraYaml)
504+ c.Check(ioutil.WriteFile(filepath.Join(metadir, "package.yaml"), []byte(content), 0644), check.IsNil)
505+ c.Check(ioutil.WriteFile(filepath.Join(metadir, "hashes.yaml"), []byte(nil), 0644), check.IsNil)
506+
507+ if active {
508+ c.Assert(os.Symlink(version, filepath.Join(dirs.SnapAppsDir, fullname, "current")), check.IsNil)
509+ }
510+}
511+
512 func (s *apiSuite) TestPackageInfoOneIntegration(c *check.C) {
513 d := New()
514 d.addRoutes()
515
516 s.vars = map[string]string{"name": "foo", "origin": "bar"}
517
518+ // the store tells us about v2
519 s.parts = []snappy.Part{&tP{
520- name: "foo",
521- version: "v1",
522- description: "description",
523- origin: "bar",
524- vendor: "a vendor",
525- isInstalled: true,
526- isActive: true,
527- icon: iconPath + "icon.png",
528- _type: "a type",
529- installedSize: 42,
530- downloadSize: 2,
531+ name: "foo",
532+ version: "v2",
533+ description: "description",
534+ origin: "bar",
535+ vendor: "a vendor",
536+ isInstalled: true,
537+ isActive: true,
538+ icon: dirs.SnapIconsDir + "icon.png",
539+ _type: pkg.TypeApp,
540+ downloadSize: 2,
541 }}
542+
543+ // we have v0 installed
544+ s.mkInstalled(c, "foo", "bar", "v0", false, "")
545+ // and v1 is current
546+ s.mkInstalled(c, "foo", "bar", "v1", true, "")
547+
548 rsp, ok := getPackageInfo(packageCmd, nil).(*resp)
549 c.Assert(ok, check.Equals, true)
550
551@@ -114,17 +148,19 @@
552 Type: ResponseTypeSync,
553 Status: http.StatusOK,
554 Result: map[string]string{
555- "name": "foo",
556- "version": "v1",
557- "description": "description",
558- "origin": "bar",
559- "vendor": "a vendor",
560- "status": "active",
561- "icon": iconPrefix + "icon.png",
562- "type": "a type",
563- "download_size": "2",
564- "installed_size": "42",
565- "resource": "/1.0/packages/foo.bar",
566+ "name": "foo",
567+ "version": "v1",
568+ "description": "description",
569+ "origin": "bar",
570+ "vendor": "a vendor",
571+ "status": "active",
572+ "icon": "/1.0/icons/foo.bar/icon",
573+ "type": string(pkg.TypeApp),
574+ "download_size": "2",
575+ "installed_size": "180",
576+ "resource": "/1.0/packages/foo.bar",
577+ "update_available": "v2",
578+ "rollback_available": "v0",
579 },
580 }
581
582@@ -144,14 +180,14 @@
583 c.Check(getPackageInfo(packageCmd, nil).Self(nil, nil).(*resp).Status, check.Equals, http.StatusNotFound)
584 }
585
586-func (s *apiSuite) TestPackageInfoWeirdDetails(c *check.C) {
587+func (s *apiSuite) TestPackageInfoIgnoresRemoteErrors(c *check.C) {
588 s.vars = map[string]string{"name": "foo", "origin": "bar"}
589 s.err = errors.New("weird")
590
591- rsp := getPackageInfo(packageCmd, nil).(*resp)
592+ rsp := getPackageInfo(packageCmd, nil).Self(nil, nil).(*resp)
593
594 c.Check(rsp.Type, check.Equals, ResponseTypeError)
595- c.Check(rsp.Status, check.Equals, http.StatusInternalServerError)
596+ c.Check(rsp.Status, check.Equals, http.StatusNotFound)
597 c.Check(rsp.Result, check.NotNil)
598 }
599
600@@ -188,47 +224,6 @@
601 c.Check(rsp.Result.(map[string]interface{})["msg"], check.Matches, `route can't build URL .*`)
602 }
603
604-func (s *apiSuite) TestParts2Map(c *check.C) {
605- parts := []snappy.Part{&tP{
606- name: "foo",
607- version: "v1",
608- description: "description",
609- origin: "bar",
610- vendor: "a vendor",
611- isInstalled: true,
612- isActive: true,
613- icon: "icon.png",
614- _type: "a type",
615- installedSize: 42,
616- downloadSize: 2,
617- }}
618-
619- resource := "/1.0/packages/foo.bar"
620-
621- expected := map[string]string{
622- "name": "foo",
623- "version": "v1",
624- "description": "description",
625- "origin": "bar",
626- "vendor": "a vendor",
627- "status": "active",
628- "icon": "icon.png",
629- "type": "a type",
630- "download_size": "2",
631- "installed_size": "42",
632- "resource": resource,
633- }
634-
635- c.Check(parts2map(parts, resource), check.DeepEquals, expected)
636-}
637-
638-func (s *apiSuite) TestParts2MapState(c *check.C) {
639- c.Check(parts2map([]snappy.Part{&tP{}}, "")["status"], check.Equals, "not installed")
640- c.Check(parts2map([]snappy.Part{&tP{isInstalled: true}}, "")["status"], check.Equals, "installed")
641- c.Check(parts2map([]snappy.Part{&tP{isInstalled: true, isActive: true}}, "")["status"], check.Equals, "active")
642- // TODO: more statuses
643-}
644-
645 func (s *apiSuite) TestListIncludesAll(c *check.C) {
646 // Very basic check to help stop us from not adding all the
647 // commands to the command list.
648@@ -263,9 +258,8 @@
649 "findServices",
650 "maxReadBuflen",
651 "muxVars",
652- "newLocalRepo",
653 "newRemoteRepo",
654- "newRepo",
655+ "newSystemRepo",
656 "newSnap",
657 "pkgActionDispatch",
658 }
659@@ -335,12 +329,12 @@
660 req, err := http.NewRequest("GET", "/1.0/packages", nil)
661 c.Assert(err, check.IsNil)
662
663- s.parts = []snappy.Part{
664- &tP{name: "foo", version: "v1", origin: "bar"},
665- &tP{name: "bar", version: "v2", origin: "baz"},
666- &tP{name: "baz", version: "v3", origin: "qux"},
667- &tP{name: "qux", version: "v4", origin: "mip"},
668+ ddirs := [][2]string{{"foo.bar", "v1"}, {"bar.baz", "v2"}, {"baz.qux", "v3"}, {"qux.mip", "v4"}}
669+
670+ for i := range ddirs {
671+ c.Assert(os.MkdirAll(filepath.Join(dirs.SnapDataDir, ddirs[i][0], ddirs[i][1]), 0755), check.IsNil)
672 }
673+
674 rsp, ok := getPackagesInfo(packagesCmd, req).(*resp)
675 c.Assert(ok, check.Equals, true)
676
677@@ -351,21 +345,22 @@
678 meta, ok := rsp.Result.(map[string]interface{})
679 c.Assert(ok, check.Equals, true)
680 c.Assert(meta, check.NotNil)
681- c.Check(meta["paging"], check.DeepEquals, map[string]interface{}{"pages": 1, "page": 1, "count": len(s.parts)})
682+ c.Check(meta["paging"], check.DeepEquals, map[string]interface{}{"pages": 1, "page": 1, "count": len(ddirs)})
683
684 packages, ok := meta["packages"].(map[string]map[string]string)
685 c.Assert(ok, check.Equals, true)
686 c.Check(packages, check.NotNil)
687- c.Check(packages, check.HasLen, len(s.parts))
688+ c.Check(packages, check.HasLen, len(ddirs))
689
690- for _, part := range s.parts {
691- part := part.(*tP)
692- qn := part.name + "." + part.origin
693+ for i := range ddirs {
694+ qn, version := ddirs[i][0], ddirs[i][1]
695+ idx := strings.LastIndexByte(qn, '.')
696+ name, origin := qn[:idx], qn[idx+1:]
697 got := packages[qn]
698 c.Assert(got, check.NotNil, check.Commentf(qn))
699- c.Check(got["name"], check.Equals, part.name)
700- c.Check(got["version"], check.Equals, part.version)
701- c.Check(got["origin"], check.Equals, part.origin)
702+ c.Check(got["name"], check.Equals, name)
703+ c.Check(got["version"], check.Equals, version)
704+ c.Check(got["origin"], check.Equals, origin)
705 }
706 }
707
708@@ -530,6 +525,14 @@
709 }
710 }
711
712+type cfgc struct{ cfg string }
713+
714+func (cfgc) IsInstalled(string) bool { return true }
715+func (cfgc) ActiveIndex() int { return 0 }
716+func (c cfgc) Load(string) (snappy.Part, error) {
717+ return &tP{name: "foo", version: "v1", origin: "bar", isActive: true, config: c.cfg}, nil
718+}
719+
720 func (s *apiSuite) TestPackageGetConfig(c *check.C) {
721 d := New()
722 d.addRoutes()
723@@ -538,13 +541,16 @@
724 c.Assert(err, check.IsNil)
725
726 configStr := "some: config"
727+ oldConcrete := husk.NewConcrete
728+ defer func() {
729+ husk.NewConcrete = oldConcrete
730+ }()
731+ husk.NewConcrete = func(*husk.Husk, string) husk.Concreter {
732+ return &cfgc{configStr}
733+ }
734+
735 s.vars = map[string]string{"name": "foo", "origin": "bar"}
736- s.parts = []snappy.Part{
737- &tP{name: "foo", version: "v1", origin: "bar", isActive: true, config: configStr},
738- &tP{name: "bar", version: "v2", origin: "baz", isActive: true},
739- &tP{name: "baz", version: "v3", origin: "qux", isActive: true},
740- &tP{name: "qux", version: "v4", origin: "mip", isActive: true},
741- }
742+ s.mkInstalled(c, "foo", "bar", "v1", true, "")
743
744 rsp := packageConfig(packagesCmd, req).(*resp)
745
746@@ -564,15 +570,18 @@
747 c.Assert(err, check.IsNil)
748
749 configStr := "some: config"
750+ oldConcrete := husk.NewConcrete
751+ defer func() {
752+ husk.NewConcrete = oldConcrete
753+ }()
754+ husk.NewConcrete = func(*husk.Husk, string) husk.Concreter {
755+ return &cfgc{configStr}
756+ }
757+
758 s.vars = map[string]string{"name": "foo", "origin": "bar"}
759- s.parts = []snappy.Part{
760- &tP{name: "foo", version: "v1", origin: "bar", isActive: true, config: configStr},
761- &tP{name: "bar", version: "v2", origin: "baz", isActive: true},
762- &tP{name: "baz", version: "v3", origin: "qux", isActive: true},
763- &tP{name: "qux", version: "v4", origin: "mip", isActive: true},
764- }
765+ s.mkInstalled(c, "foo", "bar", "v1", true, "")
766
767- rsp := packageConfig(packagesCmd, req).(*resp)
768+ rsp := packageConfig(packagesCmd, req).Self(nil, nil).(*resp)
769
770 c.Check(rsp, check.DeepEquals, &resp{
771 Type: ResponseTypeSync,
772@@ -592,11 +601,7 @@
773 req, err := http.NewRequest("GET", "/1.0/packages/foo.bar/services", nil)
774 c.Assert(err, check.IsNil)
775
776- s.parts = []snappy.Part{
777- &tP{name: "foo", version: "v1", origin: "bar", isActive: true,
778- svcYamls: []snappy.ServiceYaml{{Name: "svc"}},
779- },
780- }
781+ s.mkInstalled(c, "foo", "bar", "v1", true, "services: [{name: svc}]")
782 s.vars = map[string]string{"name": "foo", "origin": "bar"} // NB: no service specified
783
784 rsp := packageService(packageSvcsCmd, req).(*resp)
785@@ -605,11 +610,10 @@
786 c.Check(rsp.Status, check.Equals, http.StatusOK)
787
788 m := rsp.Result.(map[string]*svcDesc)
789- c.Assert(m["svc"], check.DeepEquals, &svcDesc{
790- Op: "status",
791- Spec: &snappy.ServiceYaml{Name: "svc"},
792- Status: &snappy.PackageServiceStatus{ServiceName: "svc"},
793- })
794+ c.Assert(m["svc"], check.FitsTypeOf, new(svcDesc))
795+ c.Check(m["svc"].Op, check.Equals, "status")
796+ c.Check(m["svc"].Spec, check.DeepEquals, &snappy.ServiceYaml{Name: "svc", StopTimeout: snappy.DefaultTimeout})
797+ c.Check(m["svc"].Status, check.DeepEquals, &snappy.PackageServiceStatus{ServiceName: "svc"})
798 }
799
800 func (s *apiSuite) TestPackageServicePut(c *check.C) {
801@@ -624,11 +628,7 @@
802 req, err := http.NewRequest("PUT", "/1.0/packages/foo.bar/services", buf)
803 c.Assert(err, check.IsNil)
804
805- s.parts = []snappy.Part{
806- &tP{name: "foo", version: "v1", origin: "bar", isActive: true,
807- svcYamls: []snappy.ServiceYaml{{Name: "svc"}},
808- },
809- }
810+ s.mkInstalled(c, "foo", "bar", "v1", true, "services: [{name: svc}]")
811 s.vars = map[string]string{"name": "foo", "origin": "bar"} // NB: no service specified
812
813 rsp := packageService(packageSvcsCmd, req).(*resp)
814
815=== added directory 'daemon/crypt'
816=== added file 'daemon/crypt/bcrypt.go'
817--- daemon/crypt/bcrypt.go 1970-01-01 00:00:00 +0000
818+++ daemon/crypt/bcrypt.go 2015-10-07 12:49:07 +0000
819@@ -0,0 +1,38 @@
820+// -*- Mode: Go; indent-tabs-mode: t -*-
821+
822+/*
823+ * Copyright (C) 2014-2015 Canonical Ltd
824+ *
825+ * This program is free software: you can redistribute it and/or modify
826+ * it under the terms of the GNU General Public License version 3 as
827+ * published by the Free Software Foundation.
828+ *
829+ * This program is distributed in the hope that it will be useful,
830+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
831+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
832+ * GNU General Public License for more details.
833+ *
834+ * You should have received a copy of the GNU General Public License
835+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
836+ *
837+ */
838+
839+package crypt
840+
841+import (
842+ "golang.org/x/crypto/bcrypt"
843+)
844+
845+var bcryptAlg = cryptAlg{
846+ gen: func(password []byte) ([]byte, error) {
847+ hash, err := bcrypt.GenerateFromPassword(password, -1)
848+ if err != nil {
849+ return nil, err
850+ }
851+
852+ return hash, nil
853+ },
854+ cmp: func(hashedPassword, password []byte) error {
855+ return bcrypt.CompareHashAndPassword(hashedPassword, password)
856+ },
857+}
858
859=== added file 'daemon/crypt/crypt.go'
860--- daemon/crypt/crypt.go 1970-01-01 00:00:00 +0000
861+++ daemon/crypt/crypt.go 2015-10-07 12:49:07 +0000
862@@ -0,0 +1,112 @@
863+// -*- Mode: Go; indent-tabs-mode: t -*-
864+
865+/*
866+ * Copyright (C) 2014-2015 Canonical Ltd
867+ *
868+ * This program is free software: you can redistribute it and/or modify
869+ * it under the terms of the GNU General Public License version 3 as
870+ * published by the Free Software Foundation.
871+ *
872+ * This program is distributed in the hope that it will be useful,
873+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
874+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
875+ * GNU General Public License for more details.
876+ *
877+ * You should have received a copy of the GNU General Public License
878+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
879+ *
880+ */
881+
882+package crypt
883+
884+import (
885+ "bytes"
886+ "errors"
887+ "io"
888+ "os"
889+ "path/filepath"
890+
891+ "launchpad.net/snappy/dirs"
892+ "launchpad.net/snappy/helpers"
893+)
894+
895+type cryptAlg struct {
896+ gen func(password []byte) ([]byte, error)
897+ cmp func(hashed, password []byte) error
898+}
899+
900+// the known crypt algorithms
901+//
902+// xxx is algorithm the right name even
903+const (
904+ SHA512 = "sha512"
905+ SCRYPT = "scrypt"
906+ BCRYPT = "bcrypt"
907+)
908+
909+// the default algorithm
910+var DefaultAlg = SCRYPT
911+
912+var cryptAlgs = map[string]cryptAlg{
913+ SCRYPT: scryptAlg,
914+ BCRYPT: bcryptAlg,
915+ SHA512: sha512cryptAlg,
916+}
917+
918+var errBadCreds = errors.New("bad credentials")
919+
920+const (
921+ maxSecretLen = 1024 // upper bound of the length of a secret
922+ maxPrefixLen = 8 // upper bound of the prefix length (e.g. "scrypt:")
923+)
924+
925+func pwFile() string {
926+ return filepath.Join(dirs.GlobalRootDir, dirs.SnappyDir, ".snapd_creds")
927+}
928+
929+// Write the given password out to the file, crypting it first
930+func Write(password []byte) error {
931+ // TODO: cracklib
932+
933+ hash, err := cryptAlgs[DefaultAlg].gen(password)
934+ if err != nil {
935+ return err
936+ }
937+
938+ buf := append([]byte(DefaultAlg+":"), hash...)
939+ if err := helpers.AtomicWriteFile(pwFile(), buf, 0600); err != nil {
940+ return err
941+ }
942+
943+ return nil
944+}
945+
946+// Check whether this password and the one on file match.
947+func Check(password []byte) bool {
948+ f, err := os.Open(pwFile())
949+ if err != nil {
950+ return false
951+ }
952+
953+ buf := make([]byte, maxPrefixLen+maxSecretLen)
954+ n, err := f.ReadAt(buf, 0)
955+ if err != io.EOF {
956+ return false
957+ }
958+
959+ if n < maxPrefixLen {
960+ return false
961+ }
962+
963+ idx := bytes.IndexByte(buf[:maxPrefixLen], ':')
964+ if idx < 1 {
965+ return false
966+ }
967+
968+ alg, ok := cryptAlgs[string(buf[:idx])]
969+ if !ok {
970+ return false
971+ }
972+
973+ return alg.cmp(buf[idx+1:n], password) == nil
974+}
975
976=== added file 'daemon/crypt/crypt_test.go'
977--- daemon/crypt/crypt_test.go 1970-01-01 00:00:00 +0000
978+++ daemon/crypt/crypt_test.go 2015-10-07 12:49:07 +0000
979@@ -0,0 +1,95 @@
980+// -*- Mode: Go; indent-tabs-mode: t -*-
981+
982+/*
983+ * Copyright (C) 2014-2015 Canonical Ltd
984+ *
985+ * This program is free software: you can redistribute it and/or modify
986+ * it under the terms of the GNU General Public License version 3 as
987+ * published by the Free Software Foundation.
988+ *
989+ * This program is distributed in the hope that it will be useful,
990+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
991+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
992+ * GNU General Public License for more details.
993+ *
994+ * You should have received a copy of the GNU General Public License
995+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
996+ *
997+ */
998+
999+package crypt
1000+
1001+import (
1002+ "crypto/rand"
1003+ "io"
1004+ "os"
1005+ "path/filepath"
1006+ "testing"
1007+
1008+ "gopkg.in/check.v1"
1009+
1010+ "launchpad.net/snappy/dirs"
1011+)
1012+
1013+// Hook up check.v1 into the "go test" runner
1014+func Test(t *testing.T) { check.TestingT(t) }
1015+
1016+type cryptSuite struct {
1017+ oldReader io.Reader
1018+}
1019+
1020+var _ = check.Suite(&cryptSuite{})
1021+
1022+type totallyRandom struct{}
1023+
1024+func (totallyRandom) Read(p []byte) (int, error) {
1025+ if len(p) < 1 {
1026+ return 0, nil
1027+ }
1028+ p[0] = 9 // http://dilbert.com/strip/2001-10-25 -- wait, 2001? Ow.
1029+ return 1, nil
1030+}
1031+
1032+func (s *cryptSuite) SetUpTest(c *check.C) {
1033+ dirs.SetRootDir(c.MkDir())
1034+ c.Check(os.MkdirAll(filepath.Join(dirs.GlobalRootDir, dirs.SnappyDir), 0755), check.IsNil)
1035+
1036+ s.oldReader = rand.Reader
1037+ rand.Reader = totallyRandom{}
1038+}
1039+
1040+func (s *cryptSuite) TearDownTest(c *check.C) {
1041+ rand.Reader = s.oldReader
1042+}
1043+
1044+func (s *cryptSuite) TestCryptAlgs(c *check.C) {
1045+
1046+ for _, password := range [][]byte{
1047+ []byte("hello"),
1048+ []byte{0, 255, 0, 255},
1049+ // know any interesting values to try? call 0800-CHIPACA
1050+ } {
1051+ for k, v := range cryptAlgs {
1052+ hash, err := v.gen(password)
1053+ c.Assert(err, check.IsNil, check.Commentf(k))
1054+ c.Assert(hash, check.NotNil, check.Commentf(k))
1055+
1056+ c.Check(v.cmp(hash, password), check.IsNil, check.Commentf(k))
1057+ c.Check(v.cmp(hash, []byte("what")), check.NotNil, check.Commentf(k))
1058+ }
1059+ }
1060+}
1061+
1062+func (s *cryptSuite) TestRoundtrip(c *check.C) {
1063+ c.Check(Write([]byte("hello")), check.IsNil)
1064+ c.Check(Check([]byte("hello")), check.Equals, true)
1065+ c.Check(Check([]byte("bye")), check.Equals, false)
1066+}
1067+
1068+func (s *cryptSuite) TestRoundtripChangedDefaults(c *check.C) {
1069+ DefaultAlg = "bcrypt"
1070+ c.Check(Write([]byte("hello")), check.IsNil)
1071+ DefaultAlg = "scrypt"
1072+ c.Check(Check([]byte("hello")), check.Equals, true)
1073+ c.Check(Check([]byte("bye")), check.Equals, false)
1074+}
1075
1076=== added file 'daemon/crypt/scrypt.go'
1077--- daemon/crypt/scrypt.go 1970-01-01 00:00:00 +0000
1078+++ daemon/crypt/scrypt.go 2015-10-07 12:49:07 +0000
1079@@ -0,0 +1,75 @@
1080+// -*- Mode: Go; indent-tabs-mode: t -*-
1081+
1082+/*
1083+ * Copyright (C) 2014-2015 Canonical Ltd
1084+ *
1085+ * This program is free software: you can redistribute it and/or modify
1086+ * it under the terms of the GNU General Public License version 3 as
1087+ * published by the Free Software Foundation.
1088+ *
1089+ * This program is distributed in the hope that it will be useful,
1090+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1091+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1092+ * GNU General Public License for more details.
1093+ *
1094+ * You should have received a copy of the GNU General Public License
1095+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1096+ *
1097+ */
1098+
1099+package crypt
1100+
1101+import (
1102+ "bytes"
1103+ "crypto/rand"
1104+ "encoding/ascii85"
1105+ "io"
1106+
1107+ "golang.org/x/crypto/scrypt"
1108+)
1109+
1110+const (
1111+ scryptSaltLen = 32
1112+ scryptHashLen = 64
1113+ scryptN = 1 << 14
1114+ scryptR = 8
1115+ scryptP = 16
1116+)
1117+
1118+var scryptAlg = cryptAlg{
1119+ cmp: func(hashedPassword, password []byte) error {
1120+ saltedHash := make([]byte, scryptSaltLen+scryptHashLen)
1121+ _, _, err := ascii85.Decode(saltedHash, hashedPassword, true)
1122+ if err != nil {
1123+ return err
1124+ }
1125+
1126+ salt := saltedHash[:scryptSaltLen]
1127+ hash, err := scrypt.Key([]byte(password), salt, scryptN, scryptR, scryptP, scryptHashLen)
1128+ if err != nil {
1129+ return err
1130+ }
1131+
1132+ if !bytes.Equal(hash, saltedHash[scryptSaltLen:]) {
1133+ return errBadCreds
1134+ }
1135+
1136+ return nil
1137+ },
1138+ gen: func(password []byte) ([]byte, error) {
1139+ salt := make([]byte, scryptSaltLen)
1140+ if _, err := io.ReadFull(rand.Reader, salt); err != nil {
1141+ return nil, err
1142+ }
1143+
1144+ hash, err := scrypt.Key([]byte(password), salt, scryptN, scryptR, scryptP, scryptHashLen)
1145+ if err != nil {
1146+ return nil, err
1147+ }
1148+
1149+ coded := make([]byte, ascii85.MaxEncodedLen(scryptSaltLen+scryptHashLen))
1150+ n := ascii85.Encode(coded, append(salt, hash...))
1151+
1152+ return coded[:n], nil
1153+ },
1154+}
1155
1156=== added file 'daemon/crypt/sha512crypt.go'
1157--- daemon/crypt/sha512crypt.go 1970-01-01 00:00:00 +0000
1158+++ daemon/crypt/sha512crypt.go 2015-10-07 12:49:07 +0000
1159@@ -0,0 +1,114 @@
1160+// -*- Mode: Go; indent-tabs-mode: t -*-
1161+
1162+/*
1163+ * Copyright (C) 2014-2015 Canonical Ltd
1164+ *
1165+ * This program is free software: you can redistribute it and/or modify
1166+ * it under the terms of the GNU General Public License version 3 as
1167+ * published by the Free Software Foundation.
1168+ *
1169+ * This program is distributed in the hope that it will be useful,
1170+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1171+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1172+ * GNU General Public License for more details.
1173+ *
1174+ * You should have received a copy of the GNU General Public License
1175+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1176+ *
1177+ */
1178+
1179+package crypt
1180+
1181+/*
1182+#cgo CFLAGS: -D_GNU_SOURCE -O3 -Wextra -Werror -pedantic
1183+#cgo LDFLAGS: -lcrypt
1184+#include <crypt.h>
1185+#include <string.h>
1186+*/
1187+import "C"
1188+
1189+import (
1190+ "bytes"
1191+ "crypto/rand"
1192+ "encoding/binary"
1193+ "unsafe"
1194+)
1195+
1196+const (
1197+ sha512saltLetters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789./"
1198+ sha512saltNChars = 16
1199+)
1200+
1201+func mksalt() ([]byte, error) {
1202+ raw := make([]uint32, 3)
1203+ if err := binary.Read(rand.Reader, binary.LittleEndian, &raw); err != nil {
1204+ return nil, err
1205+ }
1206+
1207+ full := make([]byte, sha512saltNChars+4)
1208+ full[0] = byte('$')
1209+ full[1] = byte('6')
1210+ full[2] = byte('$')
1211+ full[3+sha512saltNChars] = byte('$')
1212+ salt := full[3 : 3+sha512saltNChars]
1213+
1214+ m := uint32(0)
1215+ for i, n := range raw {
1216+ for j := 1; j < 6; j++ {
1217+ salt[i*5+j] = sha512saltLetters[n&63]
1218+ n >>= 6
1219+ }
1220+ m += n << (2 * uint(i))
1221+ }
1222+ salt[0] = sha512saltLetters[m]
1223+
1224+ return full, nil
1225+}
1226+
1227+func crypt(password, saltOrHashedPassword []byte, checkOnly bool) ([]byte, error) {
1228+ s := C.struct_crypt_data{}
1229+ s.initialized = 0
1230+
1231+ p, err := C.crypt_r(
1232+ (*C.char)(unsafe.Pointer(&password[0])),
1233+ (*C.char)(unsafe.Pointer(&saltOrHashedPassword[0])),
1234+ &s,
1235+ )
1236+ if err != nil {
1237+ return nil, err
1238+ }
1239+
1240+ n := C.strnlen(p, maxSecretLen)
1241+ if n == maxSecretLen {
1242+ // just in case
1243+ return nil, errBadCreds
1244+ }
1245+
1246+ buf := (*[1024]byte)(unsafe.Pointer(p))[:n]
1247+
1248+ if checkOnly {
1249+ if bytes.Equal(buf, saltOrHashedPassword) {
1250+ return nil, nil
1251+ }
1252+
1253+ return nil, errBadCreds
1254+ }
1255+
1256+ return append([]byte(nil), buf...), nil
1257+}
1258+
1259+var sha512cryptAlg = cryptAlg{
1260+ gen: func(password []byte) ([]byte, error) {
1261+ salt, err := mksalt()
1262+ if err != nil {
1263+ return nil, err
1264+ }
1265+
1266+ return crypt(password, salt, false)
1267+ },
1268+ cmp: func(hashedPassword, password []byte) error {
1269+ _, err := crypt(password, hashedPassword, true)
1270+
1271+ return err
1272+ },
1273+}
1274
1275=== modified file 'daemon/daemon.go'
1276--- daemon/daemon.go 2015-09-15 12:55:09 +0000
1277+++ daemon/daemon.go 2015-10-07 12:49:07 +0000
1278@@ -30,6 +30,7 @@
1279 "github.com/gorilla/mux"
1280 "gopkg.in/tomb.v2"
1281
1282+ "launchpad.net/snappy/daemon/crypt"
1283 "launchpad.net/snappy/logger"
1284 )
1285
1286@@ -47,7 +48,8 @@
1287
1288 // A Command routes a request to an individual per-verb ResponseFUnc
1289 type Command struct {
1290- Path string
1291+ Path string
1292+ GuestOK bool
1293 //
1294 GET ResponseFunc
1295 PUT ResponseFunc
1296@@ -57,7 +59,26 @@
1297 d *Daemon
1298 }
1299
1300+// AuthOK checks whether the request has enough authorization for the command
1301+func (c *Command) AuthOK(r *http.Request) bool {
1302+ if c.GuestOK {
1303+ return true
1304+ }
1305+
1306+ user, pass, ok := r.BasicAuth()
1307+ if !ok {
1308+ return false
1309+ }
1310+
1311+ return crypt.Check([]byte(user + pass))
1312+}
1313+
1314 func (c *Command) ServeHTTP(w http.ResponseWriter, r *http.Request) {
1315+ if !c.AuthOK(r) {
1316+ Unauthorized.ServeHTTP(w, r)
1317+ return
1318+ }
1319+
1320 var rspf ResponseFunc
1321 var rsp Response = BadMethod
1322
1323@@ -128,11 +149,6 @@
1324 return nil
1325 }
1326
1327-const (
1328- iconPath = "/var/lib/snappy/icons/"
1329- iconPrefix = "/1.0/icons/"
1330-)
1331-
1332 func (d *Daemon) addRoutes() {
1333 d.router = mux.NewRouter()
1334
1335@@ -142,8 +158,6 @@
1336 d.router.Handle(c.Path, c).Name(c.Path)
1337 }
1338
1339- // hrmph
1340- d.router.PathPrefix(iconPrefix).Handler(http.StripPrefix(iconPrefix, http.FileServer(http.Dir(iconPath)))).Name(iconPrefix)
1341 // also maybe add a /favicon.ico handler...
1342
1343 d.router.NotFoundHandler = NotFound
1344
1345=== added file 'daemon/daemon_test.go'
1346--- daemon/daemon_test.go 1970-01-01 00:00:00 +0000
1347+++ daemon/daemon_test.go 2015-10-07 12:49:07 +0000
1348@@ -0,0 +1,117 @@
1349+// -*- Mode: Go; indent-tabs-mode: t -*-
1350+
1351+/*
1352+ * Copyright (C) 2014-2015 Canonical Ltd
1353+ *
1354+ * This program is free software: you can redistribute it and/or modify
1355+ * it under the terms of the GNU General Public License version 3 as
1356+ * published by the Free Software Foundation.
1357+ *
1358+ * This program is distributed in the hope that it will be useful,
1359+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1360+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1361+ * GNU General Public License for more details.
1362+ *
1363+ * You should have received a copy of the GNU General Public License
1364+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1365+ *
1366+ */
1367+
1368+package daemon
1369+
1370+import (
1371+ "fmt"
1372+ "net/http"
1373+ "net/http/httptest"
1374+ "os"
1375+ "path/filepath"
1376+
1377+ "github.com/gorilla/mux"
1378+ "gopkg.in/check.v1"
1379+
1380+ "launchpad.net/snappy/daemon/crypt"
1381+ "launchpad.net/snappy/dirs"
1382+)
1383+
1384+type daemonSuite struct{}
1385+
1386+var _ = check.Suite(&daemonSuite{})
1387+
1388+type nullHandler struct{}
1389+
1390+func (nu *nullHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
1391+}
1392+func (nu *nullHandler) Self(*Command, *http.Request) Response {
1393+ return nu
1394+}
1395+
1396+func mkRF(c *check.C, cmd *Command, method string) ResponseFunc {
1397+ return func(innerCmd *Command, req *http.Request) Response {
1398+ c.Assert(cmd, check.Equals, innerCmd)
1399+ c.Assert(method, check.Equals, req.Method)
1400+ return &nullHandler{}
1401+ }
1402+}
1403+
1404+func (s *daemonSuite) TestCommandMethodDispatch(c *check.C) {
1405+ dirs.SetRootDir(c.MkDir())
1406+ c.Check(os.MkdirAll(filepath.Join(dirs.GlobalRootDir, dirs.SnappyDir), 0755), check.IsNil)
1407+
1408+ cmd := &Command{}
1409+ cmd.GET = mkRF(c, cmd, "GET")
1410+ cmd.PUT = mkRF(c, cmd, "PUT")
1411+ cmd.POST = mkRF(c, cmd, "POST")
1412+ cmd.DELETE = mkRF(c, cmd, "DELETE")
1413+
1414+ crypt.DefaultAlg = crypt.SHA512
1415+ c.Check(crypt.Write([]byte("hithere")), check.IsNil)
1416+
1417+ for _, method := range []string{"GET", "POST", "PUT", "DELETE"} {
1418+ req, err := http.NewRequest(method, "", nil)
1419+ c.Assert(err, check.IsNil)
1420+
1421+ cmd.GuestOK = false
1422+ rec := httptest.NewRecorder()
1423+ cmd.ServeHTTP(rec, req)
1424+ c.Check(rec.Code, check.Equals, http.StatusUnauthorized)
1425+
1426+ cmd.GuestOK = true
1427+ rec = httptest.NewRecorder()
1428+ cmd.ServeHTTP(rec, req)
1429+ c.Check(rec.Code, check.Equals, http.StatusOK)
1430+
1431+ cmd.GuestOK = false
1432+ rec = httptest.NewRecorder()
1433+ req.SetBasicAuth("hi", "there")
1434+ cmd.ServeHTTP(rec, req)
1435+ c.Check(rec.Code, check.Equals, http.StatusOK)
1436+ }
1437+
1438+ cmd.GuestOK = true
1439+
1440+ req, err := http.NewRequest("POTATO", "", nil)
1441+ c.Assert(err, check.IsNil)
1442+ rec := httptest.NewRecorder()
1443+ cmd.ServeHTTP(rec, req)
1444+ c.Check(rec.Code, check.Equals, http.StatusMethodNotAllowed)
1445+}
1446+
1447+func (s *daemonSuite) TestAddRoutes(c *check.C) {
1448+ d := New()
1449+ d.addRoutes()
1450+
1451+ expected := make([]string, len(api))
1452+ for i, v := range api {
1453+ expected[i] = v.Path
1454+ }
1455+
1456+ got := make([]string, 0, len(api))
1457+ c.Assert(d.router.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error {
1458+ got = append(got, route.GetName())
1459+ return nil
1460+ }), check.IsNil)
1461+
1462+ c.Check(got, check.DeepEquals, expected) // this'll stop being true if routes are added that aren't commands (e.g. for the favicon)
1463+
1464+ c.Check(fmt.Sprintf("%p", d.router.NotFoundHandler), check.Equals, fmt.Sprintf("%p", NotFound))
1465+}
1466
1467=== modified file 'daemon/response.go'
1468--- daemon/response.go 2015-09-15 13:47:08 +0000
1469+++ daemon/response.go 2015-10-07 12:49:07 +0000
1470@@ -41,7 +41,6 @@
1471
1472 // Response knows how to render itself, how to handle itself, and how to find itself
1473 type Response interface {
1474- Render(w http.ResponseWriter) ([]byte, int)
1475 ServeHTTP(w http.ResponseWriter, r *http.Request)
1476 Self(*Command, *http.Request) Response // has the same arity as ResponseFunc for convenience
1477 }
1478@@ -61,19 +60,15 @@
1479 })
1480 }
1481
1482-func (r *resp) Render(w http.ResponseWriter) (buf []byte, status int) {
1483+func (r *resp) ServeHTTP(w http.ResponseWriter, _ *http.Request) {
1484+ status := r.Status
1485 bs, err := r.MarshalJSON()
1486 if err != nil {
1487 logger.Noticef("unable to marshal %#v to JSON: %v", *r, err)
1488- return nil, http.StatusInternalServerError
1489+ bs = nil
1490+ status = http.StatusInternalServerError
1491 }
1492
1493- return bs, r.Status
1494-}
1495-
1496-func (r *resp) ServeHTTP(w http.ResponseWriter, _ *http.Request) {
1497- bs, status := r.Render(w)
1498-
1499 hdr := w.Header()
1500 if r.Type == ResponseTypeAsync {
1501 if m, ok := r.Result.(map[string]interface{}); ok {
1502@@ -151,15 +146,21 @@
1503 return r.SetError
1504 }
1505
1506+// A FileResponse 's ServeHTTP method serves the file
1507+type FileResponse string
1508+
1509+// Self from the Response interface
1510+func (f FileResponse) Self(*Command, *http.Request) Response { return f }
1511+
1512+// ServeHTTP from the Response interface
1513+func (f FileResponse) ServeHTTP(w http.ResponseWriter, r *http.Request) {
1514+ http.ServeFile(w, r, string(f))
1515+}
1516+
1517 // ErrorResponseFunc is a callable error Response.
1518 // So you can return e.g. InternalError, or InternalError(err, "something broke"), etc.
1519 type ErrorResponseFunc func(error, string, ...interface{}) Response
1520
1521-// Render the response
1522-func (f ErrorResponseFunc) Render(w http.ResponseWriter) ([]byte, int) {
1523- return f(nil, "").Render(w)
1524-}
1525-
1526 func (f ErrorResponseFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
1527 f(nil, "").ServeHTTP(w, r)
1528 }
1529@@ -176,4 +177,5 @@
1530 BadMethod = ErrorResponse(http.StatusMethodNotAllowed)
1531 InternalError = ErrorResponse(http.StatusInternalServerError)
1532 NotImplemented = ErrorResponse(http.StatusNotImplemented)
1533+ Unauthorized = ErrorResponse(http.StatusUnauthorized)
1534 )
1535
1536=== added directory 'pkg/husk'
1537=== added file 'pkg/husk/example_test.go'
1538--- pkg/husk/example_test.go 1970-01-01 00:00:00 +0000
1539+++ pkg/husk/example_test.go 2015-10-07 12:49:07 +0000
1540@@ -0,0 +1,54 @@
1541+// -*- Mode: Go; indent-tabs-mode: t -*-
1542+
1543+/*
1544+ * Copyright (C) 2014-2015 Canonical Ltd
1545+ *
1546+ * This program is free software: you can redistribute it and/or modify
1547+ * it under the terms of the GNU General Public License version 3 as
1548+ * published by the Free Software Foundation.
1549+ *
1550+ * This program is distributed in the hope that it will be useful,
1551+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1552+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1553+ * GNU General Public License for more details.
1554+ *
1555+ * You should have received a copy of the GNU General Public License
1556+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1557+ *
1558+ */
1559+
1560+package husk_test
1561+
1562+import (
1563+ "fmt"
1564+ "io/ioutil"
1565+ "os"
1566+ "path/filepath"
1567+
1568+ "launchpad.net/snappy/dirs"
1569+ "launchpad.net/snappy/pkg/husk"
1570+)
1571+
1572+func ExampleHusk() {
1573+ d, _ := ioutil.TempDir("", "test-xyzzy-")
1574+ defer os.RemoveAll(d)
1575+ dirs.SetRootDir(d)
1576+ os.MkdirAll(filepath.Join(dirs.SnapDataDir, "foo.bar", "0.1"), 0755)
1577+ os.MkdirAll(filepath.Join(dirs.SnapDataDir, "foo.bar", "0.2"), 0755)
1578+ os.MkdirAll(filepath.Join(dirs.SnapDataDir, "foo.bar", "0.5"), 0755)
1579+ os.MkdirAll(filepath.Join(dirs.SnapDataDir, "baz", "0.4"), 0755)
1580+ os.MkdirAll(filepath.Join(dirs.SnapDataDir, "qux", "0.5"), 0755)
1581+ os.MkdirAll(filepath.Join(dirs.SnapOemDir, "qux", "0.5"), 0755)
1582+
1583+ husks := husk.All()
1584+
1585+ for _, k := range []string{"foo.bar", "baz", "qux"} {
1586+ h := husks[k]
1587+ fmt.Printf("Found %d versions for %s, type %q: %s\n",
1588+ len(h.Versions), h.QualifiedName(), h.Type, h.Versions)
1589+ }
1590+ // Output:
1591+ // Found 3 versions for foo.bar, type "app": [0.5 0.2 0.1]
1592+ // Found 1 versions for baz, type "framework": [0.4]
1593+ // Found 1 versions for qux, type "oem": [0.5]
1594+}
1595
1596=== added file 'pkg/husk/husk.go'
1597--- pkg/husk/husk.go 1970-01-01 00:00:00 +0000
1598+++ pkg/husk/husk.go 2015-10-07 12:49:07 +0000
1599@@ -0,0 +1,508 @@
1600+// -*- Mode: Go; indent-tabs-mode: t -*-
1601+
1602+/*
1603+ * Copyright (C) 2014-2015 Canonical Ltd
1604+ *
1605+ * This program is free software: you can redistribute it and/or modify
1606+ * it under the terms of the GNU General Public License version 3 as
1607+ * published by the Free Software Foundation.
1608+ *
1609+ * This program is distributed in the hope that it will be useful,
1610+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1611+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1612+ * GNU General Public License for more details.
1613+ *
1614+ * You should have received a copy of the GNU General Public License
1615+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1616+ *
1617+ */
1618+
1619+// Package husk provides a quick way of loading things that can become snaps.
1620+//
1621+// A husk has a name and n versions; it might not even know its origin.
1622+package husk
1623+
1624+import (
1625+ "errors"
1626+ "fmt"
1627+ "os"
1628+ "path/filepath"
1629+ "sort"
1630+ "strconv"
1631+ "strings"
1632+
1633+ "launchpad.net/snappy/dirs"
1634+ "launchpad.net/snappy/helpers"
1635+ "launchpad.net/snappy/pkg"
1636+ "launchpad.net/snappy/pkg/removed"
1637+ "launchpad.net/snappy/snappy"
1638+)
1639+
1640+// split a path into the name and extension of the directory, and the file.
1641+// e.g. foo/bar.baz/quux -> bar, baz, quux
1642+//
1643+// panics if given path is lacking at least one separator (ie bar/quux
1644+// works (barely); quux panics)
1645+func split(path string) (name string, ext string, file string) {
1646+ idxFileSep := strings.LastIndexByte(path, os.PathSeparator)
1647+ if idxFileSep < 0 {
1648+ panic("bad path given to split: must have at least two separators")
1649+ }
1650+
1651+ file = path[idxFileSep+1:]
1652+ path = path[:idxFileSep]
1653+ name = path
1654+
1655+ idxDirSep := strings.LastIndexByte(path, os.PathSeparator)
1656+ if idxDirSep > -1 {
1657+ name = path[idxDirSep+1:]
1658+ }
1659+
1660+ idxOrig := strings.LastIndexByte(name, '.')
1661+ if idxOrig < 0 {
1662+ return name, "", file
1663+ }
1664+
1665+ return name[:idxOrig], name[idxOrig+1:], file
1666+}
1667+
1668+// extract the name, origin and list of versions from a list of paths that
1669+// end {name}[.{origin}]/{version}. If the origin changes, stop and
1670+// return the versions so far, and the remaining paths.
1671+func extract(paths []string) (string, string, []string, []string) {
1672+ name, origin, _ := split(paths[0])
1673+
1674+ var versions []string
1675+ for len(paths) > 0 {
1676+ n, o, v := split(paths[0])
1677+ if name != n || origin != o {
1678+ break
1679+ }
1680+
1681+ versions = append(versions, v)
1682+ paths = paths[1:]
1683+ }
1684+
1685+ return name, origin, versions, paths
1686+}
1687+
1688+func versionSort(versions []string) {
1689+ sort.Sort(sort.Reverse(snappy.ByVersion(versions)))
1690+}
1691+
1692+// ByName finds husks with the given name.
1693+func ByName(name string, origin string) *Husk {
1694+ if strings.ContainsAny(name, ".*?/") || strings.ContainsAny(origin, ".*?/") {
1695+ panic("invalid name " + name + "." + origin)
1696+ }
1697+
1698+ for _, v := range find(name, origin) {
1699+ return v
1700+ }
1701+
1702+ return nil
1703+}
1704+
1705+// All the husks in the system.
1706+func All() map[string]*Husk {
1707+ return find("*", "*")
1708+}
1709+
1710+type repo interface {
1711+ All() ([]snappy.Part, error)
1712+}
1713+
1714+func newCoreRepoImpl() repo {
1715+ return snappy.NewSystemImageRepository()
1716+}
1717+
1718+var newCoreRepo = newCoreRepoImpl
1719+
1720+func find(name string, origin string) map[string]*Husk {
1721+ husks := make(map[string]*Husk)
1722+
1723+ if (name == snappy.SystemImagePartName || name == "*") && (origin == snappy.SystemImagePartOrigin || origin == "*") {
1724+ // TODO: make this do less work
1725+ repo := newCoreRepo()
1726+ parts, err := repo.All()
1727+ if err != nil {
1728+ // can't really happen
1729+ panic(fmt.Sprintf("Bad SystemImageRepository: %v", err))
1730+ }
1731+
1732+ // parts can be empty during testing for example
1733+ if len(parts) > 0 {
1734+ versions := make([]string, len(parts))
1735+ for i, part := range parts {
1736+ versions[i] = part.Version()
1737+ }
1738+ versionSort(versions)
1739+
1740+ husk := &Husk{
1741+ Name: snappy.SystemImagePartName,
1742+ Origin: snappy.SystemImagePartOrigin,
1743+ Type: pkg.TypeCore,
1744+ Versions: versions,
1745+ concrete: &concreteCore{},
1746+ }
1747+ husks[husk.QualifiedName()] = husk
1748+ }
1749+ }
1750+
1751+ type T struct {
1752+ inst string
1753+ qn string
1754+ typ pkg.Type
1755+ }
1756+
1757+ for _, s := range []T{
1758+ {dirs.SnapAppsDir, name + "." + origin, pkg.TypeApp},
1759+ {dirs.SnapAppsDir, name, pkg.TypeFramework},
1760+ } {
1761+ paths, _ := filepath.Glob(filepath.Join(dirs.SnapDataDir, s.qn, "*"))
1762+ for len(paths) > 0 {
1763+ var name string
1764+ var origin string
1765+ var versions []string
1766+
1767+ name, origin, versions, paths = extract(paths)
1768+ if origin != "" && s.typ != pkg.TypeApp {
1769+ // this happens when called with name="*"
1770+ continue
1771+ }
1772+
1773+ versionSort(versions)
1774+
1775+ if s.typ == pkg.TypeFramework && helpers.FileExists(filepath.Join(dirs.SnapOemDir, name)) {
1776+ s.typ = pkg.TypeOem
1777+ s.inst = dirs.SnapOemDir
1778+ }
1779+
1780+ husk := &Husk{
1781+ Name: name,
1782+ Origin: origin,
1783+ Type: s.typ,
1784+ Versions: versions,
1785+ }
1786+
1787+ husk.concrete = NewConcrete(husk, s.inst)
1788+
1789+ husks[husk.QualifiedName()] = husk
1790+ }
1791+ }
1792+
1793+ return husks
1794+}
1795+
1796+// A Husk is a lightweight object that represents and knows how to
1797+// load a Part on demand.
1798+type Husk struct {
1799+ Name string
1800+ Origin string
1801+ Type pkg.Type
1802+ Versions []string
1803+ concrete Concreter
1804+}
1805+
1806+// Concreter hides the part-specific details of husks
1807+type Concreter interface {
1808+ IsInstalled(string) bool
1809+ ActiveIndex() int
1810+ Load(string) (snappy.Part, error)
1811+}
1812+
1813+// NewConcrete is meant to be overridden in tests; is called when
1814+// needing a Concreter for app/fmk/oem snaps (ie not core).
1815+var NewConcrete = newConcreteImpl
1816+
1817+func newConcreteImpl(husk *Husk, instdir string) Concreter {
1818+ return &concreteSnap{
1819+ self: husk,
1820+ instdir: instdir,
1821+ }
1822+}
1823+
1824+// QualifiedName of the husk.
1825+//
1826+// because husks read their origin from the filesystem, you don't need
1827+// to check the pacakge type.
1828+func (h *Husk) QualifiedName() string {
1829+ if h.Origin == "" {
1830+ return h.Name
1831+ }
1832+ return h.FullName()
1833+}
1834+
1835+// FullName of the husk
1836+func (h *Husk) FullName() string {
1837+ return h.Name + "." + h.Origin
1838+}
1839+
1840+var (
1841+ // ErrBadVersionIndex is returned by Load when asked to load a
1842+ // non-existent version.
1843+ ErrBadVersionIndex = errors.New("Bad version index")
1844+ // ErrVersionGone is returned in the case where we find a
1845+ // version and it disappears before we get to load it.
1846+ ErrVersionGone = errors.New("Version gone")
1847+)
1848+
1849+type concreteCore struct{}
1850+
1851+func (*concreteCore) IsInstalled(string) bool { return true }
1852+func (*concreteCore) ActiveIndex() int { return 0 }
1853+func (*concreteCore) Load(version string) (snappy.Part, error) {
1854+ parts, err := newCoreRepo().All()
1855+ if err != nil {
1856+ // can't really happen
1857+ return nil, fmt.Errorf("Bad SystemImageRepository: %v", err)
1858+ }
1859+
1860+ for _, part := range parts {
1861+ if part.Version() == version {
1862+ return part, nil
1863+ }
1864+ }
1865+
1866+ return nil, ErrVersionGone
1867+}
1868+
1869+type concreteSnap struct {
1870+ self *Husk
1871+ instdir string
1872+}
1873+
1874+func (c *concreteSnap) IsInstalled(version string) bool {
1875+ return helpers.FileExists(filepath.Join(c.instdir, c.self.QualifiedName(), version, "meta", "package.yaml"))
1876+}
1877+
1878+func (c *concreteSnap) ActiveIndex() int {
1879+ current, err := os.Readlink(filepath.Join(c.instdir, c.self.QualifiedName(), "current"))
1880+ if err != nil {
1881+ return -1
1882+ }
1883+
1884+ current = filepath.Base(current)
1885+
1886+ // Linear search is fine for now.
1887+ //
1888+ // If it ever becomes a problem, remember h.Versions is sorted
1889+ // so you can use go's sort.Search and snappy.VersionCompare,
1890+ // but VersionCompare is not cheap, so that (on my machine, at
1891+ // the time of writing) linear of even 100k versions is only
1892+ // 2×-3× slower than binary; anything below about 50k versions
1893+ // is faster even in worst-case for linear (no match). And
1894+ // that's not even looking at memory impact.
1895+ //
1896+ // For example, on the ~90k lines in /usr/share/dict/words:
1897+ //
1898+ // BenchmarkBinaryPositive-4 10000 135041 ns/op 11534 B/op 316 allocs/op
1899+ // BenchmarkBinaryNegative-4 10000 109803 ns/op 10235 B/op 231 allocs/op
1900+ // BenchmarkLinearPositive-4 5000 272980 ns/op 0 B/op 0 allocs/op
1901+ // BenchmarkLinearNegative-4 5000 362244 ns/op 0 B/op 0 allocs/op
1902+ //
1903+ // on a corpus of 100k %016x-formatted rand.Int63()s:
1904+ //
1905+ // BenchmarkBinaryNegative-4 10000 152797 ns/op 10158 B/op 243 allocs/op
1906+ // BenchmarkLinearNegative-4 10000 163924 ns/op 0 B/op 0 allocs/op
1907+ //
1908+ // ... I think I might need help.
1909+ for i := range c.self.Versions {
1910+ if c.self.Versions[i] == current {
1911+ return i
1912+ }
1913+ }
1914+
1915+ return -1
1916+}
1917+
1918+func (c *concreteSnap) Load(version string) (snappy.Part, error) {
1919+ yamlPath := filepath.Join(c.instdir, c.self.QualifiedName(), version, "meta", "package.yaml")
1920+ if !helpers.FileExists(yamlPath) {
1921+ return removed.New(c.self.Name, c.self.Origin, version, c.self.Type), nil
1922+ }
1923+
1924+ part, err := snappy.NewInstalledSnapPart(yamlPath, c.self.Origin)
1925+ if err != nil {
1926+ return nil, err
1927+ }
1928+
1929+ return part, nil
1930+}
1931+
1932+// IsInstalled checks whether the given part is installed
1933+func (h *Husk) IsInstalled(idx int) bool {
1934+ if idx < 0 || idx >= len(h.Versions) {
1935+ return false
1936+ }
1937+
1938+ return h.concrete.IsInstalled(h.Versions[idx])
1939+}
1940+
1941+// ActiveIndex returns the index of the active version, or -1
1942+func (h *Husk) ActiveIndex() int {
1943+ if h == nil || len(h.Versions) == 0 {
1944+ return -1
1945+ }
1946+
1947+ return h.concrete.ActiveIndex()
1948+}
1949+
1950+// Load a Part from the Husk
1951+func (h *Husk) Load(versionIdx int) (snappy.Part, error) {
1952+ if h == nil {
1953+ return nil, nil
1954+ }
1955+
1956+ if versionIdx < 0 || versionIdx >= len(h.Versions) {
1957+ return nil, ErrBadVersionIndex
1958+ }
1959+
1960+ version := h.Versions[versionIdx]
1961+
1962+ return h.concrete.Load(version)
1963+}
1964+
1965+// LoadBest looks for the best candidate Part and loads it.
1966+//
1967+// If there is an active part, load that. Otherwise, load the
1968+// highest-versioned installed part. Otherwise, load the first removed
1969+// part.
1970+//
1971+// If not even a removed part can be loaded, something is wrong. Nil
1972+// is returned, but you're in trouble (did the filesystem just
1973+// disappear under us?).
1974+func (h *Husk) LoadBest() snappy.Part {
1975+ if h == nil {
1976+ return nil
1977+ }
1978+ if len(h.Versions) == 0 {
1979+ return nil
1980+ }
1981+
1982+ activeIdx := h.ActiveIndex()
1983+ if part, err := h.Load(activeIdx); err == nil {
1984+ return part
1985+ }
1986+
1987+ for i := 0; i < len(h.Versions); i++ {
1988+ if h.IsInstalled(i) {
1989+ if part, err := h.Load(i); err == nil {
1990+ return part
1991+ }
1992+ }
1993+ }
1994+
1995+ part, _ := h.Load(0)
1996+
1997+ return part
1998+}
1999+
2000+// Map this husk into a map[string]string, augmenting it with the
2001+// given (purportedly remote) Part.
2002+//
2003+// It is a programming error (->panic) to call Map on a nil *Husk with
2004+// a nil Part. Husk or part may be nil, but not both.
2005+//
2006+// Also may panic if the remote part is nil and LoadBest can't load a
2007+// Part at all.
2008+func (h *Husk) Map(remotePart snappy.Part) map[string]string {
2009+ var version, update, rollback, icon, name, origin, _type, vendor, description string
2010+
2011+ if h == nil && remotePart == nil {
2012+ panic("husk & part both nil -- how did i even get here")
2013+ }
2014+
2015+ status := "not installed"
2016+ installedSize := "-1"
2017+ downloadSize := "-1"
2018+
2019+ part := h.LoadBest()
2020+ if part != nil {
2021+ if part.IsActive() {
2022+ status = "active"
2023+ } else if part.IsInstalled() {
2024+ status = "installed"
2025+ } else {
2026+ status = "removed"
2027+ }
2028+ } else if remotePart == nil {
2029+ panic("unable to load a valid part")
2030+ }
2031+
2032+ if part != nil {
2033+ name = part.Name()
2034+ origin = part.Origin()
2035+ version = part.Version()
2036+ _type = string(part.Type())
2037+
2038+ icon = part.Icon()
2039+ vendor = part.Vendor()
2040+ description = part.Description()
2041+ installedSize = strconv.FormatInt(part.InstalledSize(), 10)
2042+
2043+ downloadSize = strconv.FormatInt(part.DownloadSize(), 10)
2044+ } else {
2045+ name = remotePart.Name()
2046+ origin = remotePart.Origin()
2047+ version = remotePart.Version()
2048+ _type = string(remotePart.Type())
2049+ }
2050+
2051+ if remotePart != nil {
2052+ if icon == "" {
2053+ icon = remotePart.Icon()
2054+ }
2055+ if description == "" {
2056+ description = remotePart.Description()
2057+ }
2058+ if vendor == "" {
2059+ vendor = remotePart.Vendor()
2060+ }
2061+
2062+ downloadSize = strconv.FormatInt(remotePart.DownloadSize(), 10)
2063+ }
2064+
2065+ if activeIdx := h.ActiveIndex(); activeIdx >= 0 {
2066+ if remotePart != nil && version != remotePart.Version() {
2067+ // XXX: this does not handle the case where the
2068+ // one in the store is not the greatest version
2069+ // (e.g.: store has 1.1, locally available 1.1,
2070+ // 1.2, active 1.2)
2071+ update = remotePart.Version()
2072+ }
2073+
2074+ for i := activeIdx + 1; i < len(h.Versions); i++ {
2075+ // XXX: it's also possible to "roll back" to a
2076+ // store version in the case mentioned above;
2077+ // also not covered by this code.
2078+ if h.IsInstalled(i) {
2079+ rollback = h.Versions[i]
2080+ break
2081+ }
2082+ }
2083+ }
2084+
2085+ result := map[string]string{
2086+ "icon": icon,
2087+ "name": name,
2088+ "origin": origin,
2089+ "status": status,
2090+ "type": _type,
2091+ "vendor": vendor,
2092+ "version": version,
2093+ "description": description,
2094+ "installed_size": installedSize,
2095+ "download_size": downloadSize,
2096+ }
2097+
2098+ if rollback != "" {
2099+ result["rollback_available"] = rollback
2100+ }
2101+
2102+ if update != "" {
2103+ result["update_available"] = update
2104+ }
2105+
2106+ return result
2107+}
2108
2109=== added file 'pkg/husk/husk_test.go'
2110--- pkg/husk/husk_test.go 1970-01-01 00:00:00 +0000
2111+++ pkg/husk/husk_test.go 2015-10-07 12:49:07 +0000
2112@@ -0,0 +1,412 @@
2113+// -*- Mode: Go; indent-tabs-mode: t -*-
2114+
2115+/*
2116+ * Copyright (C) 2014-2015 Canonical Ltd
2117+ *
2118+ * This program is free software: you can redistribute it and/or modify
2119+ * it under the terms of the GNU General Public License version 3 as
2120+ * published by the Free Software Foundation.
2121+ *
2122+ * This program is distributed in the hope that it will be useful,
2123+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
2124+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2125+ * GNU General Public License for more details.
2126+ *
2127+ * You should have received a copy of the GNU General Public License
2128+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
2129+ *
2130+ */
2131+
2132+package husk
2133+
2134+import (
2135+ "fmt"
2136+ "io/ioutil"
2137+ "os"
2138+ "path/filepath"
2139+ "testing"
2140+
2141+ "gopkg.in/check.v1"
2142+ "gopkg.in/yaml.v2"
2143+
2144+ "launchpad.net/snappy/dirs"
2145+ "launchpad.net/snappy/pkg"
2146+ "launchpad.net/snappy/pkg/remote"
2147+ "launchpad.net/snappy/pkg/removed"
2148+ "launchpad.net/snappy/snappy"
2149+)
2150+
2151+type huskSuite struct {
2152+ d string
2153+}
2154+
2155+func Test(t *testing.T) { check.TestingT(t) }
2156+
2157+var _ = check.Suite(&huskSuite{})
2158+
2159+func (s *huskSuite) SetUpTest(c *check.C) {
2160+ s.d = c.MkDir()
2161+ dirs.SetRootDir(s.d)
2162+
2163+ s.MkInstalled(c, pkg.TypeApp, dirs.SnapAppsDir, "foo", "bar", "1.0", true)
2164+ s.MkRemoved(c, "foo.bar", "0.9")
2165+ s.MkRemoved(c, "foo.baz", "0.8")
2166+
2167+ s.MkInstalled(c, pkg.TypeFramework, dirs.SnapAppsDir, "fmk", "", "123", false)
2168+ s.MkInstalled(c, pkg.TypeFramework, dirs.SnapAppsDir, "fmk", "", "120", true)
2169+ s.MkInstalled(c, pkg.TypeFramework, dirs.SnapAppsDir, "fmk", "", "119", false)
2170+ s.MkRemoved(c, "fmk", "12a1")
2171+
2172+ s.MkRemoved(c, "fmk2", "4.2.0ubuntu1")
2173+
2174+ s.MkInstalled(c, pkg.TypeOem, dirs.SnapOemDir, "oem", "", "3", false)
2175+
2176+ newCoreRepo = func() repo {
2177+ // you can't ever have a removed systemimagepart, but for testing it'll do
2178+ return mockrepo{removed.New(snappy.SystemImagePartName, snappy.SystemImagePartOrigin, "1", pkg.TypeCore)}
2179+ }
2180+}
2181+
2182+func (s *huskSuite) TearDownTest(c *check.C) {
2183+ newCoreRepo = newCoreRepoImpl
2184+}
2185+
2186+func (s *huskSuite) MkInstalled(c *check.C, _type pkg.Type, appdir, name, origin, version string, active bool) {
2187+ qn := name
2188+ if origin != "" {
2189+ qn += "." + origin
2190+ }
2191+
2192+ s.MkRemoved(c, qn, version)
2193+
2194+ apath := filepath.Join(appdir, qn, version, "meta")
2195+ yaml := fmt.Sprintf("name: %s\nversion: %s\nvendor: example.com\nicon: icon.png\ntype: %s\n", name, version, _type)
2196+ c.Check(os.MkdirAll(apath, 0755), check.IsNil)
2197+ c.Check(ioutil.WriteFile(filepath.Join(apath, "package.yaml"), []byte(yaml), 0644), check.IsNil)
2198+ c.Check(ioutil.WriteFile(filepath.Join(apath, "hashes.yaml"), nil, 0644), check.IsNil)
2199+
2200+ if active {
2201+ c.Check(os.Symlink(version, filepath.Join(appdir, qn, "current")), check.IsNil)
2202+ }
2203+}
2204+
2205+func (s *huskSuite) MkRemoved(c *check.C, qn, version string) {
2206+ dpath := filepath.Join(dirs.SnapDataDir, qn, version)
2207+ c.Check(os.MkdirAll(dpath, 0755), check.IsNil)
2208+ c.Check(ioutil.WriteFile(filepath.Join(dpath, "test.txt"), []byte("hello there\n"), 0644), check.IsNil)
2209+
2210+}
2211+
2212+func (s *huskSuite) TestLoadBadName(c *check.C) {
2213+ c.Check(func() { ByName("*", "*") }, check.PanicMatches, "invalid name .*")
2214+}
2215+
2216+func (s *huskSuite) TestMapFmkNoPart(c *check.C) {
2217+ h := ByName("fmk", "sideload")
2218+ m := h.Map(nil)
2219+ c.Check(m, check.DeepEquals, map[string]string{
2220+ "name": "fmk",
2221+ "origin": "sideload",
2222+ "status": "active",
2223+ "version": "120",
2224+ "icon": filepath.Join(s.d, "apps", "fmk", "120", "icon.png"),
2225+ "type": "framework",
2226+ "vendor": "example.com",
2227+ "installed_size": "214", // this'll change :-/
2228+ "download_size": "-1",
2229+ "description": "",
2230+ "rollback_available": "119",
2231+ })
2232+}
2233+
2234+func (s *huskSuite) TestMapRemovedFmkNoPart(c *check.C) {
2235+ h := ByName("fmk2", "sideload")
2236+ m := h.Map(nil)
2237+ c.Check(m, check.DeepEquals, map[string]string{
2238+ "name": "fmk2",
2239+ "origin": "sideload",
2240+ "status": "removed",
2241+ "version": "4.2.0ubuntu1",
2242+ "icon": "",
2243+ "type": "framework",
2244+ "vendor": "",
2245+ "installed_size": "-1",
2246+ "download_size": "-1",
2247+ "description": "",
2248+ })
2249+}
2250+
2251+func (s *huskSuite) TestMapRemovedFmkNoPartButStoreMeta(c *check.C) {
2252+ snap := remote.Snap{
2253+ Name: "fmk2",
2254+ Origin: "fmk2origin",
2255+ Version: "4.2.0ubuntu1",
2256+ Type: pkg.TypeFramework,
2257+ IconURL: "http://example.com/icon",
2258+ DownloadSize: 42,
2259+ Publisher: "Example Inc.",
2260+ }
2261+ part := snappy.NewRemoteSnapPart(snap)
2262+
2263+ content, err := yaml.Marshal(snap)
2264+ c.Assert(err, check.IsNil)
2265+
2266+ p := snappy.ManifestPath(part)
2267+ c.Assert(os.MkdirAll(filepath.Dir(p), 0755), check.IsNil)
2268+ c.Assert(ioutil.WriteFile(p, content, 0644), check.IsNil)
2269+
2270+ h := ByName("fmk2", "fmk2origin")
2271+ m := h.Map(nil)
2272+ c.Check(m, check.DeepEquals, map[string]string{
2273+ "name": "fmk2",
2274+ "origin": "fmk2origin",
2275+ "status": "removed",
2276+ "version": "4.2.0ubuntu1",
2277+ "icon": "http://example.com/icon",
2278+ "type": "framework",
2279+ "vendor": "Example Inc.",
2280+ "installed_size": "-1",
2281+ "download_size": "42",
2282+ "description": "",
2283+ })
2284+}
2285+
2286+func (s *huskSuite) TestMapAppNoPart(c *check.C) {
2287+ h := ByName("foo", "bar")
2288+ m := h.Map(nil)
2289+ c.Check(m, check.DeepEquals, map[string]string{
2290+ "name": "foo",
2291+ "origin": "bar",
2292+ "status": "active",
2293+ "version": "1.0",
2294+ "icon": filepath.Join(s.d, "apps", "foo.bar", "1.0", "icon.png"),
2295+ "type": "app",
2296+ "vendor": "example.com",
2297+ "installed_size": "208", // this'll change :-/
2298+ "download_size": "-1",
2299+ "description": "",
2300+ })
2301+}
2302+
2303+func (s *huskSuite) TestMapAppWithPart(c *check.C) {
2304+ snap := remote.Snap{
2305+ Name: "foo",
2306+ Origin: "bar",
2307+ Version: "2",
2308+ Type: pkg.TypeApp,
2309+ IconURL: "http://example.com/icon",
2310+ DownloadSize: 42,
2311+ }
2312+ part := snappy.NewRemoteSnapPart(snap)
2313+
2314+ h := ByName("foo", "bar")
2315+ m := h.Map(part)
2316+ c.Check(m, check.DeepEquals, map[string]string{
2317+ "name": "foo",
2318+ "origin": "bar",
2319+ "status": "active",
2320+ "version": "1.0",
2321+ "icon": filepath.Join(s.d, "apps", "foo.bar", "1.0", "icon.png"),
2322+ "type": "app",
2323+ "vendor": "example.com",
2324+ "installed_size": "208", // this'll change :-/
2325+ "download_size": "42",
2326+ "description": "",
2327+ "update_available": "2",
2328+ })
2329+}
2330+
2331+func (s *huskSuite) TestMapAppNoHusk(c *check.C) {
2332+ snap := remote.Snap{
2333+ Name: "foo",
2334+ Origin: "bar",
2335+ Version: "2",
2336+ Type: pkg.TypeApp,
2337+ IconURL: "http://example.com/icon",
2338+ Publisher: "example.com",
2339+ DownloadSize: 42,
2340+ }
2341+ part := snappy.NewRemoteSnapPart(snap)
2342+
2343+ m := (*Husk)(nil).Map(part)
2344+ c.Check(m, check.DeepEquals, map[string]string{
2345+ "name": "foo",
2346+ "origin": "bar",
2347+ "status": "not installed",
2348+ "version": "2",
2349+ "icon": snap.IconURL,
2350+ "type": "app",
2351+ "vendor": "example.com",
2352+ "installed_size": "-1",
2353+ "download_size": "42",
2354+ "description": "",
2355+ })
2356+
2357+}
2358+
2359+func (s *huskSuite) TestMapRemovedAppNoPart(c *check.C) {
2360+ h := ByName("foo", "baz")
2361+ m := h.Map(nil)
2362+ c.Check(m, check.DeepEquals, map[string]string{
2363+ "name": "foo",
2364+ "origin": "baz",
2365+ "status": "removed",
2366+ "version": "0.8",
2367+ "icon": "",
2368+ "type": "app",
2369+ "vendor": "",
2370+ "installed_size": "-1",
2371+ "download_size": "-1",
2372+ "description": "",
2373+ })
2374+}
2375+
2376+func (s *huskSuite) TestMapInactiveOemNoPart(c *check.C) {
2377+ h := ByName("oem", "canonical")
2378+ m := h.Map(nil)
2379+ c.Check(m, check.DeepEquals, map[string]string{
2380+ "name": "oem",
2381+ "origin": "sideload", // best guess
2382+ "status": "installed",
2383+ "version": "3",
2384+ "icon": filepath.Join(s.d, "oem", "oem", "3", "icon.png"),
2385+ "type": "oem",
2386+ "vendor": "example.com",
2387+ "installed_size": "206",
2388+ "download_size": "-1",
2389+ "description": "",
2390+ })
2391+}
2392+
2393+func (s *huskSuite) TestLoadBadApp(c *check.C) {
2394+ s.MkRemoved(c, "quux.blah", "1")
2395+ // an unparsable package.yaml:
2396+ c.Check(os.MkdirAll(filepath.Join(dirs.SnapAppsDir, "quux.blah", "1", "meta", "package.yaml"), 0755), check.IsNil)
2397+
2398+ h := ByName("quux", "blah")
2399+ c.Assert(h, check.NotNil)
2400+ c.Assert(h.Versions, check.DeepEquals, []string{"1"})
2401+
2402+ p, err := h.Load(0)
2403+ c.Check(err, check.NotNil)
2404+ c.Check(p, check.IsNil)
2405+ c.Check(p == nil, check.Equals, true) // NOTE this is stronger than the above
2406+}
2407+
2408+func (s *huskSuite) TestLoadFmk(c *check.C) {
2409+ h := ByName("fmk", "")
2410+ c.Assert(h, check.NotNil)
2411+ c.Assert(h.Versions, check.HasLen, 4)
2412+ // versions are sorted backwards by version -- index 0 is always newest version
2413+ c.Check(h.Versions, check.DeepEquals, []string{"123", "120", "119", "12a1"})
2414+ // other things are as expected
2415+ c.Check(h.Name, check.Equals, "fmk")
2416+ c.Check(h.Type, check.Equals, pkg.TypeFramework)
2417+ c.Check(h.ActiveIndex(), check.Equals, 1)
2418+
2419+ c.Check(h.IsInstalled(0), check.Equals, true)
2420+ p, err := h.Load(0)
2421+ c.Check(err, check.IsNil)
2422+ // load loaded the right implementation of Part
2423+ c.Check(p, check.FitsTypeOf, new(snappy.SnapPart))
2424+ c.Check(p.IsActive(), check.Equals, false)
2425+ c.Check(p.Version(), check.Equals, "123")
2426+
2427+ c.Check(h.IsInstalled(1), check.Equals, true)
2428+ p, err = h.Load(1)
2429+ c.Check(err, check.IsNil)
2430+ c.Check(p, check.FitsTypeOf, new(snappy.SnapPart))
2431+ c.Check(p.IsActive(), check.Equals, true)
2432+ c.Check(p.Version(), check.Equals, "120")
2433+
2434+ c.Check(h.IsInstalled(2), check.Equals, true)
2435+ p, err = h.Load(2)
2436+ c.Check(err, check.IsNil)
2437+ c.Check(p, check.FitsTypeOf, new(snappy.SnapPart))
2438+ c.Check(p.IsActive(), check.Equals, false)
2439+ c.Check(p.Version(), check.Equals, "119")
2440+
2441+ c.Check(h.IsInstalled(3), check.Equals, false)
2442+ p, err = h.Load(3)
2443+ c.Check(err, check.IsNil)
2444+ c.Check(p, check.FitsTypeOf, new(removed.Removed))
2445+ c.Check(p.Version(), check.Equals, "12a1")
2446+
2447+ _, err = h.Load(42)
2448+ c.Check(err, check.Equals, ErrBadVersionIndex)
2449+
2450+}
2451+
2452+func (s *huskSuite) TestLoadApp(c *check.C) {
2453+ h0 := ByName("foo", "bar")
2454+ h1 := ByName("foo", "baz")
2455+
2456+ c.Check(h0.QualifiedName(), check.Equals, "foo.bar")
2457+ c.Check(h0.Versions, check.DeepEquals, []string{"1.0", "0.9"})
2458+ c.Check(h0.Type, check.Equals, pkg.TypeApp)
2459+ c.Check(h0.ActiveIndex(), check.Equals, 0)
2460+
2461+ c.Check(h1.QualifiedName(), check.Equals, "foo.baz")
2462+ c.Check(h1.Versions, check.DeepEquals, []string{"0.8"})
2463+ c.Check(h1.Type, check.Equals, pkg.TypeApp)
2464+ c.Check(h1.ActiveIndex(), check.Equals, -1)
2465+
2466+ c.Check(h0.IsInstalled(0), check.Equals, true)
2467+ p, err := h0.Load(0)
2468+ c.Check(err, check.IsNil)
2469+ c.Check(p, check.FitsTypeOf, new(snappy.SnapPart))
2470+ c.Check(p.IsActive(), check.Equals, true)
2471+ c.Check(p.Version(), check.Equals, "1.0")
2472+
2473+ c.Check(h0.IsInstalled(1), check.Equals, false)
2474+ p, err = h0.Load(1)
2475+ c.Check(err, check.IsNil)
2476+ c.Check(p, check.FitsTypeOf, new(removed.Removed))
2477+ c.Check(p.IsActive(), check.Equals, false)
2478+ c.Check(p.Version(), check.Equals, "0.9")
2479+
2480+ c.Check(h1.IsInstalled(0), check.Equals, false)
2481+ p, err = h1.Load(0)
2482+ c.Check(err, check.IsNil)
2483+ c.Check(p, check.FitsTypeOf, new(removed.Removed))
2484+ c.Check(p.IsActive(), check.Equals, false)
2485+ c.Check(p.Version(), check.Equals, "0.8")
2486+}
2487+
2488+func (s *huskSuite) TestLoadOem(c *check.C) {
2489+ oem := ByName("oem", "whatever")
2490+ c.Assert(oem, check.NotNil)
2491+ c.Check(oem.Versions, check.DeepEquals, []string{"3"})
2492+ c.Check(oem.Type, check.Equals, pkg.TypeOem)
2493+
2494+ c.Check(oem.IsInstalled(0), check.Equals, true)
2495+ c.Check(oem.ActiveIndex(), check.Equals, -1)
2496+ p, err := oem.Load(0)
2497+ c.Check(err, check.IsNil)
2498+ c.Check(p, check.FitsTypeOf, new(snappy.SnapPart))
2499+ c.Check(p.Version(), check.Equals, "3")
2500+}
2501+
2502+type mockrepo struct{ p snappy.Part }
2503+
2504+func (r mockrepo) All() ([]snappy.Part, error) {
2505+ return []snappy.Part{r.p}, nil
2506+}
2507+
2508+func (s *huskSuite) TestLoadCore(c *check.C) {
2509+ core := ByName(snappy.SystemImagePartName, snappy.SystemImagePartOrigin)
2510+ c.Assert(core, check.NotNil)
2511+ c.Check(core.Versions, check.DeepEquals, []string{"1"})
2512+
2513+ c.Check(core.IsInstalled(0), check.Equals, true)
2514+ c.Check(core.ActiveIndex(), check.Equals, 0)
2515+ p, err := core.Load(0)
2516+ c.Check(err, check.IsNil)
2517+ c.Check(p.Version(), check.Equals, "1")
2518+}
2519+
2520+func (s *huskSuite) TestAll(c *check.C) {
2521+ all := All()
2522+
2523+ c.Check(all, check.HasLen, 6) // 2 fmk, 2 app, 1 oem, 1 core
2524+}
2525
2526=== added file 'pkg/husk/split_test.go'
2527--- pkg/husk/split_test.go 1970-01-01 00:00:00 +0000
2528+++ pkg/husk/split_test.go 2015-10-07 12:49:07 +0000
2529@@ -0,0 +1,61 @@
2530+// -*- Mode: Go; indent-tabs-mode: t -*-
2531+
2532+/*
2533+ * Copyright (C) 2014-2015 Canonical Ltd
2534+ *
2535+ * This program is free software: you can redistribute it and/or modify
2536+ * it under the terms of the GNU General Public License version 3 as
2537+ * published by the Free Software Foundation.
2538+ *
2539+ * This program is distributed in the hope that it will be useful,
2540+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
2541+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2542+ * GNU General Public License for more details.
2543+ *
2544+ * You should have received a copy of the GNU General Public License
2545+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
2546+ *
2547+ */
2548+
2549+package husk
2550+
2551+import (
2552+ "gopkg.in/check.v1"
2553+)
2554+
2555+type splitSuite struct{}
2556+
2557+var _ = check.Suite(&splitSuite{})
2558+
2559+func (s *splitSuite) TestSplitRegular(c *check.C) {
2560+ for _, s := range []string{"meh/foo.bar/baz", "foo.bar/baz"} {
2561+ n, e, f := split(s)
2562+ c.Check(n, check.Equals, "foo")
2563+ c.Check(e, check.Equals, "bar")
2564+ c.Check(f, check.Equals, "baz")
2565+ }
2566+}
2567+
2568+func (s *splitSuite) TestSplitExtless(c *check.C) {
2569+ for _, s := range []string{"meh/foo/baz", "foo/baz"} {
2570+ n, e, f := split(s)
2571+ c.Check(n, check.Equals, "foo")
2572+ c.Check(e, check.Equals, "")
2573+ c.Check(f, check.Equals, "baz")
2574+ }
2575+}
2576+
2577+func (s *splitSuite) TestSplitBad(c *check.C) {
2578+ c.Check(func() {
2579+ split("what")
2580+ }, check.PanicMatches, `bad path given.*`)
2581+}
2582+
2583+func (s *splitSuite) TestExtract(c *check.C) {
2584+ ps := []string{"meh/foo.bar/v1", "meh/foo.bar/v2", "meh/foo.baz/v3"}
2585+ n, o, vs, ps := extract(ps)
2586+ c.Check(n, check.Equals, "foo")
2587+ c.Check(o, check.Equals, "bar")
2588+ c.Check(vs, check.DeepEquals, []string{"v1", "v2"})
2589+ c.Check(ps, check.DeepEquals, []string{"meh/foo.baz/v3"})
2590+}
2591
2592=== added directory 'pkg/remote'
2593=== added file 'pkg/remote/remote.go'
2594--- pkg/remote/remote.go 1970-01-01 00:00:00 +0000
2595+++ pkg/remote/remote.go 2015-10-07 12:49:07 +0000
2596@@ -0,0 +1,45 @@
2597+// -*- Mode: Go; indent-tabs-mode: t -*-
2598+
2599+/*
2600+ * Copyright (C) 2014-2015 Canonical Ltd
2601+ *
2602+ * This program is free software: you can redistribute it and/or modify
2603+ * it under the terms of the GNU General Public License version 3 as
2604+ * published by the Free Software Foundation.
2605+ *
2606+ * This program is distributed in the hope that it will be useful,
2607+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
2608+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2609+ * GNU General Public License for more details.
2610+ *
2611+ * You should have received a copy of the GNU General Public License
2612+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
2613+ *
2614+ */
2615+
2616+package remote
2617+
2618+import (
2619+ "launchpad.net/snappy/pkg"
2620+)
2621+
2622+// A Snap encapsulates the data sent to us from the store.
2623+type Snap struct {
2624+ Alias string `json:"alias,omitempty"`
2625+ AnonDownloadURL string `json:"anon_download_url,omitempty"`
2626+ DownloadSha512 string `json:"download_sha512,omitempty"`
2627+ Description string `json:"description,omitempty"`
2628+ DownloadSize int64 `json:"binary_filesize,omitempty"`
2629+ DownloadURL string `json:"download_url,omitempty"`
2630+ IconURL string `json:"icon_url"`
2631+ LastUpdated string `json:"last_updated,omitempty"`
2632+ Name string `json:"package_name"`
2633+ Origin string `json:"origin"`
2634+ Prices map[string]float64 `json:"prices,omitempty"`
2635+ Publisher string `json:"publisher,omitempty"`
2636+ RatingsAverage float64 `json:"ratings_average,omitempty"`
2637+ SupportURL string `json:"support_url"`
2638+ Title string `json:"title"`
2639+ Type pkg.Type `json:"content,omitempty"`
2640+ Version string `json:"version"`
2641+}
2642
2643=== added directory 'pkg/removed'
2644=== added file 'pkg/removed/removed.go'
2645--- pkg/removed/removed.go 1970-01-01 00:00:00 +0000
2646+++ pkg/removed/removed.go 2015-10-07 12:49:07 +0000
2647@@ -0,0 +1,156 @@
2648+// -*- Mode: Go; indent-tabs-mode: t -*-
2649+
2650+/*
2651+ * Copyright (C) 2014-2015 Canonical Ltd
2652+ *
2653+ * This program is free software: you can redistribute it and/or modify
2654+ * it under the terms of the GNU General Public License version 3 as
2655+ * published by the Free Software Foundation.
2656+ *
2657+ * This program is distributed in the hope that it will be useful,
2658+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
2659+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2660+ * GNU General Public License for more details.
2661+ *
2662+ * You should have received a copy of the GNU General Public License
2663+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
2664+ *
2665+ */
2666+
2667+// Package removed implements Removed packages, that are packages that
2668+// have been installed, removed, but not purged: there is no
2669+// application, but there might be data.
2670+package removed
2671+
2672+import (
2673+ "errors"
2674+ "io/ioutil"
2675+ "time"
2676+
2677+ "gopkg.in/yaml.v2"
2678+
2679+ "launchpad.net/snappy/pkg"
2680+ "launchpad.net/snappy/pkg/remote"
2681+ "launchpad.net/snappy/progress"
2682+ "launchpad.net/snappy/snappy"
2683+)
2684+
2685+// ErrRemoved is returned when you ask to operate on a removed package.
2686+var ErrRemoved = errors.New("package is removed")
2687+
2688+// Removed represents a removed package.
2689+type Removed struct {
2690+ name string
2691+ origin string
2692+ version string
2693+ pkgType pkg.Type
2694+ remote *remote.Snap
2695+}
2696+
2697+// New removed package.
2698+func New(name, origin, version string, pkgType pkg.Type) snappy.Part {
2699+ part := &Removed{
2700+ name: name,
2701+ origin: origin,
2702+ version: version,
2703+ pkgType: pkgType,
2704+ }
2705+
2706+ content, _ := ioutil.ReadFile(snappy.ManifestPath(part))
2707+ yaml.Unmarshal(content, &(part.remote))
2708+
2709+ return part
2710+}
2711+
2712+// Name from the snappy.Part interface
2713+func (r *Removed) Name() string { return r.name }
2714+
2715+// Version from the snappy.Part interface
2716+func (r *Removed) Version() string { return r.version }
2717+
2718+// Description from the snappy.Part interface
2719+func (r *Removed) Description() string {
2720+ if r.remote != nil {
2721+ return r.remote.Description
2722+ }
2723+
2724+ return ""
2725+}
2726+
2727+// Origin from the snappy.Part interface
2728+func (r *Removed) Origin() string {
2729+ if r.remote != nil {
2730+ return r.remote.Origin
2731+ }
2732+ if r.origin == "" {
2733+ return snappy.SideloadedOrigin
2734+ }
2735+ return r.origin
2736+}
2737+
2738+// Vendor from the snappy.Part interface
2739+func (r *Removed) Vendor() string {
2740+ if r.remote != nil {
2741+ return r.remote.Publisher
2742+ }
2743+
2744+ return ""
2745+}
2746+
2747+// Hash from the snappy.Part interface
2748+func (r *Removed) Hash() string { return "" }
2749+
2750+// IsActive from the snappy.Part interface
2751+func (r *Removed) IsActive() bool { return false }
2752+
2753+// IsInstalled from the snappy.Part interface
2754+func (r *Removed) IsInstalled() bool { return false }
2755+
2756+// NeedsReboot from the snappy.Part interface
2757+func (r *Removed) NeedsReboot() bool { return false }
2758+
2759+// Date from the snappy.Part interface
2760+func (r *Removed) Date() time.Time { return time.Time{} } // XXX: keep track of when the package was removed
2761+// Channel from the snappy.Part interface
2762+func (r *Removed) Channel() string { return "" }
2763+
2764+// Icon from the snappy.Part interface
2765+func (r *Removed) Icon() string {
2766+ if r.remote != nil {
2767+ return r.remote.IconURL
2768+ }
2769+
2770+ return ""
2771+}
2772+
2773+// Type from the snappy.Part interface
2774+func (r *Removed) Type() pkg.Type { return r.pkgType }
2775+
2776+// InstalledSize from the snappy.Part interface
2777+func (r *Removed) InstalledSize() int64 { return -1 }
2778+
2779+// DownloadSize from the snappy.Part interface
2780+func (r *Removed) DownloadSize() int64 {
2781+ if r.remote != nil {
2782+ return r.remote.DownloadSize
2783+ }
2784+
2785+ return -1
2786+}
2787+
2788+// Install from the snappy.Part interface
2789+func (r *Removed) Install(pb progress.Meter, flags snappy.InstallFlags) (name string, err error) {
2790+ return "", ErrRemoved
2791+}
2792+
2793+// Uninstall from the snappy.Part interface
2794+func (r *Removed) Uninstall(pb progress.Meter) error { return ErrRemoved }
2795+
2796+// Config from the snappy.Part interface
2797+func (r *Removed) Config(configuration []byte) (newConfig string, err error) { return "", ErrRemoved }
2798+
2799+// SetActive from the snappy.Part interface
2800+func (r *Removed) SetActive(bool, progress.Meter) error { return ErrRemoved }
2801+
2802+// Frameworks from the snappy.Part interface
2803+func (r *Removed) Frameworks() ([]string, error) { return nil, ErrRemoved }
2804
2805=== added file 'pkg/removed/removed_test.go'
2806--- pkg/removed/removed_test.go 1970-01-01 00:00:00 +0000
2807+++ pkg/removed/removed_test.go 2015-10-07 12:49:07 +0000
2808@@ -0,0 +1,137 @@
2809+// -*- Mode: Go; indent-tabs-mode: t -*-
2810+
2811+/*
2812+ * Copyright (C) 2014-2015 Canonical Ltd
2813+ *
2814+ * This program is free software: you can redistribute it and/or modify
2815+ * it under the terms of the GNU General Public License version 3 as
2816+ * published by the Free Software Foundation.
2817+ *
2818+ * This program is distributed in the hope that it will be useful,
2819+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
2820+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2821+ * GNU General Public License for more details.
2822+ *
2823+ * You should have received a copy of the GNU General Public License
2824+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
2825+ *
2826+ */
2827+
2828+package removed
2829+
2830+import (
2831+ "io/ioutil"
2832+ "os"
2833+ "path/filepath"
2834+ "testing"
2835+
2836+ "gopkg.in/check.v1"
2837+
2838+ "launchpad.net/snappy/dirs"
2839+ "launchpad.net/snappy/pkg"
2840+ "launchpad.net/snappy/progress"
2841+ "launchpad.net/snappy/snappy"
2842+)
2843+
2844+type removedSuite struct{}
2845+
2846+func Test(t *testing.T) { check.TestingT(t) }
2847+
2848+var _ = check.Suite(&removedSuite{})
2849+
2850+func (s *removedSuite) SetUpTest(c *check.C) {
2851+ dirs.SetRootDir(c.MkDir())
2852+ c.Check(os.MkdirAll(filepath.Join(dirs.SnapDataDir, "foo.bar", "1"), 0755), check.IsNil)
2853+}
2854+
2855+func (s *removedSuite) MkStoreYaml(c *check.C, pkgType pkg.Type) {
2856+ // creating the part to get its manifest path is cheating, a little
2857+ part := &Removed{
2858+ name: "foo",
2859+ origin: "bar",
2860+ version: "1",
2861+ pkgType: pkgType,
2862+ }
2863+
2864+ content := `
2865+name: foo
2866+origin: bar
2867+version: 1
2868+type: app
2869+description: |-
2870+ bla bla bla
2871+publisher: example.com
2872+iconurl: http://i.stack.imgur.com/i8q1U.jpg
2873+downloadsize: 5554242
2874+`
2875+ p := snappy.ManifestPath(part)
2876+ c.Assert(os.MkdirAll(filepath.Dir(p), 0755), check.IsNil)
2877+ c.Assert(ioutil.WriteFile(p, []byte(content), 0644), check.IsNil)
2878+
2879+}
2880+
2881+func (s *removedSuite) TestNoStore(c *check.C) {
2882+ part := New("foo", "bar", "1", pkg.TypeApp)
2883+
2884+ c.Check(part.Name(), check.Equals, "foo")
2885+ c.Check(part.Origin(), check.Equals, "bar")
2886+ c.Check(part.Version(), check.Equals, "1")
2887+ c.Check(part.Description(), check.Equals, "")
2888+ c.Check(part.Vendor(), check.Equals, "")
2889+ c.Check(part.Hash(), check.Equals, "")
2890+ c.Check(part.Icon(), check.Equals, "")
2891+ c.Check(part.DownloadSize(), check.Equals, int64(-1))
2892+
2893+ c.Check(part.InstalledSize(), check.Equals, int64(-1))
2894+ c.Check(part.IsActive(), check.Equals, false)
2895+ c.Check(part.IsInstalled(), check.Equals, false)
2896+ c.Check(part.NeedsReboot(), check.Equals, false)
2897+
2898+ prog := &progress.NullProgress{}
2899+ c.Check(part.Uninstall(prog), check.Equals, ErrRemoved)
2900+ c.Check(part.SetActive(true, prog), check.Equals, ErrRemoved)
2901+ _, err := part.Install(prog, 0)
2902+ c.Check(err, check.Equals, ErrRemoved)
2903+ _, err = part.Config(nil)
2904+ c.Check(err, check.Equals, ErrRemoved)
2905+ _, err = part.Frameworks()
2906+ c.Check(err, check.Equals, ErrRemoved)
2907+}
2908+
2909+func (s *removedSuite) TestNoOrigin(c *check.C) {
2910+ part := New("foo", "", "1", pkg.TypeFramework)
2911+ c.Check(part.Origin(), check.Equals, snappy.SideloadedOrigin)
2912+
2913+ s.MkStoreYaml(c, pkg.TypeFramework)
2914+ part = New("foo", "", "1", pkg.TypeFramework)
2915+ c.Check(part.Origin(), check.Equals, "bar")
2916+}
2917+
2918+func (s *removedSuite) TestWithStore(c *check.C) {
2919+ s.MkStoreYaml(c, pkg.TypeApp)
2920+ part := New("foo", "bar", "1", pkg.TypeApp)
2921+
2922+ c.Check(part.Name(), check.Equals, "foo")
2923+ c.Check(part.Origin(), check.Equals, "bar")
2924+ c.Check(part.Version(), check.Equals, "1")
2925+ c.Check(part.Description(), check.Equals, "bla bla bla")
2926+ c.Check(part.Vendor(), check.Equals, "example.com")
2927+ c.Check(part.Hash(), check.Equals, "")
2928+ c.Check(part.Icon(), check.Equals, "http://i.stack.imgur.com/i8q1U.jpg")
2929+ c.Check(part.DownloadSize(), check.Equals, int64(5554242))
2930+
2931+ c.Check(part.InstalledSize(), check.Equals, int64(-1))
2932+ c.Check(part.IsActive(), check.Equals, false)
2933+ c.Check(part.IsInstalled(), check.Equals, false)
2934+ c.Check(part.NeedsReboot(), check.Equals, false)
2935+
2936+ prog := &progress.NullProgress{}
2937+ c.Check(part.Uninstall(prog), check.Equals, ErrRemoved)
2938+ c.Check(part.SetActive(true, prog), check.Equals, ErrRemoved)
2939+ _, err := part.Install(prog, 0)
2940+ c.Check(err, check.Equals, ErrRemoved)
2941+ _, err = part.Config(nil)
2942+ c.Check(err, check.Equals, ErrRemoved)
2943+ _, err = part.Frameworks()
2944+ c.Check(err, check.Equals, ErrRemoved)
2945+}
2946
2947=== modified file 'snappy/parts.go'
2948--- snappy/parts.go 2015-10-01 19:28:27 +0000
2949+++ snappy/parts.go 2015-10-07 12:49:07 +0000
2950@@ -361,7 +361,7 @@
2951 return filepath.Join(dirs.SnapIconsDir, fmt.Sprintf("%s_%s.png", QualifiedName(s), s.Version()))
2952 }
2953
2954-// manifestPath returns the would be path for the store manifest meta data
2955-func manifestPath(s Part) string {
2956+// ManifestPath returns the would be path for the store manifest meta data
2957+func ManifestPath(s Part) string {
2958 return filepath.Join(dirs.SnapMetaDir, fmt.Sprintf("%s_%s.manifest", QualifiedName(s), s.Version()))
2959 }
2960
2961=== modified file 'snappy/snapp.go'
2962--- snappy/snapp.go 2015-10-02 15:46:24 +0000
2963+++ snappy/snapp.go 2015-10-07 12:49:07 +0000
2964@@ -44,6 +44,7 @@
2965 "launchpad.net/snappy/logger"
2966 "launchpad.net/snappy/oauth"
2967 "launchpad.net/snappy/pkg"
2968+ "launchpad.net/snappy/pkg/remote"
2969 "launchpad.net/snappy/policy"
2970 "launchpad.net/snappy/progress"
2971 "launchpad.net/snappy/release"
2972@@ -175,7 +176,7 @@
2973 // SnapPart represents a generic snap type
2974 type SnapPart struct {
2975 m *packageYaml
2976- remoteM *remoteSnap
2977+ remoteM *remote.Snap
2978 origin string
2979 hash string
2980 isActive bool
2981@@ -239,29 +240,9 @@
2982 LicenseVersion string `yaml:"license-version,omitempty"`
2983 }
2984
2985-type remoteSnap struct {
2986- Alias string `json:"alias,omitempty"`
2987- AnonDownloadURL string `json:"anon_download_url,omitempty"`
2988- DownloadSha512 string `json:"download_sha512,omitempty"`
2989- Description string `json:"description,omitempty"`
2990- DownloadSize int64 `json:"binary_filesize,omitempty"`
2991- DownloadURL string `json:"download_url,omitempty"`
2992- IconURL string `json:"icon_url"`
2993- LastUpdated string `json:"last_updated,omitempty"`
2994- Name string `json:"package_name"`
2995- Origin string `json:"origin"`
2996- Prices map[string]float64 `json:"prices,omitempty"`
2997- Publisher string `json:"publisher,omitempty"`
2998- RatingsAverage float64 `json:"ratings_average,omitempty"`
2999- SupportURL string `json:"support_url"`
3000- Title string `json:"title"`
3001- Type pkg.Type `json:"content,omitempty"`
3002- Version string `json:"version"`
3003-}
3004-
3005 type searchResults struct {
3006 Payload struct {
3007- Packages []remoteSnap `json:"clickindex:package"`
3008+ Packages []remote.Snap `json:"clickindex:package"`
3009 } `json:"_embedded"`
3010 }
3011
3012@@ -653,14 +634,14 @@
3013 }
3014 part.hash = h.ArchiveSha512
3015
3016- remoteManifestPath := manifestPath(part)
3017+ remoteManifestPath := ManifestPath(part)
3018 if helpers.FileExists(remoteManifestPath) {
3019 content, err := ioutil.ReadFile(remoteManifestPath)
3020 if err != nil {
3021 return nil, err
3022 }
3023
3024- var r remoteSnap
3025+ var r remote.Snap
3026 if err := yaml.Unmarshal(content, &r); err != nil {
3027 return nil, &ErrInvalidYaml{File: remoteManifestPath, Err: err, Yaml: content}
3028 }
3029@@ -1450,7 +1431,7 @@
3030
3031 // RemoteSnapPart represents a snap available on the server
3032 type RemoteSnapPart struct {
3033- pkg remoteSnap
3034+ pkg remote.Snap
3035 }
3036
3037 // Type returns the type of the SnapPart (app, oem, ...)
3038@@ -1634,7 +1615,7 @@
3039 }
3040
3041 // don't worry about previous contents
3042- return ioutil.WriteFile(manifestPath(s), content, 0644)
3043+ return ioutil.WriteFile(ManifestPath(s), content, 0644)
3044 }
3045
3046 // Install installs the snap
3047@@ -1682,8 +1663,8 @@
3048 }
3049
3050 // NewRemoteSnapPart returns a new RemoteSnapPart from the given
3051-// remoteSnap data
3052-func NewRemoteSnapPart(data remoteSnap) *RemoteSnapPart {
3053+// remote.Snap data
3054+func NewRemoteSnapPart(data remote.Snap) *RemoteSnapPart {
3055 return &RemoteSnapPart{pkg: data}
3056 }
3057
3058@@ -1738,7 +1719,7 @@
3059 }
3060
3061 v := url.Values{}
3062- v.Set("fields", strings.Join(getStructFields(remoteSnap{}), ","))
3063+ v.Set("fields", strings.Join(getStructFields(remote.Snap{}), ","))
3064 storeSearchURI.RawQuery = v.Encode()
3065
3066 storeDetailsURI, err = storeBaseURI.Parse("package/")
3067@@ -1831,7 +1812,7 @@
3068 }
3069
3070 // and decode json
3071- var detailsData remoteSnap
3072+ var detailsData remote.Snap
3073 dec := json.NewDecoder(resp.Body)
3074 if err := dec.Decode(&detailsData); err != nil {
3075 return nil, err
3076@@ -1952,7 +1933,7 @@
3077 }
3078 defer resp.Body.Close()
3079
3080- var updateData []remoteSnap
3081+ var updateData []remote.Snap
3082 dec := json.NewDecoder(resp.Body)
3083 if err := dec.Decode(&updateData); err != nil {
3084 return nil, err
3085
3086=== modified file 'snappy/sort_test.go'
3087--- snappy/sort_test.go 2015-08-21 12:08:28 +0000
3088+++ snappy/sort_test.go 2015-10-07 12:49:07 +0000
3089@@ -22,9 +22,10 @@
3090 import (
3091 "sort"
3092
3093+ . "gopkg.in/check.v1"
3094+
3095 "launchpad.net/snappy/helpers"
3096-
3097- . "gopkg.in/check.v1"
3098+ "launchpad.net/snappy/pkg/remote"
3099 )
3100
3101 type SortTestSuite struct {
3102@@ -83,8 +84,8 @@
3103
3104 func (s *SortTestSuite) TestSortSnaps(c *C) {
3105 snaps := []Part{
3106- &RemoteSnapPart{pkg: remoteSnap{Version: "2.0"}},
3107- &RemoteSnapPart{pkg: remoteSnap{Version: "1.0"}},
3108+ &RemoteSnapPart{pkg: remote.Snap{Version: "2.0"}},
3109+ &RemoteSnapPart{pkg: remote.Snap{Version: "1.0"}},
3110 }
3111 sort.Sort(BySnapVersion(snaps))
3112 c.Assert(snaps[0].Version(), Equals, "1.0")
3113
3114=== modified file 'snappy/systemimage.go'
3115--- snappy/systemimage.go 2015-10-05 19:12:55 +0000
3116+++ snappy/systemimage.go 2015-10-07 12:49:07 +0000
3117@@ -39,11 +39,16 @@
3118 "launchpad.net/snappy/provisioning"
3119 )
3120
3121+// SystemImagePart have constant name, origin, and vendor.
3122 const (
3123- systemImagePartName = "ubuntu-core"
3124- systemImagePartOrigin = "ubuntu"
3125- systemImagePartVendor = "Canonical Ltd."
3126+ SystemImagePartName = "ubuntu-core"
3127+ // SystemImagePartOrigin is the origin of any system image part
3128+ SystemImagePartOrigin = "ubuntu"
3129+ // SystemImagePartVendor is the vendor of any system image part
3130+ SystemImagePartVendor = "Canonical Ltd."
3131+)
3132
3133+const (
3134 // location of the channel config on the filesystem.
3135 //
3136 // This file specifies the s-i version installed on the rootfs
3137@@ -97,17 +102,17 @@
3138
3139 // Name returns the name
3140 func (s *SystemImagePart) Name() string {
3141- return systemImagePartName
3142+ return SystemImagePartName
3143 }
3144
3145 // Origin returns the origin ("ubuntu")
3146 func (s *SystemImagePart) Origin() string {
3147- return systemImagePartOrigin
3148+ return SystemImagePartOrigin
3149 }
3150
3151 // Vendor returns the vendor ("Canonical Ltd.")
3152 func (s *SystemImagePart) Vendor() string {
3153- return systemImagePartVendor
3154+ return SystemImagePartVendor
3155 }
3156
3157 // Version returns the version
3158@@ -248,7 +253,7 @@
3159 if err = s.partition.ToggleNextBoot(); err != nil {
3160 return "", err
3161 }
3162- return systemImagePartName, nil
3163+ return SystemImagePartName, nil
3164 }
3165
3166 // Ensure the expected version update was applied to the expected partition.
3167@@ -440,7 +445,7 @@
3168
3169 // Search searches the SystemImageRepository for the given terms
3170 func (s *SystemImageRepository) Search(terms string) (versions []Part, err error) {
3171- if strings.Contains(terms, systemImagePartName) {
3172+ if strings.Contains(terms, SystemImagePartName) {
3173 part := makeCurrentPart(s.partition)
3174 versions = append(versions, part)
3175 }
3176@@ -449,7 +454,7 @@
3177
3178 // Details returns details for the given snap
3179 func (s *SystemImageRepository) Details(name string, origin string) ([]Part, error) {
3180- if name == systemImagePartName && origin == systemImagePartOrigin {
3181+ if name == SystemImagePartName && origin == SystemImagePartOrigin {
3182 return []Part{makeCurrentPart(s.partition)}, nil
3183 }
3184
3185@@ -457,28 +462,33 @@
3186 }
3187
3188 // Updates returns the available updates
3189-func (s *SystemImageRepository) Updates() (parts []Part, err error) {
3190+func (s *SystemImageRepository) Updates() ([]Part, error) {
3191 configFile := filepath.Join(systemImageRoot, systemImageChannelConfig)
3192 updateStatus, err := systemImageClientCheckForUpdates(configFile)
3193+ if err != nil {
3194+ return nil, err
3195+ }
3196
3197 current := makeCurrentPart(s.partition)
3198 // no VersionCompare here because the channel provides a "order" and
3199 // that may go backwards when switching channels(?)
3200 if current.Version() != updateStatus.targetVersion {
3201- parts = append(parts, &SystemImagePart{
3202+ return []Part{&SystemImagePart{
3203 version: updateStatus.targetVersion,
3204 versionDetails: updateStatus.targetVersionDetails,
3205 lastUpdate: updateStatus.lastUpdate,
3206 updateSize: updateStatus.updateSize,
3207 channelName: current.(*SystemImagePart).channelName,
3208- partition: s.partition})
3209+ partition: s.partition}}, nil
3210 }
3211
3212- return parts, err
3213+ return nil, nil
3214 }
3215
3216 // Installed returns the installed snaps from this repository
3217-func (s *SystemImageRepository) Installed() (parts []Part, err error) {
3218+func (s *SystemImageRepository) Installed() ([]Part, error) {
3219+ var parts []Part
3220+
3221 // current partition
3222 curr := makeCurrentPart(s.partition)
3223 if curr != nil {
3224@@ -491,12 +501,18 @@
3225 parts = append(parts, other)
3226 }
3227
3228- return parts, err
3229+ return parts, nil
3230 }
3231
3232 // All installed parts. SystemImageParts are non-removable.
3233 func (s *SystemImageRepository) All() ([]Part, error) {
3234- return s.Installed()
3235+ parts, _ := s.Updates()
3236+ inst, err := s.Installed()
3237+ if err != nil {
3238+ return nil, err
3239+ }
3240+
3241+ return append(inst, parts...), nil
3242 }
3243
3244 // needsSync determines if syncing boot assets is required
3245
3246=== modified file 'snappy/systemimage_test.go'
3247--- snappy/systemimage_test.go 2015-09-29 09:45:15 +0000
3248+++ snappy/systemimage_test.go 2015-10-07 12:49:07 +0000
3249@@ -115,9 +115,9 @@
3250 c.Assert(err, IsNil)
3251 // we have one active and one inactive
3252 c.Assert(parts, HasLen, 2)
3253- c.Assert(parts[0].Name(), Equals, systemImagePartName)
3254- c.Assert(parts[0].Origin(), Equals, systemImagePartOrigin)
3255- c.Assert(parts[0].Vendor(), Equals, systemImagePartVendor)
3256+ c.Assert(parts[0].Name(), Equals, SystemImagePartName)
3257+ c.Assert(parts[0].Origin(), Equals, SystemImagePartOrigin)
3258+ c.Assert(parts[0].Vendor(), Equals, SystemImagePartVendor)
3259 c.Assert(parts[0].Version(), Equals, "1")
3260 c.Assert(parts[0].Hash(), Equals, "e09c13f68fccef3b2fe0f5c8ff5c61acf2173b170b1f2a3646487147690b0970ef6f2c555d7bcb072035f29ee4ea66a6df7f6bb320d358d3a7d78a0c37a8a549")
3261 c.Assert(parts[0].IsActive(), Equals, true)
3262@@ -461,8 +461,8 @@
3263 parts, err := s.systemImage.Installed()
3264 c.Assert(err, IsNil)
3265 c.Assert(parts, HasLen, 2)
3266- c.Assert(parts[0].Origin(), Equals, systemImagePartOrigin)
3267- c.Assert(parts[1].Origin(), Equals, systemImagePartOrigin)
3268+ c.Assert(parts[0].Origin(), Equals, SystemImagePartOrigin)
3269+ c.Assert(parts[1].Origin(), Equals, SystemImagePartOrigin)
3270 }
3271
3272 func (s *SITestSuite) TestCannotUpdateIfSideLoaded(c *C) {

Subscribers

People subscribed via source and target branches