diff options
| author | Michael Vogt <mvo@ubuntu.com> | 2016-04-14 08:42:46 +0200 |
|---|---|---|
| committer | Michael Vogt <mvo@ubuntu.com> | 2016-04-14 08:43:19 +0200 |
| commit | c28e18f0d1ea2c3ca823dc7784e5676a9a9b67be (patch) | |
| tree | 3f2730bdd6090f8a78f2b7c9642aaf385d06a614 | |
| parent | 8b6bbdecaafb2ed1abd833697383a0ed88babdcd (diff) | |
| parent | 5cb104ff73bd2895b48e5f9aef88944cb819b539 (diff) | |
Merge remote-tracking branch 'upstream/master' into feature/change-progressfeature/change-progress
35 files changed, 1531 insertions, 1180 deletions
diff --git a/cmd/snap/cmd_snap_op.go b/cmd/snap/cmd_snap_op.go index 73fd3fc020..d7a005a8b3 100644 --- a/cmd/snap/cmd_snap_op.go +++ b/cmd/snap/cmd_snap_op.go @@ -32,35 +32,45 @@ import ( ) func wait(client *client.Client, id string) error { + // FIXME: progress is all a bit simplistic, however its ok + // for now because the only meaningful progress + // we have is the download progress + + // we may have multiple downloads in a single change + lastTotal := 0 pb := progress.NewTextProgress() - defer pb.Finished() - started := false + defer func() { + pb.Set(float64(lastTotal)) + pb.Finished() + }() for { chg, err := client.Change(id) if err != nil { return err } - - // FIXME: a bit simplistic + total := 1 msg := "" - cur := 0 - total := 0 for _, t := range chg.Tasks { - cur = t.Progress.Done - total = t.Progress.Total - if total > 1 && cur != total { - break + if t.Status == "Doing" { + msg := t.Summary + // this will break once we have multiple + // downloads in parallel in a single change + if t.Progress.Total > 1 { + cur := t.Progress.Done + total = t.Progress.Total + if t.Progress.Total != lastTotal { + pb.Start(msg, float64(total)) + lastTotal = total + } + pb.Set(float64(cur)) + } } } + // we have no meaningful progress, just show spinner for + // last doing task if total == 1 { - pb.Spin("") - } else { - if !started { - pb.Start(msg, float64(total)) - started = true - } - pb.Set(float64(cur)) + pb.Spin(msg) } // XXX move this to a method of client.Change diff --git a/daemon/api.go b/daemon/api.go index 819bc51d18..830b1561dd 100644 --- a/daemon/api.go +++ b/daemon/api.go @@ -514,6 +514,7 @@ func (inst *snapInstruction) Agreed(intro, license string) bool { } var snapstateInstall = snapstate.Install +var snapstateGet = snapstate.Get func waitChange(chg *state.Change) error { select { @@ -526,30 +527,66 @@ func waitChange(chg *state.Change) error { return chg.Err() } +func ensureUbuntuCore(chg *state.Change) error { + var ss snapstate.SnapState + + ubuntuCore := "ubuntu-core" + err := snapstateGet(chg.State(), ubuntuCore, &ss) + if err != state.ErrNoState { + return err + } + + // FIXME: workaround because we are not fully state based yet + installed, err := (&snappy.Overlord{}).Installed() + snaps := snappy.FindSnapsByName(ubuntuCore, installed) + if len(snaps) > 0 { + return nil + } + + return installSnap(chg, ubuntuCore, "stable", 0) +} + +func installSnap(chg *state.Change, name, channel string, flags snappy.InstallFlags) error { + st := chg.State() + ts, err := snapstateInstall(st, name, channel, flags) + if err != nil { + return err + } + + // ensure that each of our task runs after the existing tasks + chgts := state.NewTaskSet(chg.Tasks()...) + for _, t := range ts.Tasks() { + t.WaitAll(chgts) + } + chg.AddAll(ts) + + return nil +} + func (inst *snapInstruction) install() (*state.Change, error) { flags := snappy.DoInstallGC if inst.LeaveOld { flags = 0 } - state := inst.overlord.State() - state.Lock() msg := fmt.Sprintf(i18n.G("Install %q snap"), inst.pkg) if inst.Channel != "stable" { msg = fmt.Sprintf(i18n.G("Install %q snap from %q channel"), inst.pkg, inst.Channel) } - chg := state.NewChange("install-snap", msg) - ts, err := snapstateInstall(state, inst.pkg, inst.Channel, flags) + + st := inst.overlord.State() + st.Lock() + chg := st.NewChange("install-snap", msg) + err := ensureUbuntuCore(chg) if err == nil { - chg.AddAll(ts) + err = installSnap(chg, inst.pkg, inst.Channel, flags) } - state.Unlock() + st.Unlock() if err != nil { return nil, err } - state.EnsureBefore(0) - - return chg, nil + st.EnsureBefore(0) + return chg, err // FIXME: handle license agreement need to happen in the above // code /* diff --git a/daemon/api_test.go b/daemon/api_test.go index 811db3bf7c..46960a38ac 100644 --- a/daemon/api_test.go +++ b/daemon/api_test.go @@ -91,6 +91,7 @@ func (s *apiSuite) TearDownSuite(c *check.C) { newRemoteRepo = nil muxVars = nil snapstateInstall = snapstate.Install + snapstateGet = snapstate.Get } func (s *apiSuite) SetUpTest(c *check.C) { @@ -176,7 +177,7 @@ version: %s c.Assert(err, check.IsNil) if active { - err := snappy.UpdateCurrentSymlink(localSnap, nil) + err := snappy.UpdateCurrentSymlink(localSnap.Info(), nil) c.Assert(err, check.IsNil) } @@ -362,6 +363,7 @@ func (s *apiSuite) TestListIncludesAll(c *check.C) { "pkgActionDispatch", // snapInstruction vars: "snapstateInstall", + "snapstateGet", "getConfigurator", } c.Check(found, check.Equals, len(api)+len(exceptions), @@ -1181,9 +1183,15 @@ func (s *apiSuite) TestPkgInstructionMismatch(c *check.C) { func (s *apiSuite) TestInstall(c *check.C) { calledFlags := snappy.InstallFlags(42) + installQueue := []string{} + snapstateGet = func(s *state.State, name string, snapst *snapstate.SnapState) error { + // we have ubuntu-core + return nil + } snapstateInstall = func(s *state.State, name, channel string, flags snappy.InstallFlags) (*state.TaskSet, error) { calledFlags = flags + installQueue = append(installQueue, name) t := s.NewTask("fake-install-snap", "Doing a fake install") return state.NewTaskSet(t), nil @@ -1193,6 +1201,7 @@ func (s *apiSuite) TestInstall(c *check.C) { inst := &snapInstruction{ overlord: d.overlord, Action: "install", + pkg: "some-snap", } d.overlord.Loop() @@ -1201,6 +1210,46 @@ func (s *apiSuite) TestInstall(c *check.C) { c.Check(calledFlags, check.Equals, snappy.DoInstallGC) c.Check(err, check.IsNil) + c.Check(installQueue, check.DeepEquals, []string{"some-snap"}) +} + +func (s *apiSuite) TestInstallMissingUbuntuCore(c *check.C) { + installQueue := []*state.Task{} + + snapstateGet = func(s *state.State, name string, snapst *snapstate.SnapState) error { + // pretend we do not have a state for ubuntu-core + return state.ErrNoState + } + snapstateInstall = func(s *state.State, name, channel string, flags snappy.InstallFlags) (*state.TaskSet, error) { + t1 := s.NewTask("fake-install-snap", name) + t2 := s.NewTask("fake-install-snap", "second task is just here so that we can check that the wait is correctly added to all tasks") + installQueue = append(installQueue, t1, t2) + return state.NewTaskSet(t1, t2), nil + } + + d := s.daemon(c) + inst := &snapInstruction{ + overlord: d.overlord, + Action: "install", + pkg: "some-snap", + } + + d.overlord.Loop() + defer d.overlord.Stop() + _, err := inst.dispatch()() + c.Check(err, check.IsNil) + + d.overlord.State().Lock() + defer d.overlord.State().Unlock() + c.Check(installQueue, check.HasLen, 4) + // the two "ubuntu-core" install tasks + c.Check(installQueue[0].Summary(), check.Equals, "ubuntu-core") + c.Check(installQueue[0].WaitTasks(), check.HasLen, 0) + c.Check(installQueue[1].WaitTasks(), check.HasLen, 0) + // the two "some-snap" install tasks + c.Check(installQueue[2].Summary(), check.Equals, "some-snap") + c.Check(installQueue[2].WaitTasks(), check.HasLen, 2) + c.Check(installQueue[3].WaitTasks(), check.HasLen, 2) } func (s *apiSuite) TestInstallFails(c *check.C) { diff --git a/debian/tests/integrationtests b/debian/tests/integrationtests index 90a7b6664f..23e6ece417 100644 --- a/debian/tests/integrationtests +++ b/debian/tests/integrationtests @@ -8,4 +8,4 @@ mkdir $GOPATH/src/github.com/ubuntu-core/snappy/integration-tests/data/output cp debian/tests/testconfig.json $GOPATH/src/github.com/ubuntu-core/snappy/integration-tests/data/output/ cd $GOPATH/src/github.com/ubuntu-core/snappy go test -c ./integration-tests/tests -./tests.test -check.v -check.f buildSuite +./tests.test -check.v -check.f snapHelloWorldExampleSuite diff --git a/integration-tests/tests/snap_example_test.go b/integration-tests/tests/snap_example_test.go index 3fb1984a3b..f290eec4b6 100644 --- a/integration-tests/tests/snap_example_test.go +++ b/integration-tests/tests/snap_example_test.go @@ -58,8 +58,10 @@ func (s *snapHelloWorldExampleSuite) TestCallHelloWorldBinary(c *check.C) { removeSnap(c, "hello-world") }) + // note that this also checks that we have a working ubuntu-core + // snap installed, without the ubuntu-core snap the launcher will + // not work and no "Hello World!\n" output echoOutput := cli.ExecCommand(c, "hello-world.echo") - c.Assert(echoOutput, check.Equals, "Hello World!\n", check.Commentf("Wrong output from hello-world binary")) } diff --git a/integration-tests/tests/snap_op_test.go b/integration-tests/tests/snap_op_test.go index d6e74d8854..f3702f2823 100644 --- a/integration-tests/tests/snap_op_test.go +++ b/integration-tests/tests/snap_op_test.go @@ -33,12 +33,12 @@ type snapOpSuite struct { common.SnappySuite } -func (s *snapOpSuite) testInstallRemove(c *check.C, snapName, displayName, displayDeveloper string) { +func (s *snapOpSuite) testInstallRemove(c *check.C, snapName, displayName string) { installOutput := installSnap(c, snapName) expected := "(?ms)" + "Name +Version +Developer\n" + ".*" + - displayName + " +.* +" + displayDeveloper + "\n" + + displayName + " +.*\n" + ".*" c.Assert(installOutput, check.Matches, expected) @@ -47,9 +47,5 @@ func (s *snapOpSuite) testInstallRemove(c *check.C, snapName, displayName, displ } func (s *snapOpSuite) TestInstallRemoveAliasWorks(c *check.C) { - s.testInstallRemove(c, "hello-world", "hello-world", "canonical") -} - -func (s *snapOpSuite) TestInstallRemoveFullNameWorks(c *check.C) { - s.testInstallRemove(c, "hello-world.canonical", "hello-world", "canonical") + s.testInstallRemove(c, "hello-world", "hello-world") } diff --git a/integration-tests/testutils/cli/cli.go b/integration-tests/testutils/cli/cli.go index 933f47b0f0..a38577dac5 100644 --- a/integration-tests/testutils/cli/cli.go +++ b/integration-tests/testutils/cli/cli.go @@ -39,7 +39,7 @@ var execCommand = exec.Command // of the command. In case of error, it will fail the test. func ExecCommand(c *check.C, cmds ...string) string { output, err := ExecCommandErr(cmds...) - c.Assert(err, check.IsNil, check.Commentf("Error: %v", output)) + c.Assert(err, check.IsNil, check.Commentf("Error for %v: %v", cmds, output)) return output } diff --git a/oauth/oauth.go b/oauth/oauth.go deleted file mode 100644 index 7c663de006..0000000000 --- a/oauth/oauth.go +++ /dev/null @@ -1,80 +0,0 @@ -// -*- Mode: Go; indent-tabs-mode: t -*- - -/* - * Copyright (C) 2014-2015 Canonical Ltd - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - */ - -package oauth - -import ( - "bytes" - "fmt" - "time" - - "github.com/ubuntu-core/snappy/strutil" -) - -// Token contains the sso token -type Token struct { - TokenKey string `json:"token_key"` - TokenSecret string `json:"token_secret"` - ConsumerSecret string `json:"consumer_secret"` - ConsumerKey string `json:"consumer_key"` -} - -// see https://dev.twitter.com/oauth/overview/percent-encoding-parameters -func needsEscape(c byte) bool { - return !(('A' <= c && c <= 'Z') || - ('a' <= c && c <= 'z') || - ('0' <= c && c <= '9') || - (c == '-') || - (c == '.') || - (c == '_') || - (c == '~')) -} - -// quote will quote all bytes in the input string that oauth requries to -// be quoted -func quote(s string) string { - buf := bytes.NewBuffer(nil) - // set to worst case max size, to avoid reallocs - sin := []byte(s) - buf.Grow(len(sin) * 3) - - for _, c := range sin { - if needsEscape(c) { - fmt.Fprintf(buf, "%%%02X", c) - } else { - fmt.Fprintf(buf, "%c", c) - } - } - - return buf.String() -} - -// FIXME: replace with a real oauth1 library - or wait until oauth2 becomes -// available - -// MakePlaintextSignature makes a oauth v1 plaintext signature -func MakePlaintextSignature(token *Token) string { - // hrm, rfc5849 says that nonce, timestamp are not used for PLAINTEXT - // but our sso server is unhappy without, so - nonce := strutil.MakeRandomString(60) - timestamp := time.Now().Unix() - - s := fmt.Sprintf(`OAuth oauth_nonce="%s", oauth_timestamp="%v", oauth_version="1.0", oauth_signature_method="PLAINTEXT", oauth_consumer_key="%s", oauth_token="%s", oauth_signature="%s%%26%s"`, nonce, timestamp, quote(token.ConsumerKey), quote(token.TokenKey), quote(token.ConsumerSecret), quote(token.TokenSecret)) - return s -} diff --git a/oauth/oauth_test.go b/oauth/oauth_test.go deleted file mode 100644 index 366ea8d789..0000000000 --- a/oauth/oauth_test.go +++ /dev/null @@ -1,69 +0,0 @@ -// -*- Mode: Go; indent-tabs-mode: t -*- - -/* - * Copyright (C) 2014-2015 Canonical Ltd - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 3 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - */ - -package oauth - -import ( - "testing" - - . "gopkg.in/check.v1" -) - -func Test(t *testing.T) { TestingT(t) } - -type OAuthTestSuite struct{} - -var _ = Suite(&OAuthTestSuite{}) - -func (s *OAuthTestSuite) TestMakePlaintextSignature(c *C) { - mockToken := Token{ - ConsumerKey: "consumer-key+", - ConsumerSecret: "consumer-secret+", - TokenKey: "token-key+", - TokenSecret: "token-secret+", - } - sig := MakePlaintextSignature(&mockToken) - c.Assert(sig, Matches, `OAuth oauth_nonce="[a-zA-Z0-9]+", oauth_timestamp="[0-9]+", oauth_version="1.0", oauth_signature_method="PLAINTEXT", oauth_consumer_key="consumer-key%2B", oauth_token="token-key%2B", oauth_signature="consumer-secret%2B%26token-secret%2B"`) -} - -func (s *OAuthTestSuite) TestQuote(c *C) { - // see http://wiki.oauth.net/w/page/12238556/TestCases - c.Check(quote("abcABC123"), Equals, "abcABC123") - c.Check(quote("-._~"), Equals, "-._~") - c.Check(quote("%"), Equals, "%25") - c.Check(quote("+"), Equals, "%2B") - c.Check(quote("&=*"), Equals, "%26%3D%2A") - c.Check(quote("\u000A"), Equals, "%0A") - c.Check(quote("\u0020"), Equals, "%20") - c.Check(quote("\u007F"), Equals, "%7F") - c.Check(quote("\u0080"), Equals, "%C2%80") - c.Check(quote("\u3001"), Equals, "%E3%80%81") -} - -func (s *OAuthTestSuite) TestNeedsEscape(c *C) { - for _, needed := range []byte{'?', '/', ':'} { - c.Check(needsEscape(needed), Equals, true) - } -} - -func (s *OAuthTestSuite) TestNeedsNoEscape(c *C) { - for _, no := range []byte{'a', 'z', 'A', 'Z', '-', '.', '_', '~'} { - c.Check(needsEscape(no), Equals, false) - } -} diff --git a/overlord/auth/auth.go b/overlord/auth/auth.go index d92f306f2a..061f76449c 100644 --- a/overlord/auth/auth.go +++ b/overlord/auth/auth.go @@ -20,7 +20,9 @@ package auth import ( + "bytes" "fmt" + "net/http" "github.com/ubuntu-core/snappy/overlord/state" ) @@ -81,3 +83,31 @@ func User(st *state.State, id int) (*UserState, error) { } return nil, fmt.Errorf("invalid user") } + +// Authenticator returns MacaroonAuthenticator for current authenticated user represented by UserState +func (us *UserState) Authenticator() *MacaroonAuthenticator { + return newMacaroonAuthenticator(us.Macaroon, us.Discharges) +} + +// MacaroonAuthenticator is a store authenticator based on macaroons +type MacaroonAuthenticator struct { + Macaroon string + Discharges []string +} + +func newMacaroonAuthenticator(macaroon string, discharges []string) *MacaroonAuthenticator { + return &MacaroonAuthenticator{ + Macaroon: macaroon, + Discharges: discharges, + } +} + +// Authenticate will add the store expected Authorization header for macaroons +func (ma *MacaroonAuthenticator) Authenticate(r *http.Request) { + var buf bytes.Buffer + fmt.Fprintf(&buf, `Macaroon root="%s"`, ma.Macaroon) + for _, discharge := range ma.Discharges { + fmt.Fprintf(&buf, `, discharge="%s"`, discharge) + } + r.Header.Set("Authorization", buf.String()) +} diff --git a/overlord/auth/auth_test.go b/overlord/auth/auth_test.go index 2be81581d2..bd6ccd9acf 100644 --- a/overlord/auth/auth_test.go +++ b/overlord/auth/auth_test.go @@ -20,6 +20,7 @@ package auth_test import ( + "net/http" "testing" . "gopkg.in/check.v1" @@ -127,3 +128,28 @@ func (as *authSuite) TestUser(c *C) { c.Check(err, IsNil) c.Check(userFromState, DeepEquals, user) } + +func (as *authSuite) TestGetAuthenticatorFromUser(c *C) { + as.state.Lock() + user, err := auth.NewUser(as.state, "username", "macaroon", []string{"discharge"}) + as.state.Unlock() + c.Check(err, IsNil) + + authenticator := user.Authenticator() + c.Check(authenticator.Macaroon, Equals, user.Macaroon) + c.Check(authenticator.Discharges, DeepEquals, user.Discharges) +} + +func (as *authSuite) TestAuthenticatorSetHeaders(c *C) { + as.state.Lock() + user, err := auth.NewUser(as.state, "username", "macaroon", []string{"discharge"}) + as.state.Unlock() + c.Check(err, IsNil) + + req, _ := http.NewRequest("GET", "http://example.com", nil) + authenticator := user.Authenticator() + authenticator.Authenticate(req) + + authorization := req.Header.Get("Authorization") + c.Check(authorization, Equals, `Macaroon root="macaroon", discharge="discharge"`) +} diff --git a/overlord/ifacestate/ifacemgr.go b/overlord/ifacestate/ifacemgr.go index 8f416f52ca..8fafd4a422 100644 --- a/overlord/ifacestate/ifacemgr.go +++ b/overlord/ifacestate/ifacemgr.go @@ -23,6 +23,7 @@ package ifacestate import ( "fmt" + "strings" "gopkg.in/tomb.v2" @@ -37,7 +38,6 @@ import ( "github.com/ubuntu-core/snappy/overlord/snapstate" "github.com/ubuntu-core/snappy/overlord/state" "github.com/ubuntu-core/snappy/snap" - "github.com/ubuntu-core/snappy/snappy" ) // InterfaceManager is responsible for the maintenance of interfaces in @@ -50,20 +50,15 @@ type InterfaceManager struct { } // Manager returns a new InterfaceManager. -func Manager(s *state.State) (*InterfaceManager, error) { - repo := interfaces.NewRepository() - for _, iface := range builtin.Interfaces() { - if err := repo.AddInterface(iface); err != nil { - return nil, err - } - } +// Extra interfaces can be provided for testing. +func Manager(s *state.State, extra []interfaces.Interface) (*InterfaceManager, error) { runner := state.NewTaskRunner(s) m := &InterfaceManager{ state: s, runner: runner, - repo: repo, + repo: interfaces.NewRepository(), } - if err := m.addSnaps(); err != nil { + if err := m.initialize(extra); err != nil { return nil, err } runner.AddHandler("connect", m.doConnect, nil) @@ -73,8 +68,38 @@ func Manager(s *state.State) (*InterfaceManager, error) { return m, nil } +func (m *InterfaceManager) initialize(extra []interfaces.Interface) error { + m.state.Lock() + defer m.state.Unlock() + + if err := m.addInterfaces(extra); err != nil { + return err + } + if err := m.addSnaps(); err != nil { + return err + } + if err := m.reloadConnections(); err != nil { + return err + } + return nil +} + +func (m *InterfaceManager) addInterfaces(extra []interfaces.Interface) error { + for _, iface := range builtin.Interfaces() { + if err := m.repo.AddInterface(iface); err != nil { + return err + } + } + for _, iface := range extra { + if err := m.repo.AddInterface(iface); err != nil { + return err + } + } + return nil +} + func (m *InterfaceManager) addSnaps() error { - snaps, err := xxxHackyInstalledSnaps() + snaps, err := snapstate.ActiveInfos(m.state) if err != nil { return err } @@ -87,16 +112,22 @@ func (m *InterfaceManager) addSnaps() error { return nil } -func xxxHackyInstalledSnaps() ([]*snap.Info, error) { - installed, err := (&snappy.Overlord{}).Installed() +func (m *InterfaceManager) reloadConnections() error { + conns, err := getConns(m.state) if err != nil { - return nil, err + return err } - snaps := make([]*snap.Info, len(installed)) - for i, legacySnap := range installed { - snaps[i] = legacySnap.Info() + for id := range conns { + plugRef, slotRef, err := parseConnID(id) + if err != nil { + return err + } + err = m.repo.Connect(plugRef.Snap, plugRef.Name, slotRef.Snap, slotRef.Name) + if err != nil { + return err + } } - return snaps, nil + return nil } func (m *InterfaceManager) doSetupSnapSecurity(task *state.Task, _ *tomb.Tomb) error { @@ -164,6 +195,25 @@ type connState struct { Interface string `json:"interface,omitempty"` } +func connID(plug *interfaces.PlugRef, slot *interfaces.SlotRef) string { + return fmt.Sprintf("%s:%s %s:%s", plug.Snap, plug.Name, slot.Snap, slot.Name) +} + +func parseConnID(conn string) (*interfaces.PlugRef, *interfaces.SlotRef, error) { + parts := strings.SplitN(conn, " ", 2) + if len(parts) != 2 { + return nil, nil, fmt.Errorf("malformed connection identifier: %q", conn) + } + plugParts := strings.SplitN(parts[0], ":", 2) + slotParts := strings.SplitN(parts[1], ":", 2) + if len(plugParts) != 2 || len(slotParts) != 2 { + return nil, nil, fmt.Errorf("malformed connection identifier: %q", conn) + } + plugRef := &interfaces.PlugRef{Snap: plugParts[0], Name: plugParts[1]} + slotRef := &interfaces.SlotRef{Snap: slotParts[0], Name: slotParts[1]} + return plugRef, slotRef, nil +} + func (m *InterfaceManager) autoConnect(task *state.Task, snapName string) error { var conns map[string]connState err := task.State().Get("conns", &conns) @@ -277,26 +327,72 @@ func getPlugAndSlotRefs(task *state.Task) (*interfaces.PlugRef, *interfaces.Slot return &plugRef, &slotRef, nil } +func getConns(st *state.State) (map[string]connState, error) { + // Get information about connections from the state + var conns map[string]connState + err := st.Get("conns", &conns) + if err != nil && err != state.ErrNoState { + return nil, fmt.Errorf("cannot obtain data about existing connections: %s", err) + } + if conns == nil { + conns = make(map[string]connState) + } + return conns, nil +} + +func setConns(st *state.State, conns map[string]connState) { + st.Set("conns", conns) +} + func (m *InterfaceManager) doConnect(task *state.Task, _ *tomb.Tomb) error { - task.State().Lock() - defer task.State().Unlock() + st := task.State() + st.Lock() + defer st.Unlock() plugRef, slotRef, err := getPlugAndSlotRefs(task) if err != nil { return err } - return m.repo.Connect(plugRef.Snap, plugRef.Name, slotRef.Snap, slotRef.Name) + + conns, err := getConns(st) + if err != nil { + return err + } + + err = m.repo.Connect(plugRef.Snap, plugRef.Name, slotRef.Snap, slotRef.Name) + if err != nil { + return err + } + + plug := m.repo.Plug(plugRef.Snap, plugRef.Name) + conns[connID(plugRef, slotRef)] = connState{Interface: plug.Interface} + setConns(st, conns) + return nil } func (m *InterfaceManager) doDisconnect(task *state.Task, _ *tomb.Tomb) error { - task.State().Lock() - defer task.State().Unlock() + st := task.State() + st.Lock() + defer st.Unlock() plugRef, slotRef, err := getPlugAndSlotRefs(task) if err != nil { return err } - return m.repo.Disconnect(plugRef.Snap, plugRef.Name, slotRef.Snap, slotRef.Name) + + conns, err := getConns(st) + if err != nil { + return err + } + + err = m.repo.Disconnect(plugRef.Snap, plugRef.Name, slotRef.Snap, slotRef.Name) + if err != nil { + return err + } + + delete(conns, connID(plugRef, slotRef)) + setConns(st, conns) + return nil } // Ensure implements StateManager.Ensure. diff --git a/overlord/ifacestate/ifacemgr_test.go b/overlord/ifacestate/ifacemgr_test.go index 2b0f0ee330..0b4cdbe05f 100644 --- a/overlord/ifacestate/ifacemgr_test.go +++ b/overlord/ifacestate/ifacemgr_test.go @@ -40,7 +40,8 @@ func TestInterfaceManager(t *testing.T) { TestingT(t) } type interfaceManagerSuite struct { state *state.State - mgr *ifacestate.InterfaceManager + privateMgr *ifacestate.InterfaceManager + extraIfaces []interfaces.Interface restoreBackends func() } @@ -49,24 +50,35 @@ var _ = Suite(&interfaceManagerSuite{}) func (s *interfaceManagerSuite) SetUpTest(c *C) { dirs.SetRootDir(c.MkDir()) state := state.New(nil) - mgr, err := ifacestate.Manager(state) - c.Assert(err, IsNil) s.state = state - s.mgr = mgr + s.privateMgr = nil + s.extraIfaces = nil s.restoreBackends = ifacestate.MockSecurityBackendsForSnap( func(snapInfo *snap.Info) []interfaces.SecurityBackend { return nil }, ) } func (s *interfaceManagerSuite) TearDownTest(c *C) { - s.mgr.Stop() + if s.privateMgr != nil { + s.privateMgr.Stop() + } dirs.SetRootDir("") s.restoreBackends() } +func (s *interfaceManagerSuite) manager(c *C) *ifacestate.InterfaceManager { + if s.privateMgr == nil { + mgr, err := ifacestate.Manager(s.state, s.extraIfaces) + c.Assert(err, IsNil) + s.privateMgr = mgr + } + return s.privateMgr +} + func (s *interfaceManagerSuite) TestSmoke(c *C) { - s.mgr.Ensure() - s.mgr.Wait() + mgr := s.manager(c) + mgr.Ensure() + mgr.Wait() } func (s *interfaceManagerSuite) TestConnectTask(c *C) { @@ -91,39 +103,36 @@ func (s *interfaceManagerSuite) TestConnectTask(c *C) { } func (s *interfaceManagerSuite) TestEnsureProcessesConnectTask(c *C) { - s.state.Lock() - defer s.state.Unlock() + s.mockIface(c, &interfaces.TestInterface{InterfaceName: "test"}) + s.mockSnap(c, consumerYaml) + s.mockSnap(c, producerYaml) - s.addPlugSlotAndInterface(c) + s.state.Lock() change := s.state.NewChange("kind", "summary") ts, err := ifacestate.Connect(s.state, "consumer", "plug", "producer", "slot") c.Assert(err, IsNil) change.AddAll(ts) - s.state.Unlock() - s.mgr.Ensure() - s.mgr.Wait() + + mgr := s.manager(c) + mgr.Ensure() + mgr.Wait() + s.state.Lock() + defer s.state.Unlock() task := change.Tasks()[0] c.Check(task.Kind(), Equals, "connect") c.Check(task.Status(), Equals, state.DoneStatus) c.Check(change.Status(), Equals, state.DoneStatus) - repo := s.mgr.Repository() - c.Check(repo.Interfaces(), DeepEquals, &interfaces.Interfaces{ - Slots: []*interfaces.Slot{{ - SlotInfo: &snap.SlotInfo{ - Snap: &snap.Info{SuggestedName: "producer"}, Name: "slot", Interface: "test", - }, - Connections: []interfaces.PlugRef{{Snap: "consumer", Name: "plug"}}, - }}, - Plugs: []*interfaces.Plug{{ - PlugInfo: &snap.PlugInfo{ - Snap: &snap.Info{SuggestedName: "consumer"}, Name: "plug", Interface: "test", - }, - Connections: []interfaces.SlotRef{{Snap: "producer", Name: "slot"}}, - }}, - }) + + repo := mgr.Repository() + plug := repo.Plug("consumer", "plug") + slot := repo.Slot("producer", "slot") + c.Assert(plug.Connections, HasLen, 1) + c.Assert(slot.Connections, HasLen, 1) + c.Check(plug.Connections[0], DeepEquals, interfaces.SlotRef{Snap: "producer", Name: "slot"}) + c.Check(slot.Connections[0], DeepEquals, interfaces.PlugRef{Snap: "consumer", Name: "plug"}) } func (s *interfaceManagerSuite) TestDisconnectTask(c *C) { @@ -148,46 +157,45 @@ func (s *interfaceManagerSuite) TestDisconnectTask(c *C) { } func (s *interfaceManagerSuite) TestEnsureProcessesDisconnectTask(c *C) { + s.mockIface(c, &interfaces.TestInterface{InterfaceName: "test"}) + s.mockSnap(c, consumerYaml) + s.mockSnap(c, producerYaml) + s.state.Lock() - defer s.state.Unlock() + s.state.Set("conns", map[string]interface{}{ + "consumer:plug producer:slot": map[string]interface{}{"interface": "test"}, + }) + s.state.Unlock() - s.addPlugSlotAndInterface(c) - repo := s.mgr.Repository() - err := repo.Connect("consumer", "plug", "producer", "slot") - c.Assert(err, IsNil) + s.state.Lock() change := s.state.NewChange("kind", "summary") ts, err := ifacestate.Disconnect(s.state, "consumer", "plug", "producer", "slot") c.Assert(err, IsNil) change.AddAll(ts) - s.state.Unlock() - s.mgr.Ensure() - s.mgr.Wait() + + mgr := s.manager(c) + mgr.Ensure() + mgr.Wait() + s.state.Lock() + defer s.state.Unlock() task := change.Tasks()[0] c.Check(task.Kind(), Equals, "disconnect") c.Check(task.Status(), Equals, state.DoneStatus) c.Check(change.Status(), Equals, state.DoneStatus) - c.Check(repo.Interfaces(), DeepEquals, &interfaces.Interfaces{ - // NOTE: the connection is gone now. - Slots: []*interfaces.Slot{{SlotInfo: &snap.SlotInfo{ - Snap: &snap.Info{SuggestedName: "producer"}, Name: "slot", Interface: "test"}}}, - Plugs: []*interfaces.Plug{{PlugInfo: &snap.PlugInfo{ - Snap: &snap.Info{SuggestedName: "consumer"}, Name: "plug", Interface: "test"}}}, - }) + + // The connection is gone + repo := mgr.Repository() + plug := repo.Plug("consumer", "plug") + slot := repo.Slot("producer", "slot") + c.Assert(plug.Connections, HasLen, 0) + c.Assert(slot.Connections, HasLen, 0) } -func (s *interfaceManagerSuite) addPlugSlotAndInterface(c *C) { - repo := s.mgr.Repository() - err := repo.AddInterface(&interfaces.TestInterface{InterfaceName: "test"}) - c.Assert(err, IsNil) - err = repo.AddSlot(&interfaces.Slot{SlotInfo: &snap.SlotInfo{ - Snap: &snap.Info{SuggestedName: "producer"}, Name: "slot", Interface: "test"}}) - c.Assert(err, IsNil) - err = repo.AddPlug(&interfaces.Plug{PlugInfo: &snap.PlugInfo{ - Snap: &snap.Info{SuggestedName: "consumer"}, Name: "plug", Interface: "test"}}) - c.Assert(err, IsNil) +func (s *interfaceManagerSuite) mockIface(c *C, iface interfaces.Interface) { + s.extraIfaces = append(s.extraIfaces, iface) } func (s *interfaceManagerSuite) mockSnap(c *C, yamlText string) *snap.Info { @@ -210,12 +218,9 @@ func (s *interfaceManagerSuite) mockSnap(c *C, yamlText string) *snap.Info { // Put a side info into the state snapstate.Set(s.state, snapInfo.Name(), &snapstate.SnapState{ + Active: true, Sequence: []*snap.SideInfo{{Revision: snapInfo.Revision}}, }) - - // Add it to the repository - s.mgr.Repository().AddSnap(snapInfo) - return snapInfo } @@ -249,15 +254,33 @@ plugs: interface: network ` +var consumerYaml = ` +name: consumer +version: 1 +plugs: + plug: + interface: test +` + +var producerYaml = ` +name: producer +version: 1 +slots: + slot: + interface: test +` + func (s *interfaceManagerSuite) TestDoSetupSnapSecuirty(c *C) { s.mockSnap(c, osSnapYaml) snapInfo := s.mockSnap(c, sampleSnapYaml) + mgr := s.manager(c) + // Run the setup-snap-security task change := s.addSetupSnapSecurityChange(c, snapInfo.Name()) - s.mgr.Ensure() - s.mgr.Wait() - s.mgr.Stop() + mgr.Ensure() + mgr.Wait() + mgr.Stop() s.state.Lock() defer s.state.Unlock() @@ -278,6 +301,8 @@ func (s *interfaceManagerSuite) TestDoSetupSnapSecuirtyKeepsExistingConnectionSt s.mockSnap(c, osSnapYaml) snapInfo := s.mockSnap(c, sampleSnapYaml) + mgr := s.manager(c) + // Put information about connections for another snap into the state s.state.Lock() s.state.Set("conns", map[string]interface{}{ @@ -289,9 +314,9 @@ func (s *interfaceManagerSuite) TestDoSetupSnapSecuirtyKeepsExistingConnectionSt // Run the setup-snap-security task change := s.addSetupSnapSecurityChange(c, snapInfo.Name()) - s.mgr.Ensure() - s.mgr.Wait() - s.mgr.Stop() + mgr.Ensure() + mgr.Wait() + mgr.Stop() s.state.Lock() defer s.state.Unlock() @@ -310,3 +335,90 @@ func (s *interfaceManagerSuite) TestDoSetupSnapSecuirtyKeepsExistingConnectionSt }, }) } + +func (s *interfaceManagerSuite) TestConnectTracksConnectionsInState(c *C) { + s.mockIface(c, &interfaces.TestInterface{InterfaceName: "test"}) + s.mockSnap(c, consumerYaml) + s.mockSnap(c, producerYaml) + + mgr := s.manager(c) + + s.state.Lock() + ts, err := ifacestate.Connect(s.state, "consumer", "plug", "producer", "slot") + c.Assert(err, IsNil) + change := s.state.NewChange("connect", "") + change.AddAll(ts) + s.state.Unlock() + + mgr.Ensure() + mgr.Wait() + mgr.Stop() + + s.state.Lock() + defer s.state.Unlock() + + c.Check(change.Status(), Equals, state.DoneStatus) + var conns map[string]interface{} + err = s.state.Get("conns", &conns) + c.Assert(err, IsNil) + c.Check(conns, DeepEquals, map[string]interface{}{ + "consumer:plug producer:slot": map[string]interface{}{ + "interface": "test", + }, + }) +} + +func (s *interfaceManagerSuite) TestDisconnectTracksConnectionsInState(c *C) { + s.mockIface(c, &interfaces.TestInterface{InterfaceName: "test"}) + s.mockSnap(c, consumerYaml) + s.mockSnap(c, producerYaml) + s.state.Lock() + s.state.Set("conns", map[string]interface{}{ + "consumer:plug producer:slot": map[string]interface{}{"interface": "test"}, + }) + s.state.Unlock() + + mgr := s.manager(c) + + s.state.Lock() + ts, err := ifacestate.Disconnect(s.state, "consumer", "plug", "producer", "slot") + c.Assert(err, IsNil) + change := s.state.NewChange("disconnect", "") + change.AddAll(ts) + s.state.Unlock() + + mgr.Ensure() + mgr.Wait() + mgr.Stop() + + s.state.Lock() + defer s.state.Unlock() + + c.Check(change.Status(), Equals, state.DoneStatus) + var conns map[string]interface{} + err = s.state.Get("conns", &conns) + c.Assert(err, IsNil) + c.Check(conns, DeepEquals, map[string]interface{}{}) +} + +func (s *interfaceManagerSuite) TestManagerReloadsConnections(c *C) { + s.mockIface(c, &interfaces.TestInterface{InterfaceName: "test"}) + s.mockSnap(c, consumerYaml) + s.mockSnap(c, producerYaml) + + s.state.Lock() + s.state.Set("conns", map[string]interface{}{ + "consumer:plug producer:slot": map[string]interface{}{"interface": "test"}, + }) + s.state.Unlock() + + mgr := s.manager(c) + repo := mgr.Repository() + + plug := repo.Plug("consumer", "plug") + slot := repo.Slot("producer", "slot") + c.Assert(plug.Connections, HasLen, 1) + c.Assert(slot.Connections, HasLen, 1) + c.Check(plug.Connections[0], DeepEquals, interfaces.SlotRef{Snap: "producer", Name: "slot"}) + c.Check(slot.Connections[0], DeepEquals, interfaces.PlugRef{Snap: "consumer", Name: "plug"}) +} diff --git a/overlord/overlord.go b/overlord/overlord.go index 42760a71bd..22e3f0d753 100644 --- a/overlord/overlord.go +++ b/overlord/overlord.go @@ -85,7 +85,7 @@ func New() (*Overlord, error) { o.assertMgr = assertMgr o.stateEng.AddManager(o.assertMgr) - ifaceMgr, err := ifacestate.Manager(s) + ifaceMgr, err := ifacestate.Manager(s, nil) if err != nil { return nil, err } diff --git a/overlord/overlord_test.go b/overlord/overlord_test.go index 8006f0bd03..cc6d4b708f 100644 --- a/overlord/overlord_test.go +++ b/overlord/overlord_test.go @@ -221,9 +221,6 @@ func (ovs *overlordSuite) TestCheckpoint(c *C) { o, err := overlord.New() c.Assert(err, IsNil) - _, err = os.Stat(dirs.SnapStateFile) - c.Check(os.IsNotExist(err), Equals, true) - s := o.State() s.Lock() s.Set("mark", 1) diff --git a/overlord/snapstate/backend.go b/overlord/snapstate/backend.go index 31467b8947..55c59e5178 100644 --- a/overlord/snapstate/backend.go +++ b/overlord/snapstate/backend.go @@ -20,11 +20,6 @@ package snapstate import ( - "fmt" - "path/filepath" - "strconv" - - "github.com/ubuntu-core/snappy/dirs" "github.com/ubuntu-core/snappy/progress" "github.com/ubuntu-core/snappy/snap" "github.com/ubuntu-core/snappy/snappy" @@ -33,30 +28,24 @@ import ( type managerBackend interface { // install releated Download(name, channel string, meter progress.Meter) (*snap.Info, string, error) - CheckSnap(snapFilePath string, flags int) error + CheckSnap(snapFilePath string, curInfo *snap.Info, flags int) error SetupSnap(snapFilePath string, si *snap.SideInfo, flags int) error - CopySnapData(instSnapPath string, flags int) error - LinkSnap(instSnapPath string) error - GarbageCollect(snap string, flags int, meter progress.Meter) error + CopySnapData(newSnap, oldSnap *snap.Info, flags int) error + LinkSnap(info *snap.Info) error // the undoers for install UndoSetupSnap(s snap.PlaceInfo) error - UndoCopySnapData(instSnapPath string, flags int) error - UndoLinkSnap(oldInstSnapPath, instSnapPath string) error + UndoCopySnapData(newSnap *snap.Info, flags int) error // remove releated - CanRemove(instSnapPath string) error - UnlinkSnap(instSnapPath string, meter progress.Meter) error + CanRemove(info *snap.Info, active bool) bool + UnlinkSnap(info *snap.Info, meter progress.Meter) error RemoveSnapFiles(s snap.PlaceInfo, meter progress.Meter) error - RemoveSnapData(name string, revision int) error + RemoveSnapData(info *snap.Info) error // TODO: need to be split into fine grained tasks - Update(name, channel string, flags int, meter progress.Meter) error Activate(name string, active bool, meter progress.Meter) error - // XXX: this one needs to be revno based as well - Rollback(name, ver string, meter progress.Meter) (string, error) // info - ActiveSnap(name string) *snap.Info SnapByNameAndVersion(name, version string) *snap.Info // testing helpers @@ -67,13 +56,6 @@ type defaultBackend struct{} func (b *defaultBackend) Candidate(*snap.SideInfo) {} -func (b *defaultBackend) ActiveSnap(name string) *snap.Info { - if snap := snappy.ActiveSnapByName(name); snap != nil { - return snap.Info() - } - return nil -} - func (b *defaultBackend) SnapByNameAndVersion(name, version string) *snap.Info { // XXX: use snapstate stuff! installed, err := (&snappy.Overlord{}).Installed() @@ -88,16 +70,6 @@ func (b *defaultBackend) SnapByNameAndVersion(name, version string) *snap.Info { return found[0].Info() } -func (b *defaultBackend) Update(name, channel string, flags int, meter progress.Meter) error { - // FIXME: support "channel" in snappy.Update() - _, err := snappy.Update(name, snappy.InstallFlags(flags), meter) - return err -} - -func (b *defaultBackend) Rollback(name, ver string, meter progress.Meter) (string, error) { - return snappy.Rollback(name, ver, meter) -} - func (b *defaultBackend) Activate(name string, active bool, meter progress.Meter) error { return snappy.SetActive(name, active, meter) } @@ -117,9 +89,9 @@ func (b *defaultBackend) Download(name, channel string, meter progress.Meter) (* return snap, downloadedSnapFile, nil } -func (b *defaultBackend) CheckSnap(snapFilePath string, flags int) error { +func (b *defaultBackend) CheckSnap(snapFilePath string, curInfo *snap.Info, flags int) error { meter := &progress.NullProgress{} - return snappy.CheckSnap(snapFilePath, snappy.InstallFlags(flags), meter) + return snappy.CheckSnap(snapFilePath, curInfo, snappy.InstallFlags(flags), meter) } func (b *defaultBackend) SetupSnap(snapFilePath string, sideInfo *snap.SideInfo, flags int) error { @@ -128,26 +100,14 @@ func (b *defaultBackend) SetupSnap(snapFilePath string, sideInfo *snap.SideInfo, return err } -func (b *defaultBackend) CopySnapData(snapInstPath string, flags int) error { - sn, err := snappy.NewInstalledSnap(filepath.Join(snapInstPath, "meta", "snap.yaml")) - if err != nil { - return err - } +func (b *defaultBackend) CopySnapData(newInfo, oldInfo *snap.Info, flags int) error { meter := &progress.NullProgress{} - return snappy.CopyData(sn.Info(), snappy.InstallFlags(flags), meter) + return snappy.CopyData(newInfo, oldInfo, snappy.InstallFlags(flags), meter) } -func (b *defaultBackend) LinkSnap(snapInstPath string) error { - sn, err := snappy.NewInstalledSnap(filepath.Join(snapInstPath, "meta", "snap.yaml")) - if err != nil { - return err - } +func (b *defaultBackend) LinkSnap(info *snap.Info) error { meter := &progress.NullProgress{} - if err := snappy.GenerateWrappers(sn, meter); err != nil { - return err - } - - return snappy.UpdateCurrentSymlink(sn, meter) + return snappy.LinkSnap(info, meter) } func (b *defaultBackend) UndoSetupSnap(s snap.PlaceInfo) error { @@ -156,71 +116,24 @@ func (b *defaultBackend) UndoSetupSnap(s snap.PlaceInfo) error { return nil } -func (b *defaultBackend) UndoCopySnapData(instSnapPath string, flags int) error { - sn, err := snappy.NewInstalledSnap(filepath.Join(instSnapPath, "meta", "snap.yaml")) - if err != nil { - return err - } +func (b *defaultBackend) UndoCopySnapData(newInfo *snap.Info, flags int) error { meter := &progress.NullProgress{} - snappy.UndoCopyData(sn.Info(), snappy.InstallFlags(flags), meter) + snappy.UndoCopyData(newInfo, snappy.InstallFlags(flags), meter) return nil } -func (b *defaultBackend) UndoLinkSnap(oldInstSnapPath, instSnapPath string) error { - new, err := snappy.NewInstalledSnap(filepath.Join(instSnapPath, "meta", "snap.yaml")) - if err != nil { - return err - } - old, err := snappy.NewInstalledSnap(filepath.Join(oldInstSnapPath, "meta", "snap.yaml")) - if err != nil { - return err - } - - meter := &progress.NullProgress{} - err1 := snappy.RemoveGeneratedWrappers(new, meter) - err2 := snappy.UndoUpdateCurrentSymlink(old, new, meter) - - // return firstErr - if err1 != nil { - return err1 - } - return err2 -} - -func (b *defaultBackend) CanRemove(instSnapPath string) error { - sn, err := snappy.NewInstalledSnap(filepath.Join(instSnapPath, "meta", "snap.yaml")) - if err != nil { - return err - } - if !snappy.CanRemove(sn) { - return fmt.Errorf("snap %q is not removable", sn.Name()) - } - return nil +func (b *defaultBackend) CanRemove(info *snap.Info, active bool) bool { + return snappy.CanRemove(info, active) } -func (b *defaultBackend) UnlinkSnap(instSnapPath string, meter progress.Meter) error { - sn, err := snappy.NewInstalledSnap(filepath.Join(instSnapPath, "meta", "snap.yaml")) - if err != nil { - return err - } - - return snappy.UnlinkSnap(sn, meter) +func (b *defaultBackend) UnlinkSnap(info *snap.Info, meter progress.Meter) error { + return snappy.UnlinkSnap(info, meter) } func (b *defaultBackend) RemoveSnapFiles(s snap.PlaceInfo, meter progress.Meter) error { return snappy.RemoveSnapFiles(s, meter) } -func (b *defaultBackend) RemoveSnapData(name string, revision int) error { - // XXX: hack for now - sn, err := snappy.NewInstalledSnap(filepath.Join(dirs.SnapSnapsDir, name, strconv.Itoa(revision), "meta", "snap.yaml")) - if err != nil { - return err - } - - return snappy.RemoveSnapData(sn.Info()) -} - -func (b *defaultBackend) GarbageCollect(snap string, flags int, meter progress.Meter) error { - return snappy.GarbageCollect(snap, snappy.InstallFlags(flags), meter) +func (b *defaultBackend) RemoveSnapData(info *snap.Info) error { + return snappy.RemoveSnapData(info) } diff --git a/overlord/snapstate/backend_test.go b/overlord/snapstate/backend_test.go index f923fe2495..95bbe5db7f 100644 --- a/overlord/snapstate/backend_test.go +++ b/overlord/snapstate/backend_test.go @@ -20,6 +20,7 @@ package snapstate_test import ( + "errors" "strings" "github.com/ubuntu-core/snappy/progress" @@ -36,7 +37,7 @@ type fakeOp struct { active bool sinfo snap.SideInfo - rollback string + old string } type fakeSnappyBackend struct { @@ -45,7 +46,7 @@ type fakeSnappyBackend struct { fakeCurrentProgress int fakeTotalProgress int - activeSnaps map[string]*snap.Info + linkSnapFailTrigger string } func (f *fakeSnappyBackend) InstallLocal(path string, flags int, p progress.Meter) error { @@ -77,31 +78,6 @@ func (f *fakeSnappyBackend) Download(name, channel string, p progress.Meter) (*s return info, "downloaded-snap-path", nil } -func (f *fakeSnappyBackend) Update(name, channel string, flags int, p progress.Meter) error { - f.ops = append(f.ops, fakeOp{ - op: "update", - name: name, - channel: channel, - }) - return nil -} - -func (f *fakeSnappyBackend) Remove(name string, flags int, p progress.Meter) error { - f.ops = append(f.ops, fakeOp{ - op: "remove", - name: name, - }) - return nil -} -func (f *fakeSnappyBackend) Rollback(name, ver string, p progress.Meter) (string, error) { - f.ops = append(f.ops, fakeOp{ - op: "rollback", - name: name, - rollback: ver, - }) - return "", nil -} - func (f *fakeSnappyBackend) Activate(name string, active bool, p progress.Meter) error { f.ops = append(f.ops, fakeOp{ op: "activate", @@ -111,10 +87,15 @@ func (f *fakeSnappyBackend) Activate(name string, active bool, p progress.Meter) return nil } -func (f *fakeSnappyBackend) CheckSnap(snapFilePath string, flags int) error { +func (f *fakeSnappyBackend) CheckSnap(snapFilePath string, curInfo *snap.Info, flags int) error { + cur := "<no-current>" + if curInfo != nil { + cur = curInfo.MountDir() + } f.ops = append(f.ops, fakeOp{ op: "check-snap", name: snapFilePath, + old: cur, flags: flags, }) return nil @@ -134,19 +115,37 @@ func (f *fakeSnappyBackend) SetupSnap(snapFilePath string, si *snap.SideInfo, fl return nil } -func (f *fakeSnappyBackend) CopySnapData(instSnapPath string, flags int) error { +func (f *fakeSnappyBackend) RetrieveInfo(name string, si *snap.SideInfo) (*snap.Info, error) { + // naive emulation for now, always works + return &snap.Info{SideInfo: *si}, nil +} + +func (f *fakeSnappyBackend) CopySnapData(newInfo, oldInfo *snap.Info, flags int) error { + old := "<no-old>" + if oldInfo != nil { + old = oldInfo.MountDir() + } f.ops = append(f.ops, fakeOp{ op: "copy-data", - name: instSnapPath, + name: newInfo.MountDir(), flags: flags, + old: old, }) return nil } -func (f *fakeSnappyBackend) LinkSnap(instSnapPath string) error { +func (f *fakeSnappyBackend) LinkSnap(info *snap.Info) error { + if info.MountDir() == f.linkSnapFailTrigger { + f.ops = append(f.ops, fakeOp{ + op: "link-snap.failed", + name: info.MountDir(), + }) + return errors.New("fail") + } + f.ops = append(f.ops, fakeOp{ op: "link-snap", - name: instSnapPath, + name: info.MountDir(), }) return nil } @@ -159,26 +158,14 @@ func (f *fakeSnappyBackend) UndoSetupSnap(s snap.PlaceInfo) error { return nil } -func (f *fakeSnappyBackend) UndoCopySnapData(instSnapPath string, flags int) error { +func (f *fakeSnappyBackend) UndoCopySnapData(newInfo *snap.Info, flags int) error { f.ops = append(f.ops, fakeOp{ op: "undo-copy-snap-data", - name: instSnapPath, - }) - return nil -} - -func (f *fakeSnappyBackend) UndoLinkSnap(oldInstSnapPath, instSnapPath string) error { - f.ops = append(f.ops, fakeOp{ - op: "undo-link-snap", - name: instSnapPath, + name: newInfo.MountDir(), }) return nil } -func (f *fakeSnappyBackend) ActiveSnap(name string) *snap.Info { - return f.activeSnaps[name] -} - func (f *fakeSnappyBackend) SnapByNameAndVersion(name, version string) *snap.Info { return &snap.Info{ SideInfo: snap.SideInfo{ @@ -190,18 +177,19 @@ func (f *fakeSnappyBackend) SnapByNameAndVersion(name, version string) *snap.Inf } } -func (f *fakeSnappyBackend) CanRemove(instSnapPath string) error { +func (f *fakeSnappyBackend) CanRemove(info *snap.Info, active bool) bool { f.ops = append(f.ops, fakeOp{ - op: "can-remove", - name: instSnapPath, + op: "can-remove", + name: info.MountDir(), + active: active, }) - return nil + return true } -func (f *fakeSnappyBackend) UnlinkSnap(instSnapPath string, meter progress.Meter) error { +func (f *fakeSnappyBackend) UnlinkSnap(info *snap.Info, meter progress.Meter) error { f.ops = append(f.ops, fakeOp{ op: "unlink-snap", - name: instSnapPath, + name: info.MountDir(), }) return nil } @@ -214,11 +202,10 @@ func (f *fakeSnappyBackend) RemoveSnapFiles(s snap.PlaceInfo, meter progress.Met return nil } -func (f *fakeSnappyBackend) RemoveSnapData(name string, revno int) error { +func (f *fakeSnappyBackend) RemoveSnapData(info *snap.Info) error { f.ops = append(f.ops, fakeOp{ - op: "remove-snap-data", - name: name, - revno: revno, + op: "remove-snap-data", + name: info.MountDir(), }) return nil } diff --git a/overlord/snapstate/export_test.go b/overlord/snapstate/export_test.go index 25455ccd5b..f11f0052fb 100644 --- a/overlord/snapstate/export_test.go +++ b/overlord/snapstate/export_test.go @@ -20,9 +20,12 @@ package snapstate import ( + "errors" + "gopkg.in/tomb.v2" "github.com/ubuntu-core/snappy/overlord/state" + "github.com/ubuntu-core/snappy/snap" ) type ManagerBackend managerBackend @@ -41,4 +44,15 @@ func (m *SnapManager) AddForeignTaskHandlers() { fakeHandler := func(task *state.Task, _ *tomb.Tomb) error { return nil } m.runner.AddHandler("setup-snap-security", fakeHandler, fakeHandler) m.runner.AddHandler("remove-snap-security", fakeHandler, fakeHandler) + + // Add handler to test full aborting of changes + erroringHandler := func(task *state.Task, _ *tomb.Tomb) error { + return errors.New("error out") + } + m.runner.AddHandler("error-trigger", erroringHandler, nil) +} + +func ChangeRetrieveInfo(retrieve func(name string, si *snap.SideInfo) (*snap.Info, error)) func() { + retrieveInfo = retrieve + return func() { retrieveInfo = retrieveInfoImpl } } diff --git a/overlord/snapstate/progress.go b/overlord/snapstate/progress.go index fa0a28eef3..30ad32cd7b 100644 --- a/overlord/snapstate/progress.go +++ b/overlord/snapstate/progress.go @@ -26,8 +26,9 @@ import ( // TaskProgressAdapter adapts the progress.Meter to the task progress // until we have native install/update/remove. type TaskProgressAdapter struct { - task *state.Task - total float64 + task *state.Task + total float64 + current float64 } // Start sets total @@ -37,6 +38,7 @@ func (t *TaskProgressAdapter) Start(pkg string, total float64) { // Set sets the current progress func (t *TaskProgressAdapter) Set(current float64) { + t.current = current t.task.State().Lock() defer t.task.State().Unlock() t.task.SetProgress(int(current), int(t.total)) @@ -56,6 +58,11 @@ func (t *TaskProgressAdapter) Finished() { // Write does nothing func (t *TaskProgressAdapter) Write(p []byte) (n int, err error) { + t.task.State().Lock() + defer t.task.State().Unlock() + + t.current += float64(len(p)) + t.task.SetProgress(int(t.current), int(t.total)) return len(p), nil } diff --git a/overlord/snapstate/snapmgr.go b/overlord/snapstate/snapmgr.go index 912c44e2bd..b5149d5862 100644 --- a/overlord/snapstate/snapmgr.go +++ b/overlord/snapstate/snapmgr.go @@ -39,19 +39,23 @@ type SnapManager struct { // SnapSetup holds the necessary snap details to perform most snap manager tasks. type SnapSetup struct { - Name string `json:"name"` - Developer string `json:"developer,omitempty"` - Revision int `json:"revision,omitempty"` - Channel string `json:"channel,omitempty"` - - // XXX: should be switched to use Revision instead - RollbackVersion string `json:"rollback-version,omitempty"` + Name string `json:"name"` + Revision int `json:"revision,omitempty"` + Channel string `json:"channel,omitempty"` Flags int `json:"flags,omitempty"` SnapPath string `json:"snap-path,omitempty"` } +func (ss *SnapSetup) placeInfo() snap.PlaceInfo { + return snap.MinimalPlaceInfo(ss.Name, ss.Revision) +} + +func (ss *SnapSetup) MountDir() string { + return snap.MountDir(ss.Name, ss.Revision) +} + // SnapState holds the state for a snap installed in the system. type SnapState struct { Sequence []*snap.SideInfo `json:"sequence"` // Last is current @@ -61,12 +65,13 @@ type SnapState struct { DevMode bool `json:"dev-mode,omitempty"` } -func (ss *SnapSetup) placeInfo() snap.PlaceInfo { - return snap.MinimalPlaceInfo(ss.Name, ss.Revision) -} - -func (ss *SnapSetup) MountDir() string { - return snap.MountDir(ss.Name, ss.Revision) +// Current returns the side info for the current revision in the snap revision sequence if there is one. +func (snapst *SnapState) Current() *snap.SideInfo { + n := len(snapst.Sequence) + if n == 0 { + return nil + } + return snapst.Sequence[n-1] } // Manager returns a new snap manager. @@ -88,6 +93,7 @@ func Manager(s *state.State) (*SnapManager, error) { runner.AddHandler("prepare-snap", m.doPrepareSnap, m.undoPrepareSnap) runner.AddHandler("download-snap", m.doDownloadSnap, m.undoPrepareSnap) runner.AddHandler("mount-snap", m.doMountSnap, m.undoMountSnap) + runner.AddHandler("unlink-current-snap", m.doUnlinkCurrentSnap, m.undoUnlinkCurrentSnap) runner.AddHandler("copy-snap-data", m.doCopySnapData, m.undoCopySnapData) runner.AddHandler("link-snap", m.doLinkSnap, m.undoLinkSnap) // FIXME: port to native tasks and rename @@ -95,11 +101,10 @@ func Manager(s *state.State) (*SnapManager, error) { // remove releated runner.AddHandler("unlink-snap", m.doUnlinkSnap, nil) - runner.AddHandler("remove-snap-files", m.doRemoveSnapFiles, nil) - runner.AddHandler("remove-snap-data", m.doRemoveSnapData, nil) + runner.AddHandler("clear-snap", m.doClearSnapData, nil) + runner.AddHandler("discard-snap", m.doDiscardSnap, nil) // FIXME: work on those - runner.AddHandler("rollback-snap", m.doRollbackSnap, nil) runner.AddHandler("activate-snap", m.doActivateSnap, nil) runner.AddHandler("deactivate-snap", m.doDeactivateSnap, nil) @@ -170,13 +175,8 @@ func (m *SnapManager) doDownloadSnap(t *state.Task, _ *tomb.Tomb) error { return err } - // construct the store name - name := ss.Name - if ss.Developer != "" { - name = fmt.Sprintf("%s.%s", ss.Name, ss.Developer) - } pb := &TaskProgressAdapter{task: t} - storeInfo, downloadedSnapFile, err := m.backend.Download(name, ss.Channel, pb) + storeInfo, downloadedSnapFile, err := m.backend.Download(ss.Name, ss.Channel, pb) if err != nil { return err } @@ -194,20 +194,39 @@ func (m *SnapManager) doDownloadSnap(t *state.Task, _ *tomb.Tomb) error { } func (m *SnapManager) doUnlinkSnap(t *state.Task, _ *tomb.Tomb) error { - var ss SnapSetup + // invoked only if snap has a current active revision - t.State().Lock() - err := t.Get("snap-setup", &ss) - t.State().Unlock() + st := t.State() + + // Hold the lock for the full duration of the task here so + // nobody observes a world where the state engine and + // the file system are reporting different things. + st.Lock() + defer st.Unlock() + + ss, snapst, err := snapSetupAndState(t) + if err != nil { + return err + } + + info, err := Info(t.State(), ss.Name, ss.Revision) if err != nil { return err } pb := &TaskProgressAdapter{task: t} - return m.backend.UnlinkSnap(ss.MountDir(), pb) + err = m.backend.UnlinkSnap(info, pb) + if err != nil { + return err + } + + // mark as inactive + snapst.Active = false + Set(st, ss.Name, snapst) + return nil } -func (m *SnapManager) doRemoveSnapFiles(t *state.Task, _ *tomb.Tomb) error { +func (m *SnapManager) doClearSnapData(t *state.Task, _ *tomb.Tomb) error { t.State().Lock() ss, err := TaskSnapSetup(t) t.State().Unlock() @@ -215,34 +234,52 @@ func (m *SnapManager) doRemoveSnapFiles(t *state.Task, _ *tomb.Tomb) error { return err } - pb := &TaskProgressAdapter{task: t} - return m.backend.RemoveSnapFiles(ss.placeInfo(), pb) -} - -func (m *SnapManager) doRemoveSnapData(t *state.Task, _ *tomb.Tomb) error { t.State().Lock() - ss, err := TaskSnapSetup(t) + info, err := Info(t.State(), ss.Name, ss.Revision) t.State().Unlock() if err != nil { return err } - return m.backend.RemoveSnapData(ss.Name, ss.Revision) + return m.backend.RemoveSnapData(info) } -func (m *SnapManager) doRollbackSnap(t *state.Task, _ *tomb.Tomb) error { - var ss SnapSetup +func (m *SnapManager) doDiscardSnap(t *state.Task, _ *tomb.Tomb) error { + st := t.State() - t.State().Lock() - err := t.Get("snap-setup", &ss) - t.State().Unlock() + st.Lock() + ss, snapst, err := snapSetupAndState(t) + st.Unlock() if err != nil { return err } + st.Lock() + if len(snapst.Sequence) == 1 { + snapst.Sequence = nil + } else { + newSeq := make([]*snap.SideInfo, 0, len(snapst.Sequence)) + for _, si := range snapst.Sequence { + if si.Revision == ss.Revision { + // leave out + continue + } + newSeq = append(newSeq, si) + } + snapst.Sequence = newSeq + } + st.Unlock() + pb := &TaskProgressAdapter{task: t} - _, err = m.backend.Rollback(ss.Name, ss.RollbackVersion, pb) - return err + err = m.backend.RemoveSnapFiles(ss.placeInfo(), pb) + if err != nil { + return err + } + + st.Lock() + Set(st, ss.Name, snapst) + st.Unlock() + return nil } func (m *SnapManager) doActivateSnap(t *state.Task, _ *tomb.Tomb) error { @@ -345,7 +382,17 @@ func (m *SnapManager) doMountSnap(t *state.Task, _ *tomb.Tomb) error { return err } - if err := m.backend.CheckSnap(ss.SnapPath, ss.Flags); err != nil { + var curInfo *snap.Info + if cur := snapst.Current(); cur != nil { + var err error + curInfo, err = retrieveInfo(ss.Name, cur) + if err != nil { + return err + } + + } + + if err := m.backend.CheckSnap(ss.SnapPath, curInfo, ss.Flags); err != nil { return err } @@ -354,26 +401,109 @@ func (m *SnapManager) doMountSnap(t *state.Task, _ *tomb.Tomb) error { return m.backend.SetupSnap(ss.SnapPath, snapst.Candidate, ss.Flags) } +func (m *SnapManager) undoUnlinkCurrentSnap(t *state.Task, _ *tomb.Tomb) error { + st := t.State() + + // Hold the lock for the full duration of the task here so + // nobody observes a world where the state engine and + // the file system are reporting different things. + st.Lock() + defer st.Unlock() + + ss, snapst, err := snapSetupAndState(t) + if err != nil { + return err + } + + oldInfo, err := retrieveInfo(ss.Name, snapst.Current()) + if err != nil { + return err + } + + snapst.Active = true + err = m.backend.LinkSnap(oldInfo) + if err != nil { + return err + } + + // mark as active again + Set(st, ss.Name, snapst) + return nil + +} + +func (m *SnapManager) doUnlinkCurrentSnap(t *state.Task, _ *tomb.Tomb) error { + st := t.State() + + // Hold the lock for the full duration of the task here so + // nobody observes a world where the state engine and + // the file system are reporting different things. + st.Lock() + defer st.Unlock() + + ss, snapst, err := snapSetupAndState(t) + if err != nil { + return err + } + + oldInfo, err := retrieveInfo(ss.Name, snapst.Current()) + if err != nil { + return err + } + + snapst.Active = false + + pb := &TaskProgressAdapter{task: t} + err = m.backend.UnlinkSnap(oldInfo, pb) + if err != nil { + return err + } + + // mark as inactive + Set(st, ss.Name, snapst) + return nil +} + func (m *SnapManager) undoCopySnapData(t *state.Task, _ *tomb.Tomb) error { t.State().Lock() - ss, err := TaskSnapSetup(t) + ss, snapst, err := snapSetupAndState(t) t.State().Unlock() if err != nil { return err } - return m.backend.UndoCopySnapData(ss.MountDir(), ss.Flags) + newInfo, err := retrieveInfo(ss.Name, snapst.Candidate) + if err != nil { + return err + } + + return m.backend.UndoCopySnapData(newInfo, ss.Flags) } func (m *SnapManager) doCopySnapData(t *state.Task, _ *tomb.Tomb) error { t.State().Lock() - ss, err := TaskSnapSetup(t) + ss, snapst, err := snapSetupAndState(t) t.State().Unlock() if err != nil { return err } - return m.backend.CopySnapData(ss.MountDir(), ss.Flags) + newInfo, err := retrieveInfo(ss.Name, snapst.Candidate) + if err != nil { + return err + } + + var oldInfo *snap.Info + if cur := snapst.Current(); cur != nil { + var err error + oldInfo, err = retrieveInfo(ss.Name, cur) + if err != nil { + return err + } + + } + + return m.backend.CopySnapData(newInfo, oldInfo, ss.Flags) } func (m *SnapManager) doLinkSnap(t *state.Task, _ *tomb.Tomb) error { @@ -389,12 +519,20 @@ func (m *SnapManager) doLinkSnap(t *state.Task, _ *tomb.Tomb) error { if err != nil { return err } + + cand := snapst.Candidate + m.backend.Candidate(snapst.Candidate) snapst.Sequence = append(snapst.Sequence, snapst.Candidate) snapst.Candidate = nil snapst.Active = true - err = m.backend.LinkSnap(ss.MountDir()) + newInfo, err := retrieveInfo(ss.Name, cand) + if err != nil { + return err + } + + err = m.backend.LinkSnap(newInfo) if err != nil { return err } @@ -405,32 +543,37 @@ func (m *SnapManager) doLinkSnap(t *state.Task, _ *tomb.Tomb) error { } func (m *SnapManager) undoLinkSnap(t *state.Task, _ *tomb.Tomb) error { - t.State().Lock() + st := t.State() + + // Hold the lock for the full duration of the task here so + // nobody observes a world where the state engine and + // the file system are reporting different things. + st.Lock() + defer st.Unlock() + ss, snapst, err := snapSetupAndState(t) - t.State().Unlock() if err != nil { return err } - // No need to undo "snaps" in state here. The only chance of - // having the new state there is a working doLinkSnap call. - newDir := ss.MountDir() - oldDir := "" - if len(snapst.Sequence) > 0 { - latest := snapst.Sequence[len(snapst.Sequence)-1] - oldDir = snap.MountDir(ss.Name, latest.Revision) - } - return m.backend.UndoLinkSnap(oldDir, newDir) -} + // relinking of the old snap is done in the undo of unlink-current-snap -func (m *SnapManager) doGarbageCollect(t *state.Task, _ *tomb.Tomb) error { - t.State().Lock() - ss, err := TaskSnapSetup(t) - t.State().Unlock() + snapst.Candidate = snapst.Sequence[len(snapst.Sequence)-1] + snapst.Sequence = snapst.Sequence[:len(snapst.Sequence)-1] + snapst.Active = false + + newInfo, err := retrieveInfo(ss.Name, snapst.Candidate) if err != nil { return err } pb := &TaskProgressAdapter{task: t} - return m.backend.GarbageCollect(ss.Name, ss.Flags, pb) + err = m.backend.UnlinkSnap(newInfo, pb) + if err != nil { + return err + } + + // mark as inactive + Set(st, ss.Name, snapst) + return nil } diff --git a/overlord/snapstate/snapmgr_test.go b/overlord/snapstate/snapmgr_test.go index f1b8fc48ae..d537c06b13 100644 --- a/overlord/snapstate/snapmgr_test.go +++ b/overlord/snapstate/snapmgr_test.go @@ -42,6 +42,8 @@ type snapmgrTestSuite struct { snapmgr *snapstate.SnapManager fakeBackend *fakeSnappyBackend + + reset func() } func (s *snapmgrTestSuite) settle() { @@ -58,8 +60,6 @@ func (s *snapmgrTestSuite) SetUpTest(c *C) { s.fakeBackend = &fakeSnappyBackend{ fakeCurrentProgress: 75, fakeTotalProgress: 100, - - activeSnaps: make(map[string]*snap.Info), } s.state = state.New(nil) @@ -67,17 +67,35 @@ func (s *snapmgrTestSuite) SetUpTest(c *C) { s.snapmgr, err = snapstate.Manager(s.state) c.Assert(err, IsNil) s.snapmgr.AddForeignTaskHandlers() + + // XXX: have just one, reset! snapstate.SetSnapManagerBackend(s.snapmgr, s.fakeBackend) snapstate.SetSnapstateBackend(s.fakeBackend) + + s.reset = snapstate.ChangeRetrieveInfo(s.fakeBackend.RetrieveInfo) } -func verifyInstallUpdateTasks(c *C, ts *state.TaskSet) { +func (s *snapmgrTestSuite) TearDownTest(c *C) { + s.reset() +} + +func verifyInstallUpdateTasks(c *C, curActive bool, ts *state.TaskSet, st *state.State) { i := 0 - c.Assert(ts.Tasks(), HasLen, 5) + n := 5 + if curActive { + n++ + } + c.Assert(ts.Tasks(), HasLen, n) + // all tasks are accounted + c.Assert(st.Tasks(), HasLen, n) c.Assert(ts.Tasks()[i].Kind(), Equals, "download-snap") i++ c.Assert(ts.Tasks()[i].Kind(), Equals, "mount-snap") i++ + if curActive { + c.Assert(ts.Tasks()[i].Kind(), Equals, "unlink-current-snap") + i++ + } c.Assert(ts.Tasks()[i].Kind(), Equals, "copy-snap-data") i++ c.Assert(ts.Tasks()[i].Kind(), Equals, "setup-snap-security") @@ -91,42 +109,46 @@ func (s *snapmgrTestSuite) TestInstallTasks(c *C) { ts, err := snapstate.Install(s.state, "some-snap", "some-channel", 0) c.Assert(err, IsNil) - verifyInstallUpdateTasks(c, ts) + verifyInstallUpdateTasks(c, false, ts, s.state) } func (s *snapmgrTestSuite) TestUpdateTasks(c *C) { - s.fakeBackend.activeSnaps["some-snap"] = &snap.Info{ - SuggestedName: "some-snap", - } - s.state.Lock() defer s.state.Unlock() + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{{OfficialName: "some-snap", Revision: 11}}, + }) + ts, err := snapstate.Update(s.state, "some-snap", "some-channel", 0) c.Assert(err, IsNil) - verifyInstallUpdateTasks(c, ts) + verifyInstallUpdateTasks(c, true, ts, s.state) } func (s *snapmgrTestSuite) TestRemoveTasks(c *C) { - s.fakeBackend.activeSnaps["foo"] = &snap.Info{ - SuggestedName: "foo", - } - s.state.Lock() defer s.state.Unlock() + snapstate.Set(s.state, "foo", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{{OfficialName: "foo"}}, + }) + ts, err := snapstate.Remove(s.state, "foo", 0) c.Assert(err, IsNil) i := 0 c.Assert(ts.Tasks(), HasLen, 4) + // all tasks are accounted + c.Assert(s.state.Tasks(), HasLen, 4) c.Assert(ts.Tasks()[i].Kind(), Equals, "unlink-snap") i++ c.Assert(ts.Tasks()[i].Kind(), Equals, "remove-snap-security") i++ - c.Assert(ts.Tasks()[i].Kind(), Equals, "remove-snap-data") + c.Assert(ts.Tasks()[i].Kind(), Equals, "clear-snap") i++ - c.Assert(ts.Tasks()[i].Kind(), Equals, "remove-snap-files") + c.Assert(ts.Tasks()[i].Kind(), Equals, "discard-snap") } func (s *snapmgrTestSuite) TestInstallIntegration(c *C) { @@ -134,25 +156,26 @@ func (s *snapmgrTestSuite) TestInstallIntegration(c *C) { defer s.state.Unlock() chg := s.state.NewChange("install", "install a snap") - ts, err := snapstate.Install(s.state, "some-snap.mvo", "some-channel", 0) + ts, err := snapstate.Install(s.state, "some-snap", "some-channel", 0) c.Assert(err, IsNil) chg.AddAll(ts) s.state.Unlock() - s.settle() defer s.snapmgr.Stop() + s.settle() s.state.Lock() // ensure all our tasks ran c.Assert(s.fakeBackend.ops, DeepEquals, []fakeOp{ fakeOp{ op: "download", - name: "some-snap.mvo", + name: "some-snap", channel: "some-channel", }, fakeOp{ op: "check-snap", name: "downloaded-snap-path", + old: "<no-current>", }, fakeOp{ op: "setup-snap", @@ -162,6 +185,7 @@ func (s *snapmgrTestSuite) TestInstallIntegration(c *C) { fakeOp{ op: "copy-data", name: "/snap/some-snap/11", + old: "<no-old>", }, fakeOp{ op: "candidate", @@ -188,11 +212,10 @@ func (s *snapmgrTestSuite) TestInstallIntegration(c *C) { err = task.Get("snap-setup", &ss) c.Assert(err, IsNil) c.Assert(ss, DeepEquals, snapstate.SnapSetup{ - Name: "some-snap", - Revision: 11, - Developer: "mvo", - Channel: "some-channel", - SnapPath: "downloaded-snap-path", + Name: "some-snap", + Revision: 11, + Channel: "some-channel", + SnapPath: "downloaded-snap-path", }) // verify snaps in the system state @@ -211,37 +234,40 @@ func (s *snapmgrTestSuite) TestInstallIntegration(c *C) { } func (s *snapmgrTestSuite) TestUpdateIntegration(c *C) { - s.fakeBackend.activeSnaps["some-snap"] = &snap.Info{ - SideInfo: snap.SideInfo{ - OfficialName: "some-snap", - Revision: 7, - }, + si := snap.SideInfo{ + OfficialName: "some-snap", + Revision: 7, } s.state.Lock() defer s.state.Unlock() + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{&si}, + }) + chg := s.state.NewChange("install", "install a snap") - ts, err := snapstate.Update(s.state, "some-snap.mvo", "some-channel", snappy.DoInstallGC) + ts, err := snapstate.Update(s.state, "some-snap", "some-channel", snappy.DoInstallGC) c.Assert(err, IsNil) chg.AddAll(ts) s.state.Unlock() - s.settle() defer s.snapmgr.Stop() + s.settle() s.state.Lock() - // ensure all our tasks ran - c.Assert(s.fakeBackend.ops, DeepEquals, []fakeOp{ + expected := []fakeOp{ fakeOp{ op: "download", - name: "some-snap.mvo", + name: "some-snap", channel: "some-channel", }, fakeOp{ op: "check-snap", name: "downloaded-snap-path", flags: int(snappy.DoInstallGC), + old: "/snap/some-snap/7", }, fakeOp{ op: "setup-snap", @@ -250,9 +276,14 @@ func (s *snapmgrTestSuite) TestUpdateIntegration(c *C) { revno: 11, }, fakeOp{ + op: "unlink-snap", + name: "/snap/some-snap/7", + }, + fakeOp{ op: "copy-data", name: "/snap/some-snap/11", flags: int(snappy.DoInstallGC), + old: "/snap/some-snap/7", }, fakeOp{ op: "candidate", @@ -266,7 +297,10 @@ func (s *snapmgrTestSuite) TestUpdateIntegration(c *C) { op: "link-snap", name: "/snap/some-snap/11", }, - }) + } + + // ensure all our tasks ran + c.Assert(s.fakeBackend.ops, DeepEquals, expected) // check progress task := ts.Tasks()[0] @@ -279,15 +313,240 @@ func (s *snapmgrTestSuite) TestUpdateIntegration(c *C) { err = task.Get("snap-setup", &ss) c.Assert(err, IsNil) c.Assert(ss, DeepEquals, snapstate.SnapSetup{ - Name: "some-snap", - Developer: "mvo", - Channel: "some-channel", - Flags: int(snappy.DoInstallGC), + Name: "some-snap", + Channel: "some-channel", + Flags: int(snappy.DoInstallGC), Revision: 11, SnapPath: "downloaded-snap-path", }) + + // verify snaps in the system state + var snapst snapstate.SnapState + err = snapstate.Get(s.state, "some-snap", &snapst) + c.Assert(err, IsNil) + + c.Assert(snapst.Active, Equals, true) + c.Assert(snapst.Candidate, IsNil) + c.Assert(snapst.Sequence, HasLen, 2) + c.Assert(snapst.Sequence[0], DeepEquals, &snap.SideInfo{ + OfficialName: "some-snap", + Channel: "", + Revision: 7, + }) + c.Assert(snapst.Sequence[1], DeepEquals, &snap.SideInfo{ + OfficialName: "some-snap", + Channel: "some-channel", + Revision: 11, + }) +} + +func (s *snapmgrTestSuite) TestUpdateUndoIntegration(c *C) { + si := snap.SideInfo{ + OfficialName: "some-snap", + Revision: 7, + } + + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{&si}, + }) + + chg := s.state.NewChange("install", "install a snap") + ts, err := snapstate.Update(s.state, "some-snap", "some-channel", snappy.DoInstallGC) + c.Assert(err, IsNil) + chg.AddAll(ts) + + s.fakeBackend.linkSnapFailTrigger = "/snap/some-snap/11" + + s.state.Unlock() + defer s.snapmgr.Stop() + s.settle() + s.state.Lock() + + expected := []fakeOp{ + { + op: "download", + name: "some-snap", + channel: "some-channel", + }, + { + op: "check-snap", + name: "downloaded-snap-path", + flags: int(snappy.DoInstallGC), + old: "/snap/some-snap/7", + }, + { + op: "setup-snap", + name: "downloaded-snap-path", + flags: int(snappy.DoInstallGC), + revno: 11, + }, + { + op: "unlink-snap", + name: "/snap/some-snap/7", + }, + { + op: "copy-data", + name: "/snap/some-snap/11", + flags: int(snappy.DoInstallGC), + old: "/snap/some-snap/7", + }, + { + op: "candidate", + sinfo: snap.SideInfo{ + OfficialName: "some-snap", + Channel: "some-channel", + Revision: 11, + }, + }, + { + op: "link-snap.failed", + name: "/snap/some-snap/11", + }, + // no unlink-snap here is expected! + { + op: "undo-copy-snap-data", + name: "/snap/some-snap/11", + }, + { + op: "link-snap", + name: "/snap/some-snap/7", + }, + { + op: "undo-setup-snap", + name: "/snap/some-snap/11", + }, + } + + // ensure all our tasks ran + c.Assert(s.fakeBackend.ops, DeepEquals, expected) + + // verify snaps in the system state + var snapst snapstate.SnapState + err = snapstate.Get(s.state, "some-snap", &snapst) + c.Assert(err, IsNil) + + c.Assert(snapst.Active, Equals, true) + c.Assert(snapst.Candidate, IsNil) + c.Assert(snapst.Sequence, HasLen, 1) + c.Assert(snapst.Sequence[0], DeepEquals, &snap.SideInfo{ + OfficialName: "some-snap", + Channel: "", + Revision: 7, + }) +} + +func (s *snapmgrTestSuite) TestUpdateTotalUndoIntegration(c *C) { + si := snap.SideInfo{ + OfficialName: "some-snap", + Revision: 7, + } + + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{&si}, + }) + + chg := s.state.NewChange("install", "install a snap") + ts, err := snapstate.Update(s.state, "some-snap", "some-channel", snappy.DoInstallGC) + c.Assert(err, IsNil) + chg.AddAll(ts) + + tasks := ts.Tasks() + last := tasks[len(tasks)-1] + + terr := s.state.NewTask("error-trigger", "provoking total undo") + terr.WaitFor(last) + chg.AddTask(terr) + + s.state.Unlock() + defer s.snapmgr.Stop() + s.settle() + s.state.Lock() + + expected := []fakeOp{ + { + op: "download", + name: "some-snap", + channel: "some-channel", + }, + { + op: "check-snap", + name: "downloaded-snap-path", + flags: int(snappy.DoInstallGC), + old: "/snap/some-snap/7", + }, + { + op: "setup-snap", + name: "downloaded-snap-path", + flags: int(snappy.DoInstallGC), + revno: 11, + }, + { + op: "unlink-snap", + name: "/snap/some-snap/7", + }, + { + op: "copy-data", + name: "/snap/some-snap/11", + flags: int(snappy.DoInstallGC), + old: "/snap/some-snap/7", + }, + { + op: "candidate", + sinfo: snap.SideInfo{ + OfficialName: "some-snap", + Channel: "some-channel", + Revision: 11, + }, + }, + { + op: "link-snap", + name: "/snap/some-snap/11", + }, + // undoing everything from here down... + { + op: "unlink-snap", + name: "/snap/some-snap/11", + }, + { + op: "undo-copy-snap-data", + name: "/snap/some-snap/11", + }, + { + op: "link-snap", + name: "/snap/some-snap/7", + }, + { + op: "undo-setup-snap", + name: "/snap/some-snap/11", + }, + } + + // ensure all our tasks ran + c.Assert(s.fakeBackend.ops, DeepEquals, expected) + + // verify snaps in the system state + var snapst snapstate.SnapState + err = snapstate.Get(s.state, "some-snap", &snapst) + c.Assert(err, IsNil) + + c.Assert(snapst.Active, Equals, true) + c.Assert(snapst.Candidate, IsNil) + c.Assert(snapst.Sequence, HasLen, 1) + c.Assert(snapst.Sequence[0], DeepEquals, &snap.SideInfo{ + OfficialName: "some-snap", + Channel: "", + Revision: 7, + }) } func makeTestSnap(c *C, snapYamlContent string) (snapFilePath string) { @@ -318,8 +577,8 @@ version: 1.0`) chg.AddAll(ts) s.state.Unlock() - s.settle() defer s.snapmgr.Stop() + s.settle() s.state.Lock() // ensure only local install was run, i.e. first action is check-snap @@ -340,79 +599,86 @@ version: 1.0`) Revision: 0, SnapPath: mockSnap, }) + + // verify snaps in the system state + var snapst snapstate.SnapState + err = snapstate.Get(s.state, "mock", &snapst) + c.Assert(err, IsNil) + + c.Assert(snapst.Active, Equals, true) + c.Assert(snapst.Candidate, IsNil) + c.Assert(snapst.Sequence[0], DeepEquals, &snap.SideInfo{ + OfficialName: "", // XXX: do we want this state of things? + Channel: "", + Revision: 0, + }) } func (s *snapmgrTestSuite) TestRemoveIntegration(c *C) { - s.fakeBackend.activeSnaps["some-snap"] = &snap.Info{ - SideInfo: snap.SideInfo{ - OfficialName: "some-name", - Developer: "mvo", - Revision: 7, - }, + si := snap.SideInfo{ + OfficialName: "some-snap", + Revision: 7, } s.state.Lock() defer s.state.Unlock() + + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{&si}, + }) + chg := s.state.NewChange("remove", "remove a snap") - ts, err := snapstate.Remove(s.state, "some-snap.mvo", 0) + ts, err := snapstate.Remove(s.state, "some-snap", 0) c.Assert(err, IsNil) chg.AddAll(ts) s.state.Unlock() - s.settle() defer s.snapmgr.Stop() + s.settle() s.state.Lock() c.Assert(s.fakeBackend.ops, HasLen, 4) - c.Assert(s.fakeBackend.ops, DeepEquals, []fakeOp{ + expected := []fakeOp{ fakeOp{ - op: "can-remove", - name: "/snap/some-snap/7", + op: "can-remove", + name: "/snap/some-snap/7", + active: true, }, fakeOp{ op: "unlink-snap", name: "/snap/some-snap/7", }, fakeOp{ - op: "remove-snap-data", - name: "some-snap", - revno: 7, + op: "remove-snap-data", + name: "/snap/some-snap/7", }, fakeOp{ op: "remove-snap-files", name: "/snap/some-snap/7", }, - }) + } + c.Assert(s.fakeBackend.ops, DeepEquals, expected) // verify snapSetup info - task := ts.Tasks()[0] + tasks := ts.Tasks() + task := tasks[len(tasks)-1] var ss snapstate.SnapSetup err = task.Get("snap-setup", &ss) c.Assert(err, IsNil) c.Assert(ss, DeepEquals, snapstate.SnapSetup{ - Name: "some-snap", - Developer: "mvo", - Revision: 7, + Name: "some-snap", + Revision: 7, }) -} - -func (s *snapmgrTestSuite) TestRollbackIntegration(c *C) { - s.state.Lock() - defer s.state.Unlock() - chg := s.state.NewChange("rollback", "rollback a snap") - ts, err := snapstate.Rollback(s.state, "some-snap-to-rollback", "1.0") + // verify snaps in the system state + var snapst snapstate.SnapState + err = snapstate.Get(s.state, "some-snap", &snapst) c.Assert(err, IsNil) - chg.AddAll(ts) - s.state.Unlock() - s.settle() - defer s.snapmgr.Stop() - s.state.Lock() - - c.Assert(s.fakeBackend.ops[0].op, Equals, "rollback") - c.Assert(s.fakeBackend.ops[0].name, Equals, "some-snap-to-rollback") - c.Assert(s.fakeBackend.ops[0].rollback, Equals, "1.0") + c.Assert(snapst.Sequence, HasLen, 0) + c.Assert(snapst.Active, Equals, false) + c.Assert(snapst.Candidate, IsNil) } func (s *snapmgrTestSuite) TestActivate(c *C) { @@ -424,8 +690,8 @@ func (s *snapmgrTestSuite) TestActivate(c *C) { chg.AddAll(ts) s.state.Unlock() - s.settle() defer s.snapmgr.Stop() + s.settle() s.state.Lock() c.Assert(s.fakeBackend.ops[0].op, Equals, "activate") @@ -442,8 +708,8 @@ func (s *snapmgrTestSuite) TestSetInactive(c *C) { chg.AddAll(ts) s.state.Unlock() - s.settle() defer s.snapmgr.Stop() + s.settle() s.state.Lock() c.Assert(s.fakeBackend.ops[0].op, Equals, "activate") @@ -451,9 +717,14 @@ func (s *snapmgrTestSuite) TestSetInactive(c *C) { c.Assert(s.fakeBackend.ops[0].active, Equals, false) } -func (s *snapmgrTestSuite) TestInfo(c *C) { - s.state.Lock() - defer s.state.Unlock() +type snapmgrQuerySuite struct{} + +var _ = Suite(&snapmgrQuerySuite{}) + +func (s *snapmgrQuerySuite) TestInfo(c *C) { + st := state.New(nil) + st.Lock() + defer st.Unlock() dirs.SetRootDir(c.MkDir()) defer dirs.SetRootDir("") @@ -470,14 +741,14 @@ description: | Lots of text`), 0644) c.Assert(err, IsNil) - snapstate.Set(s.state, "name1", &snapstate.SnapState{ + snapstate.Set(st, "name1", &snapstate.SnapState{ Sequence: []*snap.SideInfo{ {OfficialName: "name1", Revision: 11, EditedSummary: "s11"}, {OfficialName: "name1", Revision: 12, EditedSummary: "s12"}, }, }) - info, err := snapstate.Info(s.state, "name1", 11) + info, err := snapstate.Info(st, "name1", 11) c.Assert(err, IsNil) c.Check(info.Name(), Equals, "name1") diff --git a/overlord/snapstate/snapstate.go b/overlord/snapstate/snapstate.go index 404609344e..f6673f081d 100644 --- a/overlord/snapstate/snapstate.go +++ b/overlord/snapstate/snapstate.go @@ -29,6 +29,7 @@ import ( "strings" "github.com/ubuntu-core/snappy/i18n" + "github.com/ubuntu-core/snappy/logger" "github.com/ubuntu-core/snappy/osutil" "github.com/ubuntu-core/snappy/overlord/state" "github.com/ubuntu-core/snappy/snap" @@ -38,7 +39,7 @@ import ( // allow exchange in the tests var backend managerBackend = &defaultBackend{} -func doInstall(s *state.State, snapName, channel string, flags snappy.InstallFlags) (*state.TaskSet, error) { +func doInstall(s *state.State, curActive bool, snapName, channel string, flags snappy.InstallFlags) (*state.TaskSet, error) { // download var prepare *state.Task ss := SnapSetup{ @@ -49,58 +50,77 @@ func doInstall(s *state.State, snapName, channel string, flags snappy.InstallFla ss.SnapPath = snapName prepare = s.NewTask("prepare-snap", fmt.Sprintf(i18n.G("Prepare snap %q"), snapName)) } else { - name, developer := snappy.SplitDeveloper(snapName) - ss.Name = name - ss.Developer = developer + ss.Name = snapName prepare = s.NewTask("download-snap", fmt.Sprintf(i18n.G("Download snap %q"), snapName)) } prepare.Set("snap-setup", ss) + tasks := []*state.Task{prepare} + addTask := func(t *state.Task) { + t.Set("snap-setup-task", prepare.ID()) + tasks = append(tasks, t) + } + // mount mount := s.NewTask("mount-snap", fmt.Sprintf(i18n.G("Mount snap %q"), snapName)) - mount.Set("snap-setup-task", prepare.ID()) + addTask(mount) mount.WaitFor(prepare) + precopy := mount + + if curActive { + // unlink-current-snap (will stop services for copy-data) + unlink := s.NewTask("unlink-current-snap", fmt.Sprintf(i18n.G("Make current revision for snap %q unavailable"), snapName)) + addTask(unlink) + unlink.WaitFor(mount) + precopy = unlink + } - // copy-data (needs to stop services) + // copy-data (needs stopped services by unlink) copyData := s.NewTask("copy-snap-data", fmt.Sprintf(i18n.G("Copy snap %q data"), snapName)) - copyData.Set("snap-setup-task", prepare.ID()) - copyData.WaitFor(mount) + addTask(copyData) + copyData.WaitFor(precopy) // security setupSecurity := s.NewTask("setup-snap-security", fmt.Sprintf(i18n.G("Setup snap %q security profiles"), snapName)) - setupSecurity.Set("snap-setup-task", prepare.ID()) + addTask(setupSecurity) setupSecurity.WaitFor(copyData) // finalize (wrappers+current symlink) linkSnap := s.NewTask("link-snap", fmt.Sprintf(i18n.G("Make snap %q available to the system"), snapName)) - linkSnap.Set("snap-setup-task", prepare.ID()) + addTask(linkSnap) linkSnap.WaitFor(setupSecurity) - return state.NewTaskSet(prepare, mount, copyData, setupSecurity, linkSnap), nil + return state.NewTaskSet(tasks...), nil } // Install returns a set of tasks for installing snap. // Note that the state must be locked by the caller. -func Install(s *state.State, snap, channel string, flags snappy.InstallFlags) (*state.TaskSet, error) { - name, _ := snappy.SplitDeveloper(snap) - info := backend.ActiveSnap(name) - if info != nil { - return nil, fmt.Errorf("snap %q already installed", snap) +func Install(s *state.State, name, channel string, flags snappy.InstallFlags) (*state.TaskSet, error) { + var snapst SnapState + err := Get(s, name, &snapst) + if err != nil && err != state.ErrNoState { + return nil, err + } + if snapst.Current() != nil { + return nil, fmt.Errorf("snap %q already installed", name) } - return doInstall(s, snap, channel, flags) + return doInstall(s, false, name, channel, flags) } // Update initiates a change updating a snap. // Note that the state must be locked by the caller. -func Update(s *state.State, snap, channel string, flags snappy.InstallFlags) (*state.TaskSet, error) { - name, _ := snappy.SplitDeveloper(snap) - info := backend.ActiveSnap(name) - if info == nil { - return nil, fmt.Errorf("cannot find snap %q", snap) +func Update(s *state.State, name, channel string, flags snappy.InstallFlags) (*state.TaskSet, error) { + var snapst SnapState + err := Get(s, name, &snapst) + if err != nil && err != state.ErrNoState { + return nil, err + } + if snapst.Current() == nil { + return nil, fmt.Errorf("cannot find snap %q", name) } - return doInstall(s, snap, channel, flags) + return doInstall(s, snapst.Active, name, channel, flags) } // parseSnapspec parses a string like: name[.developer][=version] @@ -118,15 +138,27 @@ func Remove(s *state.State, snapSpec string, flags snappy.RemoveFlags) (*state.T // allow remove by version so that we can remove snaps that are // not active name, version := parseSnapSpec(snapSpec) - name, developer := snappy.SplitDeveloper(name) + + var snapst SnapState + err := Get(s, name, &snapst) + if err != nil && err != state.ErrNoState { + return nil, err + } + + cur := snapst.Current() + if cur == nil { + return nil, fmt.Errorf("cannot find snap %q", name) + } + revision := 0 + active := false if version == "" { - info := backend.ActiveSnap(name) - if info == nil { + if !snapst.Active { return nil, fmt.Errorf("cannot find active snap for %q", name) } - revision = info.Revision + revision = snapst.Current().Revision } else { + // XXX: change this to use snapstate stuff info := backend.SnapByNameAndVersion(name, version) if info == nil { return nil, fmt.Errorf("cannot find snap for %q and version %q", name, version) @@ -134,46 +166,69 @@ func Remove(s *state.State, snapSpec string, flags snappy.RemoveFlags) (*state.T revision = info.Revision } + // removing active? + if snapst.Active && cur.Revision == revision { + active = true + } + + info, err := Info(s, name, revision) + if err != nil { + return nil, err + } + ss := SnapSetup{ - Name: name, - Developer: developer, - Revision: revision, - Flags: int(flags), + Name: name, + Revision: revision, + Flags: int(flags), } + // check if this is something that can be removed - if err := backend.CanRemove(ss.MountDir()); err != nil { - return nil, err + if !backend.CanRemove(info, active) { + return nil, fmt.Errorf("snap %q is not removable", ss.Name) } // trigger remove - unlink := s.NewTask("unlink-snap", fmt.Sprintf(i18n.G("Deactivating %q"), snapSpec)) - unlink.Set("snap-setup", ss) - removeSecurity := s.NewTask("remove-snap-security", fmt.Sprintf(i18n.G("Removing security profile for %q"), snapSpec)) - removeSecurity.WaitFor(unlink) - removeSecurity.Set("snap-setup-task", unlink.ID()) + // last task but the one holding snap-setup + discardSnap := s.NewTask("discard-snap", fmt.Sprintf(i18n.G("Remove snap %q from the system"), snapSpec)) + discardSnap.Set("snap-setup", ss) + + discardSnapID := discardSnap.ID() + tasks := ([]*state.Task)(nil) + var chain *state.Task + addNext := func(t *state.Task) { + if chain != nil { + t.WaitFor(chain) + } + if t.ID() != discardSnapID { + t.Set("snap-setup-task", discardSnapID) + } + tasks = append(tasks, t) + chain = t + } + + if active { + unlink := s.NewTask("unlink-snap", fmt.Sprintf(i18n.G("Make snap %q unavailable to the system"), snapSpec)) + + addNext(unlink) + } + + removeSecurity := s.NewTask("remove-snap-security", fmt.Sprintf(i18n.G("Remove security profile for snap %q"), snapSpec)) + addNext(removeSecurity) - removeData := s.NewTask("remove-snap-data", fmt.Sprintf(i18n.G("Removing data for %q"), snapSpec)) - removeData.Set("snap-setup-task", unlink.ID()) - removeData.WaitFor(removeSecurity) + clearData := s.NewTask("clear-snap", fmt.Sprintf(i18n.G("Remove data for snap %q"), snapSpec)) + addNext(clearData) - removeFiles := s.NewTask("remove-snap-files", fmt.Sprintf(i18n.G("Removing files for %q"), snapSpec)) - removeFiles.Set("snap-setup-task", unlink.ID()) - removeFiles.WaitFor(removeData) + // discard is last + addNext(discardSnap) - return state.NewTaskSet(unlink, removeSecurity, removeData, removeFiles), nil + return state.NewTaskSet(tasks...), nil } // Rollback returns a set of tasks for rolling back a snap. // Note that the state must be locked by the caller. func Rollback(s *state.State, snap, ver string) (*state.TaskSet, error) { - t := s.NewTask("rollback-snap", fmt.Sprintf(i18n.G("Rolling back %q"), snap)) - t.Set("snap-setup", SnapSetup{ - Name: snap, - RollbackVersion: ver, - }) - - return state.NewTaskSet(t), nil + return nil, fmt.Errorf("rollback not implemented") } // Activate returns a set of tasks for activating a snap. @@ -202,7 +257,7 @@ func Deactivate(s *state.State, snap string) (*state.TaskSet, error) { // Retrieval functions -func retrieveInfo(name string, si *snap.SideInfo) (*snap.Info, error) { +func retrieveInfoImpl(name string, si *snap.SideInfo) (*snap.Info, error) { // XXX: move some of this in snap as helper? snapYamlFn := filepath.Join(snap.MountDir(name, si.Revision), "meta", "snap.yaml") meta, err := ioutil.ReadFile(snapYamlFn) @@ -223,6 +278,8 @@ func retrieveInfo(name string, si *snap.SideInfo) (*snap.Info, error) { return info, nil } +var retrieveInfo = retrieveInfoImpl + // Info returns the information about the snap with given name and revision. // Works also for a mounted candidate snap in the process of being installed. func Info(s *state.State, name string, revision int) (*snap.Info, error) { @@ -285,3 +342,25 @@ func Set(s *state.State, name string, snapst *SnapState) { snaps[name] = &raw s.Set("snaps", snaps) } + +// ActiveInfos returns information about all active snaps. +func ActiveInfos(s *state.State) ([]*snap.Info, error) { + var stateMap map[string]*SnapState + var infos []*snap.Info + if err := s.Get("snaps", &stateMap); err != nil && err != state.ErrNoState { + return nil, err + } + for snapName, snapState := range stateMap { + if !snapState.Active { + continue + } + sideInfo := snapState.Sequence[len(snapState.Sequence)-1] + snapInfo, err := retrieveInfo(snapName, sideInfo) + if err != nil { + logger.Noticef("cannot retrieve info for snap %q: %s", snapName, err) + continue + } + infos = append(infos, snapInfo) + } + return infos, nil +} diff --git a/po/snappy.pot b/po/snappy.pot index 0d4a1ceecb..64bf2a7f72 100644 --- a/po/snappy.pot +++ b/po/snappy.pot @@ -7,7 +7,7 @@ msgid "" msgstr "Project-Id-Version: snappy\n" "Report-Msgid-Bugs-To: snappy-devel@lists.ubuntu.com\n" - "POT-Creation-Date: 2016-04-12 10:31+0200\n" + "POT-Creation-Date: 2016-04-13 21:37+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -103,10 +103,6 @@ msgid "Deactivate an installed active snap" msgstr "" #, c-format -msgid "Deactivating %q" -msgstr "" - -#, c-format msgid "Disconnect %s:%s from %s:%s" msgstr "" @@ -185,10 +181,18 @@ msgid "Login successful" msgstr "" #, c-format +msgid "Make current revision for snap %q unavailable" +msgstr "" + +#, c-format msgid "Make snap %q available to the system" msgstr "" #, c-format +msgid "Make snap %q unavailable to the system" +msgstr "" + +#, c-format msgid "Mount snap %q" msgstr "" @@ -261,21 +265,21 @@ msgstr "" msgid "Remove a snapp part" msgstr "" -#. TRANSLATORS: the %s is a pkgname #, c-format -msgid "Removing %s\n" +msgid "Remove data for snap %q" msgstr "" #, c-format -msgid "Removing data for %q" +msgid "Remove security profile for snap %q" msgstr "" #, c-format -msgid "Removing files for %q" +msgid "Remove snap %q from the system" msgstr "" +#. TRANSLATORS: the %s is a pkgname #, c-format -msgid "Removing security profile for %q" +msgid "Removing %s\n" msgstr "" #, c-format @@ -288,10 +292,6 @@ msgstr "" msgid "Rollback to a previous version of a package" msgstr "" -#, c-format -msgid "Rolling back %q" -msgstr "" - msgid "Runs unsupported experimental commands" msgstr "" diff --git a/snappy/desktop_test.go b/snappy/desktop_test.go index 62c052e550..b485a0ac2b 100644 --- a/snappy/desktop_test.go +++ b/snappy/desktop_test.go @@ -104,7 +104,7 @@ Name=foo Icon=/snap/foo/11/foo.png`) // unlink (deactivate) removes it again - err = UnlinkSnap(snap, nil) + err = UnlinkSnap(snap.Info(), nil) c.Assert(err, IsNil) c.Assert(osutil.FileExists(mockDesktopFilePath), Equals, false) } diff --git a/snappy/info_test.go b/snappy/info_test.go index 400934c822..95ffd22b20 100644 --- a/snappy/info_test.go +++ b/snappy/info_test.go @@ -165,7 +165,7 @@ func (s *SnapTestSuite) TestPackageNameInstalled(c *C) { c.Assert(ActivateSnap(snap, ag), IsNil) c.Check(PackageNameActive("hello-snap"), Equals, true) - c.Assert(UnlinkSnap(snap, ag), IsNil) + c.Assert(UnlinkSnap(snap.Info(), ag), IsNil) c.Check(PackageNameActive("hello-snap"), Equals, false) } diff --git a/snappy/install_test.go b/snappy/install_test.go index 7da5f04ce7..71474b6aeb 100644 --- a/snappy/install_test.go +++ b/snappy/install_test.go @@ -151,15 +151,15 @@ func (s *SnapTestSuite) TestInstallAppTwiceFails(c *C) { var dlURL, iconURL string mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { - case "/details/foo/ch": - io.WriteString(w, `{ + case "/search": + io.WriteString(w, `{"_embedded": {"clickindex:package": [{ "package_name": "foo", "version": "2", "developer": "test", "anon_download_url": "`+dlURL+`", "download_url": "`+dlURL+`", "icon_url": "`+iconURL+`" -}`) +}]}}`) case "/dl": snapR.Seek(0, 0) io.Copy(w, snapR) @@ -175,7 +175,7 @@ func (s *SnapTestSuite) TestInstallAppTwiceFails(c *C) { dlURL = mockServer.URL + "/dl" iconURL = mockServer.URL + "/icon" - s.storeCfg.DetailsURI, err = url.Parse(mockServer.URL + "/details/") + s.storeCfg.SearchURI, err = url.Parse(mockServer.URL + "/search") c.Assert(err, IsNil) name, err := Install("foo", "ch", 0, &progress.NullProgress{}) @@ -203,19 +203,19 @@ func (s *SnapTestSuite) TestInstallAppPackageNameFails(c *C) { mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { - case "/details/hello-snap.potato/ch": - io.WriteString(w, `{ + case "/search": + io.WriteString(w, `{"_embedded": {"clickindex:package": [{ "developer": "potato", "package_name": "hello-snap", "version": "2", "anon_download_url": "blah" -}`) +}]}}`) default: panic("unexpected url path: " + r.URL.Path) } })) - s.storeCfg.DetailsURI, err = url.Parse(mockServer.URL + "/details/") + s.storeCfg.SearchURI, err = url.Parse(mockServer.URL + "/search") c.Assert(err, IsNil) c.Assert(mockServer, NotNil) @@ -244,15 +244,15 @@ func (s *SnapTestSuite) TestUpdate(c *C) { var dlURL, iconURL string mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { - case "/details/foo." + testDeveloper: - io.WriteString(w, `{ + case "/search": + io.WriteString(w, `{"_embedded": {"clickindex:package": [{ "package_name": "foo", "version": "2", "revision": 27, "developer": "`+testDeveloper+`", "anon_download_url": "`+dlURL+`", "icon_url": "`+iconURL+`" -}`) +}]}}`) case "/dl": snapR.Seek(0, 0) io.Copy(w, snapR) @@ -268,7 +268,7 @@ func (s *SnapTestSuite) TestUpdate(c *C) { dlURL = mockServer.URL + "/dl" iconURL = mockServer.URL + "/icon" - s.storeCfg.DetailsURI, err = url.Parse(mockServer.URL + "/details/") + s.storeCfg.SearchURI, err = url.Parse(mockServer.URL + "/search") c.Assert(err, IsNil) // bulk diff --git a/snappy/kernel_os.go b/snappy/kernel_os.go index 9308c9575d..85f38b67ad 100644 --- a/snappy/kernel_os.go +++ b/snappy/kernel_os.go @@ -122,8 +122,8 @@ func extractKernelAssets(s *snap.Info, snapf snap.File, flags InstallFlags, inte // setNextBoot will schedule the given os or kernel snap to be used in // the next boot -func setNextBoot(s *Snap) error { - if s.m.Type != snap.TypeOS && s.m.Type != snap.TypeKernel { +func setNextBoot(s *snap.Info) error { + if s.Type != snap.TypeOS && s.Type != snap.TypeKernel { return nil } @@ -133,13 +133,13 @@ func setNextBoot(s *Snap) error { } var bootvar string - switch s.m.Type { + switch s.Type { case snap.TypeOS: bootvar = "snappy_os" case snap.TypeKernel: bootvar = "snappy_kernel" } - blobName := filepath.Base(s.Info().MountFile()) + blobName := filepath.Base(s.MountFile()) if err := bootloader.SetBootVar(bootvar, blobName); err != nil { return err } diff --git a/snappy/overlord.go b/snappy/overlord.go index b42cbea470..5679aeb1ec 100644 --- a/snappy/overlord.go +++ b/snappy/overlord.go @@ -40,7 +40,7 @@ type Overlord struct { } // CheckSnap ensures that the snap can be installed -func CheckSnap(snapFilePath string, flags InstallFlags, meter progress.Meter) error { +func CheckSnap(snapFilePath string, curInfo *snap.Info, flags InstallFlags, meter progress.Meter) error { allowGadget := (flags & AllowGadget) != 0 allowUnauth := (flags & AllowUnauthenticated) != 0 @@ -57,7 +57,7 @@ func CheckSnap(snapFilePath string, flags InstallFlags, meter progress.Meter) er // This is done earlier in // openSnapFile() to ensure that we do not mount/inspect // potentially dangerous snaps - return canInstall(s, snapf, allowGadget, meter) + return canInstall(s, snapf, curInfo, allowGadget, meter) } // SetupSnap does prepare and mount the snap for further processing @@ -179,71 +179,39 @@ func UndoSetupSnap(s snap.PlaceInfo, meter progress.Meter) { // and can only be used during install right now } -// XXX: ideally should go from Info to Info, likely we will move to something else anyway -func currentSnap(newSnap *snap.Info) *Snap { - currentActiveDir, _ := filepath.EvalSymlinks(filepath.Join(newSnap.MountDir(), "..", "current")) - if currentActiveDir == "" { - return nil - } - - currentSnap, err := NewInstalledSnap(filepath.Join(currentActiveDir, "meta", "snap.yaml")) - if err != nil { - return nil - } - return currentSnap -} - -func CopyData(newSnap *snap.Info, flags InstallFlags, meter progress.Meter) error { +func CopyData(newSnap, oldSnap *snap.Info, flags InstallFlags, meter progress.Meter) error { dataDir := newSnap.DataDir() - // deal with the data: - // - // if there was a previous version, stop it - // from being active so that it stops running and can no longer be - // started then copy the data - // + // deal with the old data or // otherwise just create a empty data dir - oldSnap := currentSnap(newSnap) + if oldSnap == nil { return os.MkdirAll(dataDir, 0755) } - // we need to stop any services and make the commands unavailable - // so that the data can be safely copied - if err := UnlinkSnap(oldSnap, meter); err != nil { - return err - } - - return copySnapData(oldSnap.Info(), newSnap) + return copySnapData(oldSnap, newSnap) } func UndoCopyData(newInfo *snap.Info, flags InstallFlags, meter progress.Meter) { // XXX we were copying data, assume InhibitHooks was false - oldSnap := currentSnap(newInfo) - if oldSnap != nil { - // reactivate the previously inactivated snap - if err := ActivateSnap(oldSnap, meter); err != nil { - logger.Noticef("Setting old version back to active failed: %v", err) - } - } - if err := RemoveSnapData(newInfo); err != nil { logger.Noticef("When cleaning up data for %s %s: %v", newInfo.Name(), newInfo.Version, err) } + } -func GenerateWrappers(s *Snap, inter interacter) error { +func GenerateWrappers(s *snap.Info, inter interacter) error { // add the CLI apps from the snap.yaml - if err := addPackageBinaries(s.Info()); err != nil { + if err := addPackageBinaries(s); err != nil { return err } // add the daemons from the snap.yaml - if err := addPackageServices(s.Info(), inter); err != nil { + if err := addPackageServices(s, inter); err != nil { return err } // add the desktop files - if err := addPackageDesktopFiles(s.Info()); err != nil { + if err := addPackageDesktopFiles(s); err != nil { return err } @@ -252,19 +220,19 @@ func GenerateWrappers(s *Snap, inter interacter) error { // RemoveGeneratedWrappers removes the generated services, binaries, desktop // wrappers -func RemoveGeneratedWrappers(s *Snap, inter interacter) error { +func RemoveGeneratedWrappers(s *snap.Info, inter interacter) error { - err1 := removePackageBinaries(s.Info()) + err1 := removePackageBinaries(s) if err1 != nil { logger.Noticef("Failed to remove binaries for %q: %v", s.Name(), err1) } - err2 := removePackageServices(s.Info(), inter) + err2 := removePackageServices(s, inter) if err2 != nil { logger.Noticef("Failed to remove services for %q: %v", s.Name(), err2) } - err3 := removePackageDesktopFiles(s.Info()) + err3 := removePackageDesktopFiles(s) if err3 != nil { logger.Noticef("Failed to remove desktop files for %q: %v", s.Name(), err3) } @@ -272,8 +240,8 @@ func RemoveGeneratedWrappers(s *Snap, inter interacter) error { return firstErr(err1, err2, err3) } -func UpdateCurrentSymlink(s *Snap, inter interacter) error { - info := s.Info() +// XXX: would really like not to expose this but used in daemon tests atm +func UpdateCurrentSymlink(info *snap.Info, inter interacter) error { mountDir := info.MountDir() currentActiveSymlink := filepath.Join(mountDir, "..", "current") @@ -299,23 +267,15 @@ func UpdateCurrentSymlink(s *Snap, inter interacter) error { // FIXME: create {Os,Kernel}Snap type instead of adding special // cases here - if err := setNextBoot(s); err != nil { + if err := setNextBoot(info); err != nil { return err } return os.Symlink(filepath.Base(dataDir), currentDataSymlink) } -func UndoUpdateCurrentSymlink(oldSnap, newSnap *Snap, inter interacter) error { - if err := removeCurrentSymlink(newSnap, inter); err != nil { - return err - } - return UpdateCurrentSymlink(oldSnap, inter) -} - -func removeCurrentSymlink(s *Snap, inter interacter) error { +func removeCurrentSymlink(info snap.PlaceInfo, inter interacter) error { var err1, err2 error - info := s.Info() // the snap "current" symlink currentActiveSymlink := filepath.Join(info.MountDir(), "..", "current") @@ -371,6 +331,10 @@ func ActivateSnap(s *Snap, inter interacter) error { // security setup was done here! + return LinkSnap(s.Info(), inter) +} + +func LinkSnap(s *snap.Info, inter interacter) error { if err := GenerateWrappers(s, inter); err != nil { return err } @@ -379,8 +343,7 @@ func ActivateSnap(s *Snap, inter interacter) error { } // UnlinkSnap deactivates the given active snap. -func UnlinkSnap(s *Snap, inter interacter) error { - info := s.Info() +func UnlinkSnap(info *snap.Info, inter interacter) error { mountDir := info.MountDir() currentSymlink := filepath.Join(mountDir, "..", "current") @@ -396,12 +359,12 @@ func UnlinkSnap(s *Snap, inter interacter) error { } // remove generated services, binaries, security policy - err1 := RemoveGeneratedWrappers(s, inter) + err1 := RemoveGeneratedWrappers(info, inter) // removing security setup move here! // and finally remove current symlink - err2 := removeCurrentSymlink(s, inter) + err2 := removeCurrentSymlink(info, inter) // FIXME: aggregate errors instead return firstErr(err1, err2) @@ -419,7 +382,16 @@ func (o *Overlord) Install(snapFilePath string, flags InstallFlags, meter progre // // It returns the local snap file or an error func (o *Overlord) InstallWithSideInfo(snapFilePath string, sideInfo *snap.SideInfo, flags InstallFlags, meter progress.Meter) (sp *snap.Info, err error) { - if err := CheckSnap(snapFilePath, flags, meter); err != nil { + var oldInfo *snap.Info + + if sideInfo != nil { + oldSnap := ActiveSnapByName(sideInfo.OfficialName) + if oldSnap != nil { + oldInfo = oldSnap.Info() + } + } + + if err := CheckSnap(snapFilePath, oldInfo, flags, meter); err != nil { return nil, err } @@ -442,10 +414,26 @@ func (o *Overlord) InstallWithSideInfo(snapFilePath string, sideInfo *snap.SideI } // we need this for later - oldSnap := currentSnap(newInfo) + + if oldInfo != nil { + // we need to stop any services and make the commands unavailable + // so that copying data and later activating the new revision + // can work + err = UnlinkSnap(oldInfo, meter) + defer func() { + if err != nil { + if err := LinkSnap(oldInfo, meter); err != nil { + logger.Noticef("When linking old revision: %v", newInfo.Name(), err) + } + } + }() + if err != nil { + return nil, err + } + } // deal with the data - err = CopyData(newInfo, flags, meter) + err = CopyData(newInfo, oldInfo, flags, meter) defer func() { if err != nil { UndoCopyData(newInfo, flags, meter) @@ -462,19 +450,11 @@ func (o *Overlord) InstallWithSideInfo(snapFilePath string, sideInfo *snap.SideI return newInfo, nil } - // if get this far we know the snap is actually mounted. - // XXX: use infos further but anyway this is going away mostly - // once we simplify u-d-f - newSnap, err := NewInstalledSnap(filepath.Join(newInfo.MountDir(), "meta", "snap.yaml")) - if err != nil { - return nil, err - } - - err = ActivateSnap(newSnap, meter) + err = LinkSnap(newInfo, meter) defer func() { - if err != nil && oldSnap != nil { - if err := ActivateSnap(oldSnap, meter); err != nil { - logger.Noticef("When setting old %s version back to active: %v", newSnap.Name(), err) + if err != nil { + if err := UnlinkSnap(newInfo, meter); err != nil { + logger.Noticef("When unlinking failed new snap revision: %v", newInfo.Name(), err) } } }() @@ -482,11 +462,11 @@ func (o *Overlord) InstallWithSideInfo(snapFilePath string, sideInfo *snap.SideI return nil, err } - return newSnap.Info(), nil + return newInfo, nil } // CanInstall checks whether the Snap passes a series of tests required for installation -func canInstall(s *snap.Info, snapf snap.File, allowGadget bool, inter interacter) error { +func canInstall(s *snap.Info, snapf snap.File, curInfo *snap.Info, allowGadget bool, inter interacter) error { // verify we have a valid architecture if !arch.IsSupportedArchitecture(s.Architectures) { return &ErrArchitectureNotSupported{s.Architectures} @@ -505,14 +485,7 @@ func canInstall(s *snap.Info, snapf snap.File, allowGadget bool, inter interacte } } - // XXX: can be cleaner later - currSnap := currentSnap(s) - var curr *snap.Info - if currSnap != nil { - curr = currSnap.Info() - } - - if err := checkLicenseAgreement(s, snapf, curr, inter); err != nil { + if err := checkLicenseAgreement(s, snapf, curInfo, inter); err != nil { return err } @@ -552,20 +525,20 @@ func checkLicenseAgreement(s *snap.Info, snapf snap.File, cur *snap.Info, ag agr return nil } -func CanRemove(s *Snap) bool { +func CanRemove(s *snap.Info, active bool) bool { // Gadget snaps should not be removed as they are a key // building block for Gadgets. Prunning non active ones // is acceptible. - if s.m.Type == snap.TypeGadget && s.IsActive() { + if s.Type == snap.TypeGadget && active { return false } // You never want to remove an active kernel or OS - if (s.m.Type == snap.TypeKernel || s.m.Type == snap.TypeOS) && s.IsActive() { + if (s.Type == snap.TypeKernel || s.Type == snap.TypeOS) && active { return false } - if IsBuiltInSoftware(s.Name()) && s.IsActive() { + if IsBuiltInSoftware(s.Name()) && active { return false } return true @@ -617,11 +590,11 @@ func RemoveSnapFiles(s snap.PlaceInfo, meter progress.Meter) error { // // It returns an error on failure func (o *Overlord) Uninstall(s *Snap, meter progress.Meter) error { - if !CanRemove(s) { + if !CanRemove(s.Info(), s.IsActive()) { return ErrPackageNotRemovable } - if err := UnlinkSnap(s, meter); err != nil && err != ErrSnapNotActive { + if err := UnlinkSnap(s.Info(), meter); err != nil && err != ErrSnapNotActive { return err } @@ -639,14 +612,14 @@ func (o *Overlord) SetActive(s *Snap, active bool, meter progress.Meter) error { if active { // deactivate current first if current := ActiveSnapByName(s.Name()); current != nil { - if err := UnlinkSnap(current, meter); err != nil { + if err := UnlinkSnap(current.Info(), meter); err != nil { return err } } return ActivateSnap(s, meter) } - return UnlinkSnap(s, meter) + return UnlinkSnap(s.Info(), meter) } // Configure configures the given snap diff --git a/snappy/overlord_test.go b/snappy/overlord_test.go index abfabe592e..45098839af 100644 --- a/snappy/overlord_test.go +++ b/snappy/overlord_test.go @@ -195,12 +195,12 @@ license-version: 2 pkgdir := filepath.Dir(filepath.Dir(yamlFile)) c.Assert(os.MkdirAll(filepath.Join(pkgdir, ".click", "info"), 0755), IsNil) c.Assert(ioutil.WriteFile(filepath.Join(pkgdir, ".click", "info", "foox."+testDeveloper+".manifest"), []byte(`{"name": "foox"}`), 0644), IsNil) - snap, err := NewInstalledSnap(yamlFile) + installedSnap, err := NewInstalledSnap(yamlFile) c.Assert(err, IsNil) - c.Assert(ActivateSnap(snap, ag), IsNil) + c.Assert(ActivateSnap(installedSnap, ag), IsNil) pkg := makeTestSnapPackage(c, yaml+"version: 2") - _, err = (&Overlord{}).Install(pkg, 0, ag) + _, err = (&Overlord{}).InstallWithSideInfo(pkg, &snap.SideInfo{OfficialName: "foox"}, 0, ag) c.Assert(err, Equals, nil) c.Check(IsLicenseNotAccepted(err), Equals, false) c.Check(ag.intro, Equals, "") @@ -376,7 +376,7 @@ func (s *SnapTestSuite) TestClickSetActive(c *C) { c.Assert(snaps[1].IsActive(), Equals, true) // deactivate v2 - err = UnlinkSnap(snaps[1], nil) + err = UnlinkSnap(snaps[1].Info(), nil) // set v1 active err = ActivateSnap(snaps[0], nil) snaps, err = (&Overlord{}).Installed() diff --git a/snappy/snapp_test.go b/snappy/snapp_test.go index 216ca1f9d8..b8bff9649a 100644 --- a/snappy/snapp_test.go +++ b/snappy/snapp_test.go @@ -78,9 +78,8 @@ func (s *SnapTestSuite) SetUpTest(c *C) { // do not attempt to hit the real store servers in the tests nowhereURI, _ := url.Parse("") s.storeCfg = &store.SnapUbuntuStoreConfig{ - SearchURI: nowhereURI, - DetailsURI: nowhereURI, - BulkURI: nowhereURI, + SearchURI: nowhereURI, + BulkURI: nowhereURI, } storeConfig = s.storeCfg @@ -236,7 +235,7 @@ func (s *SnapTestSuite) TestUbuntuStoreRepositoryUpdates(c *C) { func (s *SnapTestSuite) TestUbuntuStoreRepositoryUpdatesNoSnaps(c *C) { var err error - s.storeCfg.DetailsURI, err = url.Parse("https://some-uri") + s.storeCfg.SearchURI, err = url.Parse("https://some-uri") c.Assert(err, IsNil) repo := store.NewUbuntuStoreSnapRepository(s.storeCfg, "") c.Assert(repo, NotNil) @@ -513,7 +512,7 @@ type: gadget c.Assert(err, IsNil) makeSnapActive(snapYamlFn) - s.storeCfg.DetailsURI, err = url.Parse(mockServer.URL) + s.storeCfg.SearchURI, err = url.Parse(mockServer.URL) c.Assert(err, IsNil) repo := NewConfiguredUbuntuStoreSnapRepository() c.Assert(repo, NotNil) diff --git a/snappy/undo_test.go b/snappy/undo_test.go index 42453e2dc0..4f0ce5a2f8 100644 --- a/snappy/undo_test.go +++ b/snappy/undo_test.go @@ -137,11 +137,14 @@ version: 2.0`, 12) c.Assert(err, IsNil) + sn1, err := NewInstalledSnap(v1) + c.Assert(err, IsNil) + sn, err := NewInstalledSnap(v2) c.Assert(err, IsNil) // copy data - err = CopyData(sn.Info(), 0, &s.meter) + err = CopyData(sn.Info(), sn1.Info(), 0, &s.meter) c.Assert(err, IsNil) v2data := filepath.Join(dirs.SnapDataDir, "hello/12") l, _ := filepath.Glob(filepath.Join(v2data, "*")) @@ -169,7 +172,7 @@ apps: sn, err := NewInstalledSnap(yaml) c.Assert(err, IsNil) - err = GenerateWrappers(sn, &s.meter) + err = GenerateWrappers(sn.Info(), &s.meter) c.Assert(err, IsNil) l, err := filepath.Glob(filepath.Join(dirs.SnapBinariesDir, "*")) @@ -180,7 +183,7 @@ apps: c.Assert(l, HasLen, 1) // undo via remove - err = RemoveGeneratedWrappers(sn, &s.meter) + err = RemoveGeneratedWrappers(sn.Info(), &s.meter) l, err = filepath.Glob(filepath.Join(dirs.SnapBinariesDir, "*")) c.Assert(err, IsNil) c.Assert(l, HasLen, 0) @@ -208,7 +211,7 @@ version: 2.0 v2, err := NewInstalledSnap(v2yaml) c.Assert(err, IsNil) - err = UpdateCurrentSymlink(v2, &s.meter) + err = UpdateCurrentSymlink(v2.Info(), &s.meter) c.Assert(err, IsNil) v1MountDir := v1.Info().MountDir() @@ -224,8 +227,8 @@ version: 2.0 c.Assert(err, IsNil) c.Assert(currentDataDir, Matches, `.*/22`) - // undo sets the symlink back - err = UndoUpdateCurrentSymlink(v1, v2, &s.meter) + // undo is just update again + err = UpdateCurrentSymlink(v1.Info(), &s.meter) currentActiveDir, err = filepath.EvalSymlinks(currentActiveSymlink) c.Assert(err, IsNil) c.Assert(currentActiveDir, Equals, v1MountDir) diff --git a/store/auth.go b/store/auth.go index f606a37232..c10e385dab 100644 --- a/store/auth.go +++ b/store/auth.go @@ -23,12 +23,7 @@ import ( "encoding/json" "fmt" "net/http" - "os" - "path/filepath" "strings" - - "github.com/ubuntu-core/snappy/oauth" - "github.com/ubuntu-core/snappy/osutil" ) var ( @@ -36,20 +31,13 @@ var ( // MyAppsPackageAccessAPI points to MyApps endpoint to get a package access macaroon MyAppsPackageAccessAPI = myappsAPIBase + "/acl/package_access/" ubuntuoneAPIBase = authURL() - ubuntuoneOauthAPI = ubuntuoneAPIBase + "/tokens/oauth" // UbuntuoneDischargeAPI points to SSO endpoint to discharge a macaroon UbuntuoneDischargeAPI = ubuntuoneAPIBase + "/tokens/discharge" ) -// StoreToken contains the personal token to access the store -type StoreToken struct { - OpenID string `json:"openid"` - TokenName string `json:"token_name"` - DateUpdated string `json:"date_updated"` - DateCreated string `json:"date_created"` - Href string `json:"href"` - - oauth.Token +// Authenticator interface to set required authorization headers for requests to the store +type Authenticator interface { + Authenticate(r *http.Request) } type ssoMsg struct { @@ -67,103 +55,6 @@ func httpStatusCodeClientError(httpStatusCode int) bool { return httpStatusCode/100 == 4 } -// RequestStoreToken requests a token for accessing the ubuntu store -func RequestStoreToken(username, password, tokenName, otp string) (*StoreToken, error) { - data := map[string]string{ - "email": username, - "password": password, - "token_name": tokenName, - } - if otp != "" { - data["otp"] = otp - } - jsonData, err := json.Marshal(data) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("POST", ubuntuoneOauthAPI, strings.NewReader(string(jsonData))) - if err != nil { - return nil, err - } - req.Header.Set("content-type", "application/json") - - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - // check return code, error on 4xx and anything !200 - switch { - case httpStatusCodeClientError(resp.StatusCode): - // we get a error code, check json details - var msg ssoMsg - dec := json.NewDecoder(resp.Body) - if err := dec.Decode(&msg); err != nil { - return nil, err - } - if msg.Code == "TWOFACTOR_REQUIRED" { - return nil, ErrAuthenticationNeeds2fa - } - - // XXX: maybe return msg.Message as well to the client? - return nil, ErrInvalidCredentials - - case !httpStatusCodeSuccess(resp.StatusCode): - // unexpected result, bail - return nil, fmt.Errorf("failed to get store token: %v (%v)", resp.StatusCode, resp) - } - - var token StoreToken - dec := json.NewDecoder(resp.Body) - if err := dec.Decode(&token); err != nil { - return nil, err - } - - return &token, nil -} - -// FIXME: maybe use a name in /var/lib/users/$user/snappy instead? -// as sabdfl prefers $HOME to be for user created data? -func storeTokenFilename() string { - homeDir, _ := osutil.CurrentHomeDir() - return filepath.Join(homeDir, "snaps", "snappy", "auth", "sso.json") -} - -// WriteStoreToken takes the token and stores it on the filesystem for -// later reading via ReadStoreToken() -func WriteStoreToken(token StoreToken) error { - targetFile := storeTokenFilename() - if err := os.MkdirAll(filepath.Dir(targetFile), 0750); err != nil { - return err - } - outStr, err := json.MarshalIndent(token, "", " ") - if err != nil { - return nil - } - - return osutil.AtomicWriteFile(targetFile, []byte(outStr), 0600, 0) -} - -// ReadStoreToken reads a token previously write via WriteStoreToken -func ReadStoreToken() (*StoreToken, error) { - targetFile := storeTokenFilename() - f, err := os.Open(targetFile) - if err != nil { - return nil, err - } - - var readStoreToken StoreToken - dec := json.NewDecoder(f) - if err := dec.Decode(&readStoreToken); err != nil { - return nil, err - } - - return &readStoreToken, nil -} - // RequestPackageAccessMacaroon requests a macaroon for accessing package data from the ubuntu store. func RequestPackageAccessMacaroon() (string, error) { const errorPrefix = "cannot get package access macaroon from store: " diff --git a/store/auth_test.go b/store/auth_test.go index 559099d9c1..e73a772119 100644 --- a/store/auth_test.go +++ b/store/auth_test.go @@ -21,28 +21,16 @@ package store import ( "io" - "io/ioutil" "net/http" "net/http/httptest" - "os" - "path/filepath" - - "github.com/ubuntu-core/snappy/oauth" - "github.com/ubuntu-core/snappy/osutil" . "gopkg.in/check.v1" ) -type authTestSuite struct { - tempdir string -} +type authTestSuite struct{} var _ = Suite(&authTestSuite{}) -func (s *authTestSuite) SetUpTest(c *C) { - s.tempdir = c.MkDir() -} - const mockStoreInvalidLoginCode = 401 const mockStoreInvalidLogin = ` { @@ -61,20 +49,6 @@ const mockStoreNeeds2fa = ` } ` -const mockStoreReturnToken = ` -{ - "openid": "the-open-id-string-that-is-also-the-consumer-key-in-our-store", - "token_name": "some-token-name", - "date_updated": "2015-02-27T15:00:55.062", - "token_key": "the-token-key", - "consumer_secret": "the-consumer-secret", - "href": "/api/v2/tokens/oauth/something", - "date_created": "2015-02-27T14:54:30.863", - "consumer_key": "the-consumer-key", - "token_secret": "the-token-secret" -} -` - const mockStoreReturnMacaroon = ` { "macaroon": "the-root-macaroon-serialized-data" @@ -89,48 +63,6 @@ const mockStoreReturnDischarge = ` const mockStoreReturnNoMacaroon = `{}` -func (s *authTestSuite) TestRequestStoreToken(c *C) { - mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - io.WriteString(w, mockStoreReturnToken) - })) - c.Assert(mockServer, NotNil) - defer mockServer.Close() - ubuntuoneOauthAPI = mockServer.URL + "/token/oauth" - - token, err := RequestStoreToken("guy@example.com", "passwd", "some-token-name", "") - c.Assert(err, IsNil) - c.Assert(token.TokenKey, Equals, "the-token-key") - c.Assert(token.TokenSecret, Equals, "the-token-secret") - c.Assert(token.ConsumerSecret, Equals, "the-consumer-secret") - c.Assert(token.ConsumerKey, Equals, "the-consumer-key") -} - -func (s *authTestSuite) TestRequestStoreTokenNeeds2fa(c *C) { - mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(mockStoreNeeds2faHTTPCode) - io.WriteString(w, mockStoreNeeds2fa) - })) - c.Assert(mockServer, NotNil) - defer mockServer.Close() - ubuntuoneOauthAPI = mockServer.URL + "/token/oauth" - - _, err := RequestStoreToken("foo@example.com", "passwd", "some-token-name", "") - c.Assert(err, Equals, ErrAuthenticationNeeds2fa) -} - -func (s *authTestSuite) TestRequestStoreTokenInvalidLogin(c *C) { - mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(mockStoreInvalidLoginCode) - io.WriteString(w, mockStoreInvalidLogin) - })) - c.Assert(mockServer, NotNil) - defer mockServer.Close() - ubuntuoneOauthAPI = mockServer.URL + "/token/oauth" - - _, err := RequestStoreToken("foo@example.com", "passwd", "some-token-name", "") - c.Assert(err, Equals, ErrInvalidCredentials) -} - func (s *authTestSuite) TestRequestPackageAccessMacaroon(c *C) { mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { io.WriteString(w, mockStoreReturnMacaroon) @@ -228,43 +160,3 @@ func (s *authTestSuite) TestDischargeAuthCaveatError(c *C) { c.Assert(err, ErrorMatches, "cannot get discharge macaroon from store: server returned status 500") c.Assert(discharge, Equals, "") } - -func (s *authTestSuite) TestWriteStoreToken(c *C) { - os.Setenv("HOME", s.tempdir) - mockStoreToken := StoreToken{TokenName: "meep"} - err := WriteStoreToken(mockStoreToken) - - c.Assert(err, IsNil) - outFile := filepath.Join(s.tempdir, "snaps", "snappy", "auth", "sso.json") - c.Assert(osutil.FileExists(outFile), Equals, true) - content, err := ioutil.ReadFile(outFile) - c.Assert(err, IsNil) - c.Assert(string(content), Equals, `{ - "openid": "", - "token_name": "meep", - "date_updated": "", - "date_created": "", - "href": "", - "token_key": "", - "token_secret": "", - "consumer_secret": "", - "consumer_key": "" -}`) -} - -func (s *authTestSuite) TestReadStoreToken(c *C) { - os.Setenv("HOME", s.tempdir) - mockStoreToken := StoreToken{ - TokenName: "meep", - Token: oauth.Token{ - TokenKey: "token-key", - TokenSecret: "token-secret", - }, - } - err := WriteStoreToken(mockStoreToken) - c.Assert(err, IsNil) - - readToken, err := ReadStoreToken() - c.Assert(err, IsNil) - c.Assert(readToken, DeepEquals, &mockStoreToken) -} diff --git a/store/snap_remote_repo.go b/store/snap_remote_repo.go index a0f02858d8..c1b8189802 100644 --- a/store/snap_remote_repo.go +++ b/store/snap_remote_repo.go @@ -36,7 +36,7 @@ import ( "github.com/ubuntu-core/snappy/arch" "github.com/ubuntu-core/snappy/asserts" - "github.com/ubuntu-core/snappy/oauth" + "github.com/ubuntu-core/snappy/logger" "github.com/ubuntu-core/snappy/progress" "github.com/ubuntu-core/snappy/release" "github.com/ubuntu-core/snappy/snap" @@ -72,7 +72,6 @@ func infoFromRemote(d snapDetails) *snap.Info { // SnapUbuntuStoreConfig represents the configuration to access the snap store type SnapUbuntuStoreConfig struct { SearchURI *url.URL - DetailsURI *url.URL BulkURI *url.URL AssertionsURI *url.URL } @@ -81,7 +80,6 @@ type SnapUbuntuStoreConfig struct { type SnapUbuntuStoreRepository struct { storeID string searchURI *url.URL - detailsURI *url.URL bulkURI *url.URL assertionsURI *url.URL // reused http client @@ -163,11 +161,6 @@ func init() { v.Set("fields", strings.Join(getStructFields(snapDetails{}), ",")) defaultConfig.SearchURI.RawQuery = v.Encode() - defaultConfig.DetailsURI, err = storeBaseURI.Parse("package/") - if err != nil { - panic(err) - } - defaultConfig.BulkURI, err = storeBaseURI.Parse("click-metadata") if err != nil { panic(err) @@ -201,28 +194,12 @@ func NewUbuntuStoreSnapRepository(cfg *SnapUbuntuStoreConfig, storeID string) *S return &SnapUbuntuStoreRepository{ storeID: storeID, searchURI: cfg.SearchURI, - detailsURI: cfg.DetailsURI, bulkURI: cfg.BulkURI, assertionsURI: cfg.AssertionsURI, client: &http.Client{}, } } -// setAuthHeader sets the authorization header. -func setAuthHeader(req *http.Request, token *StoreToken) { - if token != nil { - req.Header.Set("Authorization", oauth.MakePlaintextSignature(&token.Token)) - } -} - -// configureAuthHeader optionally sets the auth header if a token is available. -func configureAuthHeader(req *http.Request) { - ssoToken, err := ReadStoreToken() - if err == nil { - setAuthHeader(req, ssoToken) - } -} - // small helper that sets the correct http headers for the ubuntu store func (s *SnapUbuntuStoreRepository) applyUbuntuStoreHeaders(req *http.Request, accept string) { if accept == "" { @@ -239,12 +216,6 @@ func (s *SnapUbuntuStoreRepository) applyUbuntuStoreHeaders(req *http.Request, a } } -// small helper that sets the correct http headers for a store request including auth -func (s *SnapUbuntuStoreRepository) configureStoreReq(req *http.Request, accept string) { - configureAuthHeader(req) - s.applyUbuntuStoreHeaders(req, accept) -} - // read all the available metadata from the store response and cache func (s *SnapUbuntuStoreRepository) checkStoreResponse(resp *http.Response) { suggestedCurrency := resp.Header.Get("X-Suggested-Currency") @@ -259,18 +230,20 @@ func (s *SnapUbuntuStoreRepository) checkStoreResponse(resp *http.Response) { // Snap returns the snap.Info for the store hosted snap with the given name or an error. func (s *SnapUbuntuStoreRepository) Snap(name, channel string) (*snap.Info, error) { - url, err := s.detailsURI.Parse(path.Join(name, channel)) - if err != nil { - return nil, err - } + u := *s.searchURI // make a copy, so we can mutate it - req, err := http.NewRequest("GET", url.String(), nil) + q := u.Query() + q.Set("q", "package_name:\""+name+"\"") + u.RawQuery = q.Encode() + + req, err := http.NewRequest("GET", u.String(), nil) if err != nil { return nil, err } // set headers - s.configureStoreReq(req, "") + s.applyUbuntuStoreHeaders(req, "") + req.Header.Set("X-Ubuntu-Device-Channel", channel) resp, err := s.client.Do(req) if err != nil { @@ -283,19 +256,28 @@ func (s *SnapUbuntuStoreRepository) Snap(name, channel string) (*snap.Info, erro case resp.StatusCode == 404: return nil, ErrSnapNotFound case resp.StatusCode != 200: - return nil, fmt.Errorf("SnapUbuntuStoreRepository: unexpected HTTP status code %d while looking forsnap %q/%q", resp.StatusCode, name, channel) + return nil, fmt.Errorf("SnapUbuntuStoreRepository: unexpected HTTP status code %d while looking for snap %q/%q", resp.StatusCode, name, channel) } // and decode json - var detailsData snapDetails + var searchData searchResults dec := json.NewDecoder(resp.Body) - if err := dec.Decode(&detailsData); err != nil { + if err := dec.Decode(&searchData); err != nil { return nil, err } + switch len(searchData.Payload.Packages) { + case 0: + return nil, ErrSnapNotFound + case 1: + // whee + default: + logger.Noticef("expected at most one result from this search, got %d. Using first one.", len(searchData.Payload.Packages)) + } + s.checkStoreResponse(resp) - return infoFromRemote(detailsData), nil + return infoFromRemote(searchData.Payload.Packages[0]), nil } // FindSnaps finds (installable) snaps from the store, matching the @@ -319,8 +301,8 @@ func (s *SnapUbuntuStoreRepository) FindSnaps(searchTerm string, channel string) } // set headers - s.configureStoreReq(req, "") - req.Header.Set("X-Ubuntu-Device-Channnel", channel) + s.applyUbuntuStoreHeaders(req, "") + req.Header.Set("X-Ubuntu-Device-Channel", channel) resp, err := s.client.Do(req) if err != nil { @@ -359,7 +341,7 @@ func (s *SnapUbuntuStoreRepository) Updates(installed []string) (snaps []*snap.I // set headers // the updates call is a special snowflake right now // (see LP: #1427155) - s.configureStoreReq(req, "application/json") + s.applyUbuntuStoreHeaders(req, "application/json") resp, err := s.client.Do(req) if err != nil { @@ -401,10 +383,8 @@ func (s *SnapUbuntuStoreRepository) Download(remoteSnap *snap.Info, pbar progres } }() - ssoToken, _ := ReadStoreToken() - url := remoteSnap.AnonDownloadURL - if url == "" || ssoToken != nil { + if url == "" { url = remoteSnap.DownloadURL } @@ -412,7 +392,6 @@ func (s *SnapUbuntuStoreRepository) Download(remoteSnap *snap.Info, pbar progres if err != nil { return "", err } - setAuthHeader(req, ssoToken) s.applyUbuntuStoreHeaders(req, "") if err := download(remoteSnap.Name(), w, req, pbar); err != nil { @@ -467,7 +446,6 @@ func (s *SnapUbuntuStoreRepository) Assertion(assertType *asserts.AssertionType, return nil, err } - configureAuthHeader(req) req.Header.Set("Accept", asserts.MediaType) resp, err := s.client.Do(req) diff --git a/store/snap_remote_repo_test.go b/store/snap_remote_repo_test.go index 7331cd51df..982997fbe7 100644 --- a/store/snap_remote_repo_test.go +++ b/store/snap_remote_repo_test.go @@ -83,33 +83,34 @@ func (t *remoteRepoTestSuite) TestDownloadOK(c *C) { c.Assert(string(content), Equals, "I was downloaded") } -func (t *remoteRepoTestSuite) TestAuthenticatedDownloadDoesNotUseAnonURL(c *C) { - home := os.Getenv("HOME") - os.Setenv("HOME", c.MkDir()) - defer os.Setenv("HOME", home) - mockStoreToken := StoreToken{TokenName: "meep"} - err := WriteStoreToken(mockStoreToken) - c.Assert(err, IsNil) - - download = func(name string, w io.Writer, req *http.Request, pbar progress.Meter) error { - c.Check(req.URL.String(), Equals, "AUTH-URL") - w.Write([]byte("I was downloaded")) - return nil - } - - snap := &snap.Info{} - snap.OfficialName = "foo" - snap.AnonDownloadURL = "anon-url" - snap.DownloadURL = "AUTH-URL" - - path, err := t.store.Download(snap, nil) - c.Assert(err, IsNil) - defer os.Remove(path) - - content, err := ioutil.ReadFile(path) - c.Assert(err, IsNil) - c.Assert(string(content), Equals, "I was downloaded") -} +// TODO: re-enable this test once authenticator support is in place +// func (t *remoteRepoTestSuite) TestAuthenticatedDownloadDoesNotUseAnonURL(c *C) { +// home := os.Getenv("HOME") +// os.Setenv("HOME", c.MkDir()) +// defer os.Setenv("HOME", home) +// mockStoreToken := StoreToken{TokenName: "meep"} +// err := WriteStoreToken(mockStoreToken) +// c.Assert(err, IsNil) +// +// download = func(name string, w io.Writer, req *http.Request, pbar progress.Meter) error { +// c.Check(req.URL.String(), Equals, "AUTH-URL") +// w.Write([]byte("I was downloaded")) +// return nil +// } +// +// snap := &snap.Info{} +// snap.OfficialName = "foo" +// snap.AnonDownloadURL = "anon-url" +// snap.DownloadURL = "AUTH-URL" +// +// path, err := t.store.Download(snap, nil) +// c.Assert(err, IsNil) +// defer os.Remove(path) +// +// content, err := ioutil.ReadFile(path) +// c.Assert(err, IsNil) +// c.Assert(string(content), Equals, "I was downloaded") +// } func (t *remoteRepoTestSuite) TestDownloadFails(c *C) { var tmpfile *os.File @@ -157,23 +158,15 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryHeaders(c *C) { req, err := http.NewRequest("GET", "http://example.com", nil) c.Assert(err, IsNil) - t.store.configureStoreReq(req, "") + t.store.applyUbuntuStoreHeaders(req, "") c.Assert(req.Header.Get("X-Ubuntu-Release"), Equals, release.String()) c.Check(req.Header.Get("Accept"), Equals, "application/hal+json") - t.store.configureStoreReq(req, "application/json") + t.store.applyUbuntuStoreHeaders(req, "application/json") c.Check(req.Header.Get("Accept"), Equals, "application/json") c.Assert(req.Header.Get("Authorization"), Equals, "") - - mockStoreToken := StoreToken{TokenName: "meep"} - err = WriteStoreToken(mockStoreToken) - c.Assert(err, IsNil) - - t.store.configureStoreReq(req, "") - - c.Assert(req.Header.Get("Authorization"), Matches, "OAuth .*") } const ( @@ -182,9 +175,39 @@ const ( ) /* acquired via - curl -s -H "accept: application/hal+json" -H "X-Ubuntu-Release: 15.04-core" https://search.apps.ubuntu.com/api/v1/package/8nzc1x4iim2xj1g2ul64.chipaca | python -m json.tool +curl -s -H "accept: application/hal+json" -H "X-Ubuntu-Release: rolling-core" -H "X-Ubuntu-Device-Channel: edge" 'https://search.apps.ubuntu.com/api/v1/search?q=package_name:"hello-world"&fields=publisher,package_name,origin,description,summary,title,icon_url,prices,content,ratings_average,version,anon_download_url,download_url,download_sha512,last_updated,binary_filesize,support_url,revision' | python -m json.tool */ const MockDetailsJSON = `{ + "_embedded": { + "clickindex:package": [ + { + "_links": { + "self": { + "href": "https://search.apps.ubuntu.com/api/v1/package/iZvp6HUG9XOQv4vuRQL9MlEgKBgFwsc6" + } + }, + "anon_download_url": "https://public.apps.ubuntu.com/anon/download/canonical/hello-world.canonical/hello-world.canonical_5.0_all.snap", + "binary_filesize": 20480, + "channel": "edge", + "content": "application", + "description": "This is a simple hello world example.", + "download_sha512": "4faffe7e2fee66dbcd1cff629b4f6fa7e5e8e904b4a49b0a908a0ea5518b025bf01f0e913617f6088b30c6c6151eff0a83e89c6b12aea420c4dd0e402bf10c81", + "download_url": "https://public.apps.ubuntu.com/download-snap/canonical/hello-world.canonical/hello-world.canonical_5.0_all.snap", + "icon_url": "https://myapps.developer.ubuntu.com/site_media/appmedia/2015/03/hello.svg_NZLfWbh.png", + "last_updated": "2016-03-03T19:52:01.075726Z", + "origin": "canonical", + "package_name": "hello-world", + "prices": {}, + "publisher": "Canonical", + "ratings_average": 0.0, + "revision": 22, + "summary": "Hello world example", + "support_url": "mailto:snappy-devel@lists.ubuntu.com", + "title": "hello-world", + "version": "5.0" + } + ] + }, "_links": { "curies": [ { @@ -193,72 +216,16 @@ const MockDetailsJSON = `{ "templated": true } ], + "first": { + "href": "https://search.apps.ubuntu.com/api/v1/search?q=package_name%3A%22hello-world%22&fields=channel%2Cpublisher%2Cpackage_name%2Corigin%2Cdescription%2Csummary%2Ctitle%2Cicon_url%2Cprices%2Ccontent%2Cratings_average%2Cversion%2Canon_download_url%2Cdownload_url%2Cdownload_sha512%2Clast_updated%2Cbinary_filesize%2Csupport_url%2Crevision&page=1" + }, + "last": { + "href": "https://search.apps.ubuntu.com/api/v1/search?q=package_name%3A%22hello-world%22&fields=channel%2Cpublisher%2Cpackage_name%2Corigin%2Cdescription%2Csummary%2Ctitle%2Cicon_url%2Cprices%2Ccontent%2Cratings_average%2Cversion%2Canon_download_url%2Cdownload_url%2Cdownload_sha512%2Clast_updated%2Cbinary_filesize%2Csupport_url%2Crevision&page=1" + }, "self": { - "href": "https://search.apps.ubuntu.com/api/v1/package/8nzc1x4iim2xj1g2ul64.chipaca" + "href": "https://search.apps.ubuntu.com/api/v1/search?q=package_name%3A%22hello-world%22&fields=channel%2Cpublisher%2Cpackage_name%2Corigin%2Cdescription%2Csummary%2Ctitle%2Cicon_url%2Cprices%2Ccontent%2Cratings_average%2Cversion%2Canon_download_url%2Cdownload_url%2Cdownload_sha512%2Clast_updated%2Cbinary_filesize%2Csupport_url%2Crevision&page=1" } - }, - "alias": null, - "allow_unauthenticated": true, - "anon_download_url": "https://public.apps.ubuntu.com/anon/download/chipaca/8nzc1x4iim2xj1g2ul64.chipaca/8nzc1x4iim2xj1g2ul64.chipaca_42_all.snap", - "architecture": [ - "all" - ], - "binary_filesize": 65375, - "blacklist_country_codes": [ - "AX" - ], - "channel": "edge", - "changelog": "", - "click_framework": [], - "click_version": "0.1", - "company_name": "", - "content": "application", - "date_published": "2015-04-15T18:34:40.060874Z", - "department": [ - "food-drink" - ], - "summary": "hello world example", - "description": "Returns for store credit only.\nThis is a simple hello world example.", - "developer_name": "John Lenton", - "download_sha512": "5364253e4a988f4f5c04380086d542f410455b97d48cc6c69ca2a5877d8aef2a6b2b2f83ec4f688cae61ebc8a6bf2cdbd4dbd8f743f0522fc76540429b79df42", - "download_url": "https://public.apps.ubuntu.com/download/chipaca/8nzc1x4iim2xj1g2ul64.chipaca/8nzc1x4iim2xj1g2ul64.chipaca_42_all.snap", - "framework": [], - "icon_url": "https://myapps.developer.ubuntu.com/site_media/appmedia/2015/04/hello.svg_Dlrd3L4.png", - "icon_urls": { - "256": "https://myapps.developer.ubuntu.com/site_media/appmedia/2015/04/hello.svg_Dlrd3L4.png" - }, - "id": 2333, - "keywords": [], - "last_updated": "2015-04-15T18:30:16Z", - "license": "Proprietary", - "name": "8nzc1x4iim2xj1g2ul64.chipaca", - "origin": "chipaca", - "package_name": "8nzc1x4iim2xj1g2ul64", - "price": 0.0, - "prices": {}, - "publisher": "John Lenton", - "ratings_average": 0.0, - "release": [ - "15.04-core" - ], - "revision": 15, - "screenshot_url": null, - "screenshot_urls": [], - "status": "Published", - "stores": { - "ubuntu": { - "status": "Published" - } - }, - "support_url": "http://lmgtfy.com", - "terms_of_service": "", - "title": "Returns for store credit only.", - "translations": {}, - "version": "42", - "video_embedded_html_urls": [], - "video_urls": [], - "website": "", - "whitelist_country_codes": [] + } } ` @@ -268,40 +235,71 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryDetails(c *C) { storeID := r.Header.Get("X-Ubuntu-Store") c.Check(storeID, Equals, "") - c.Check(r.URL.Path, Equals, fmt.Sprintf("/details/%s.%s/edge", funkyAppName, funkyAppDeveloper)) + c.Check(r.URL.Path, Equals, "/search") + + q := r.URL.Query() + c.Check(q.Get("q"), Equals, "package_name:\"hello-world\"") + c.Check(r.Header.Get("X-Ubuntu-Device-Channel"), Equals, "edge") io.WriteString(w, MockDetailsJSON) })) c.Assert(mockServer, NotNil) defer mockServer.Close() - var err error - detailsURI, err := url.Parse(mockServer.URL + "/details/") + searchURI, err := url.Parse(mockServer.URL + "/search") c.Assert(err, IsNil) cfg := SnapUbuntuStoreConfig{ - DetailsURI: detailsURI, + SearchURI: searchURI, } repo := NewUbuntuStoreSnapRepository(&cfg, "") c.Assert(repo, NotNil) // the actual test - result, err := repo.Snap(funkyAppName+"."+funkyAppDeveloper, "edge") + result, err := repo.Snap("hello-world", "edge") c.Assert(err, IsNil) - c.Check(result.Name(), Equals, funkyAppName) - c.Check(result.Developer, Equals, funkyAppDeveloper) - c.Check(result.Version, Equals, "42") - c.Check(result.Sha512, Equals, "5364253e4a988f4f5c04380086d542f410455b97d48cc6c69ca2a5877d8aef2a6b2b2f83ec4f688cae61ebc8a6bf2cdbd4dbd8f743f0522fc76540429b79df42") - c.Check(result.Size, Equals, int64(65375)) + c.Check(result.Name(), Equals, "hello-world") + c.Check(result.Developer, Equals, "canonical") + c.Check(result.Version, Equals, "5.0") + c.Check(result.Sha512, Equals, "4faffe7e2fee66dbcd1cff629b4f6fa7e5e8e904b4a49b0a908a0ea5518b025bf01f0e913617f6088b30c6c6151eff0a83e89c6b12aea420c4dd0e402bf10c81") + c.Check(result.Size, Equals, int64(20480)) c.Check(result.Channel, Equals, "edge") - c.Check(result.Description(), Equals, "Returns for store credit only.\nThis is a simple hello world example.") - c.Check(result.Summary(), Equals, "hello world example") + c.Check(result.Description(), Equals, "This is a simple hello world example.") + c.Check(result.Summary(), Equals, "Hello world example") } -const MockNoDetailsJSON = `{"errors": ["No such package"], "result": "error"}` +/* +acquired via +curl -s -H "accept: application/hal+json" -H "X-Ubuntu-Release: rolling-core" -H "X-Ubuntu-Device-Channel: edge" 'https://search.apps.ubuntu.com/api/v1/search?q=package_name:""&fields=channel,publisher,package_name,origin,description,summary,title,icon_url,prices,content,ratings_average,version,anon_download_url,download_url,download_sha512,last_updated,binary_filesize,support_url,revision' | python -m json.tool +*/ +const MockNoDetailsJSON = `{ + "_links": { + "curies": [ + { + "href": "https://wiki.ubuntu.com/AppStore/Interfaces/ClickPackageIndex#reltype_{rel}", + "name": "clickindex", + "templated": true + } + ], + "first": { + "href": "https://search.apps.ubuntu.com/api/v1/search?q=package_name%3A%22%22&fields=publisher%2Cpackage_name%2Corigin%2Cdescription%2Csummary%2Ctitle%2Cicon_url%2Cprices%2Ccontent%2Cratings_average%2Cversion%2Canon_download_url%2Cdownload_url%2Cdownload_sha512%2Clast_updated%2Cbinary_filesize%2Csupport_url%2Crevision&page=1" + }, + "last": { + "href": "https://search.apps.ubuntu.com/api/v1/search?q=package_name%3A%22%22&fields=publisher%2Cpackage_name%2Corigin%2Cdescription%2Csummary%2Ctitle%2Cicon_url%2Cprices%2Ccontent%2Cratings_average%2Cversion%2Canon_download_url%2Cdownload_url%2Cdownload_sha512%2Clast_updated%2Cbinary_filesize%2Csupport_url%2Crevision&page=1" + }, + "self": { + "href": "https://search.apps.ubuntu.com/api/v1/search?q=package_name%3A%22%22&fields=publisher%2Cpackage_name%2Corigin%2Cdescription%2Csummary%2Ctitle%2Cicon_url%2Cprices%2Ccontent%2Cratings_average%2Cversion%2Canon_download_url%2Cdownload_url%2Cdownload_sha512%2Clast_updated%2Cbinary_filesize%2Csupport_url%2Crevision&page=1" + } + } +} +` func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryNoDetails(c *C) { mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - c.Assert(r.URL.Path, Equals, "/details/no-such-pkg/edge") + c.Check(r.URL.Path, Equals, "/search") + + q := r.URL.Query() + c.Check(q.Get("q"), Equals, "package_name:\"no-such-pkg\"") + c.Check(r.Header.Get("X-Ubuntu-Device-Channel"), Equals, "edge") w.WriteHeader(404) io.WriteString(w, MockNoDetailsJSON) })) @@ -309,11 +307,10 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryNoDetails(c *C) { c.Assert(mockServer, NotNil) defer mockServer.Close() - var err error - detailsURI, err := url.Parse(mockServer.URL + "/details/") + searchURI, err := url.Parse(mockServer.URL + "/search") c.Assert(err, IsNil) cfg := SnapUbuntuStoreConfig{ - DetailsURI: detailsURI, + SearchURI: searchURI, } repo := NewUbuntuStoreSnapRepository(&cfg, "") c.Assert(repo, NotNil) @@ -505,7 +502,6 @@ func (t *remoteRepoTestSuite) TestMyAppsURLDependsOnEnviron(c *C) { func (t *remoteRepoTestSuite) TestDefaultConfig(c *C) { c.Check(strings.HasPrefix(defaultConfig.SearchURI.String(), "https://search.apps.ubuntu.com/api/v1/search?"), Equals, true) - c.Check(defaultConfig.DetailsURI.String(), Equals, "https://search.apps.ubuntu.com/api/v1/package/") c.Check(strings.HasPrefix(defaultConfig.BulkURI.String(), "https://search.apps.ubuntu.com/api/v1/click-metadata?"), Equals, true) c.Check(defaultConfig.AssertionsURI.String(), Equals, "https://assertions.ubuntu.com/v1/assertions/") } @@ -587,11 +583,10 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositorySuggestedCurrency(c *C) { c.Assert(mockServer, NotNil) defer mockServer.Close() - var err error - detailsURI, err := url.Parse(mockServer.URL + "/details/") + searchURI, err := url.Parse(mockServer.URL + "/search") c.Assert(err, IsNil) cfg := SnapUbuntuStoreConfig{ - DetailsURI: detailsURI, + SearchURI: searchURI, } repo := NewUbuntuStoreSnapRepository(&cfg, "") c.Assert(repo, NotNil) @@ -600,7 +595,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositorySuggestedCurrency(c *C) { c.Check(repo.SuggestedCurrency(), Equals, "USD") // we should soon have a suggested currency - result, err := repo.Snap(funkyAppName+"."+funkyAppDeveloper, "edge") + result, err := repo.Snap(funkyAppName, "edge") c.Assert(err, IsNil) c.Assert(result, NotNil) c.Check(repo.SuggestedCurrency(), Equals, "GBP") @@ -608,7 +603,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositorySuggestedCurrency(c *C) { suggestedCurrency = "EUR" // checking the currency updates - result, err = repo.Snap(funkyAppName+"."+funkyAppDeveloper, "edge") + result, err = repo.Snap(funkyAppName, "edge") c.Assert(err, IsNil) c.Assert(result, NotNil) c.Check(repo.SuggestedCurrency(), Equals, "EUR") |
