github.com/stolowski/snapd@v0.0.0-20210407085831-115137ce5a22/daemon/api_apps_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2014-2020 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package daemon_test
    21  
    22  import (
    23  	"bytes"
    24  	"encoding/json"
    25  	"errors"
    26  	"fmt"
    27  	"io"
    28  	"io/ioutil"
    29  	"math"
    30  	"net/http"
    31  	"net/http/httptest"
    32  	"sort"
    33  	"strconv"
    34  	"strings"
    35  
    36  	"gopkg.in/check.v1"
    37  
    38  	"github.com/snapcore/snapd/client"
    39  	"github.com/snapcore/snapd/daemon"
    40  	"github.com/snapcore/snapd/overlord/hookstate"
    41  	"github.com/snapcore/snapd/overlord/servicestate"
    42  	"github.com/snapcore/snapd/overlord/snapstate"
    43  	"github.com/snapcore/snapd/overlord/state"
    44  	"github.com/snapcore/snapd/snap"
    45  	"github.com/snapcore/snapd/systemd"
    46  	"github.com/snapcore/snapd/testutil"
    47  )
    48  
    49  var _ = check.Suite(&appsSuite{})
    50  
    51  type appsSuite struct {
    52  	apiBaseSuite
    53  
    54  	journalctlRestorer func()
    55  	jctlSvcses         [][]string
    56  	jctlNs             []int
    57  	jctlFollows        []bool
    58  	jctlRCs            []io.ReadCloser
    59  	jctlErrs           []error
    60  
    61  	serviceControlError error
    62  	serviceControlCalls []serviceControlArgs
    63  
    64  	infoA, infoB, infoC, infoD, infoE *snap.Info
    65  }
    66  
    67  func (s *appsSuite) journalctl(svcs []string, n int, follow bool) (rc io.ReadCloser, err error) {
    68  	s.jctlSvcses = append(s.jctlSvcses, svcs)
    69  	s.jctlNs = append(s.jctlNs, n)
    70  	s.jctlFollows = append(s.jctlFollows, follow)
    71  
    72  	if len(s.jctlErrs) > 0 {
    73  		err, s.jctlErrs = s.jctlErrs[0], s.jctlErrs[1:]
    74  	}
    75  	if len(s.jctlRCs) > 0 {
    76  		rc, s.jctlRCs = s.jctlRCs[0], s.jctlRCs[1:]
    77  	}
    78  
    79  	return rc, err
    80  }
    81  
    82  type serviceControlArgs struct {
    83  	action  string
    84  	options string
    85  	names   []string
    86  }
    87  
    88  func (s *appsSuite) fakeServiceControl(st *state.State, appInfos []*snap.AppInfo, inst *servicestate.Instruction, flags *servicestate.Flags, context *hookstate.Context) ([]*state.TaskSet, error) {
    89  	if flags != nil {
    90  		panic("flags are not expected")
    91  	}
    92  
    93  	if s.serviceControlError != nil {
    94  		return nil, s.serviceControlError
    95  	}
    96  
    97  	serviceCommand := serviceControlArgs{action: inst.Action}
    98  	if inst.RestartOptions.Reload {
    99  		serviceCommand.options = "reload"
   100  	}
   101  	// only one flag should ever be set (depending on Action), but appending
   102  	// them below acts as an extra sanity check.
   103  	if inst.StartOptions.Enable {
   104  		serviceCommand.options += "enable"
   105  	}
   106  	if inst.StopOptions.Disable {
   107  		serviceCommand.options += "disable"
   108  	}
   109  	for _, app := range appInfos {
   110  		serviceCommand.names = append(serviceCommand.names, fmt.Sprintf("%s.%s", app.Snap.InstanceName(), app.Name))
   111  	}
   112  	s.serviceControlCalls = append(s.serviceControlCalls, serviceCommand)
   113  
   114  	t := st.NewTask("dummy", "")
   115  	ts := state.NewTaskSet(t)
   116  	return []*state.TaskSet{ts}, nil
   117  }
   118  
   119  func (s *appsSuite) SetUpSuite(c *check.C) {
   120  	s.apiBaseSuite.SetUpSuite(c)
   121  	s.journalctlRestorer = systemd.MockJournalctl(s.journalctl)
   122  }
   123  
   124  func (s *appsSuite) TearDownSuite(c *check.C) {
   125  	s.journalctlRestorer()
   126  	s.apiBaseSuite.TearDownSuite(c)
   127  }
   128  
   129  func (s *appsSuite) SetUpTest(c *check.C) {
   130  	s.apiBaseSuite.SetUpTest(c)
   131  
   132  	s.jctlSvcses = nil
   133  	s.jctlNs = nil
   134  	s.jctlFollows = nil
   135  	s.jctlRCs = nil
   136  	s.jctlErrs = nil
   137  
   138  	d := s.daemon(c)
   139  
   140  	s.serviceControlCalls = nil
   141  	s.serviceControlError = nil
   142  	restoreServicestateCtrl := daemon.MockServicestateControl(s.fakeServiceControl)
   143  	s.AddCleanup(restoreServicestateCtrl)
   144  
   145  	s.infoA = s.mkInstalledInState(c, s.d, "snap-a", "dev", "v1", snap.R(1), true, "apps: {svc1: {daemon: simple}, svc2: {daemon: simple, reload-command: x}}")
   146  	s.infoB = s.mkInstalledInState(c, s.d, "snap-b", "dev", "v1", snap.R(1), false, "apps: {svc3: {daemon: simple}, cmd1: {}}")
   147  	s.infoC = s.mkInstalledInState(c, s.d, "snap-c", "dev", "v1", snap.R(1), true, "")
   148  	s.infoD = s.mkInstalledInState(c, s.d, "snap-d", "dev", "v1", snap.R(1), true, "apps: {cmd2: {}, cmd3: {}}")
   149  	s.infoE = s.mkInstalledInState(c, s.d, "snap-e", "dev", "v1", snap.R(1), true, "apps: {svc4: {daemon: simple, daemon-scope: user}}")
   150  
   151  	d.Overlord().Loop()
   152  	s.AddCleanup(func() { d.Overlord().Stop() })
   153  }
   154  
   155  func (s *appsSuite) TestSplitAppName(c *check.C) {
   156  	type T struct {
   157  		name string
   158  		snap string
   159  		app  string
   160  	}
   161  
   162  	for _, x := range []T{
   163  		{name: "foo.bar", snap: "foo", app: "bar"},
   164  		{name: "foo", snap: "foo", app: ""},
   165  		{name: "foo.bar.baz", snap: "foo", app: "bar.baz"},
   166  		{name: ".", snap: "", app: ""}, // SISO
   167  	} {
   168  		snap, app := daemon.SplitAppName(x.name)
   169  		c.Check(x.snap, check.Equals, snap, check.Commentf(x.name))
   170  		c.Check(x.app, check.Equals, app, check.Commentf(x.name))
   171  	}
   172  }
   173  
   174  func (s *appsSuite) TestGetAppsInfo(c *check.C) {
   175  	// System services from active snaps
   176  	svcNames := []string{"snap-a.svc1", "snap-a.svc2"}
   177  	for _, name := range svcNames {
   178  		s.SysctlBufs = append(s.SysctlBufs, []byte(fmt.Sprintf(`
   179  Id=snap.%s.service
   180  Type=simple
   181  ActiveState=active
   182  UnitFileState=enabled
   183  `[1:], name)))
   184  	}
   185  	// System services from inactive snaps
   186  	svcNames = append(svcNames, "snap-b.svc3")
   187  	// User services from active snaps
   188  	svcNames = append(svcNames, "snap-e.svc4")
   189  	s.SysctlBufs = append(s.SysctlBufs, []byte("enabled\n"))
   190  
   191  	req, err := http.NewRequest("GET", "/v2/apps", nil)
   192  	c.Assert(err, check.IsNil)
   193  
   194  	rsp := s.syncReq(c, req, nil)
   195  	c.Assert(rsp.Status, check.Equals, 200)
   196  	c.Assert(rsp.Result, check.FitsTypeOf, []client.AppInfo{})
   197  	apps := rsp.Result.([]client.AppInfo)
   198  	c.Assert(apps, check.HasLen, 7)
   199  
   200  	for _, name := range svcNames {
   201  		snapName, app := daemon.SplitAppName(name)
   202  		needle := client.AppInfo{
   203  			Snap:        snapName,
   204  			Name:        app,
   205  			Daemon:      "simple",
   206  			DaemonScope: snap.SystemDaemon,
   207  		}
   208  		if snapName != "snap-b" {
   209  			// snap-b is not active (all the others are)
   210  			needle.Active = true
   211  			needle.Enabled = true
   212  		}
   213  		if snapName == "snap-e" {
   214  			// snap-e contains user services
   215  			needle.DaemonScope = snap.UserDaemon
   216  			needle.Active = false
   217  		}
   218  		c.Check(apps, testutil.DeepContains, needle)
   219  	}
   220  
   221  	for _, name := range []string{"snap-b.cmd1", "snap-d.cmd2", "snap-d.cmd3"} {
   222  		snap, app := daemon.SplitAppName(name)
   223  		c.Check(apps, testutil.DeepContains, client.AppInfo{
   224  			Snap: snap,
   225  			Name: app,
   226  		})
   227  	}
   228  
   229  	appNames := make([]string, len(apps))
   230  	for i, app := range apps {
   231  		appNames[i] = app.Snap + "." + app.Name
   232  	}
   233  	c.Check(sort.StringsAreSorted(appNames), check.Equals, true)
   234  }
   235  
   236  func (s *appsSuite) TestGetAppsInfoNames(c *check.C) {
   237  
   238  	req, err := http.NewRequest("GET", "/v2/apps?names=snap-d", nil)
   239  	c.Assert(err, check.IsNil)
   240  
   241  	rsp := s.syncReq(c, req, nil)
   242  	c.Assert(rsp.Status, check.Equals, 200)
   243  	c.Assert(rsp.Result, check.FitsTypeOf, []client.AppInfo{})
   244  	apps := rsp.Result.([]client.AppInfo)
   245  	c.Assert(apps, check.HasLen, 2)
   246  
   247  	for _, name := range []string{"snap-d.cmd2", "snap-d.cmd3"} {
   248  		snap, app := daemon.SplitAppName(name)
   249  		c.Check(apps, testutil.DeepContains, client.AppInfo{
   250  			Snap: snap,
   251  			Name: app,
   252  		})
   253  	}
   254  
   255  	appNames := make([]string, len(apps))
   256  	for i, app := range apps {
   257  		appNames[i] = app.Snap + "." + app.Name
   258  	}
   259  	c.Check(sort.StringsAreSorted(appNames), check.Equals, true)
   260  }
   261  
   262  func (s *appsSuite) TestGetAppsInfoServices(c *check.C) {
   263  	// System services from active snaps
   264  	svcNames := []string{"snap-a.svc1", "snap-a.svc2"}
   265  	for _, name := range svcNames {
   266  		s.SysctlBufs = append(s.SysctlBufs, []byte(fmt.Sprintf(`
   267  Id=snap.%s.service
   268  Type=simple
   269  ActiveState=active
   270  UnitFileState=enabled
   271  `[1:], name)))
   272  	}
   273  	// System services from inactive snaps
   274  	svcNames = append(svcNames, "snap-b.svc3")
   275  	// User services from active snaps
   276  	svcNames = append(svcNames, "snap-e.svc4")
   277  	s.SysctlBufs = append(s.SysctlBufs, []byte("enabled\n"))
   278  
   279  	req, err := http.NewRequest("GET", "/v2/apps?select=service", nil)
   280  	c.Assert(err, check.IsNil)
   281  
   282  	rsp := s.syncReq(c, req, nil)
   283  	c.Assert(rsp.Status, check.Equals, 200)
   284  	c.Assert(rsp.Result, check.FitsTypeOf, []client.AppInfo{})
   285  	svcs := rsp.Result.([]client.AppInfo)
   286  	c.Assert(svcs, check.HasLen, 4)
   287  
   288  	for _, name := range svcNames {
   289  		snapName, app := daemon.SplitAppName(name)
   290  		needle := client.AppInfo{
   291  			Snap:        snapName,
   292  			Name:        app,
   293  			Daemon:      "simple",
   294  			DaemonScope: snap.SystemDaemon,
   295  		}
   296  		if snapName != "snap-b" {
   297  			// snap-b is not active (all the others are)
   298  			needle.Active = true
   299  			needle.Enabled = true
   300  		}
   301  		if snapName == "snap-e" {
   302  			// snap-e contains user services
   303  			needle.DaemonScope = snap.UserDaemon
   304  			needle.Active = false
   305  		}
   306  		c.Check(svcs, testutil.DeepContains, needle)
   307  	}
   308  
   309  	appNames := make([]string, len(svcs))
   310  	for i, svc := range svcs {
   311  		appNames[i] = svc.Snap + "." + svc.Name
   312  	}
   313  	c.Check(sort.StringsAreSorted(appNames), check.Equals, true)
   314  }
   315  
   316  func (s *appsSuite) TestGetAppsInfoBadSelect(c *check.C) {
   317  	req, err := http.NewRequest("GET", "/v2/apps?select=potato", nil)
   318  	c.Assert(err, check.IsNil)
   319  
   320  	rsp := s.errorReq(c, req, nil)
   321  	c.Assert(rsp.Status, check.Equals, 400)
   322  }
   323  
   324  func (s *appsSuite) TestGetAppsInfoBadName(c *check.C) {
   325  	req, err := http.NewRequest("GET", "/v2/apps?names=potato", nil)
   326  	c.Assert(err, check.IsNil)
   327  
   328  	rsp := s.errorReq(c, req, nil)
   329  	c.Assert(rsp.Status, check.Equals, 404)
   330  }
   331  
   332  func (s *appsSuite) TestAppInfosForOne(c *check.C) {
   333  	st := s.d.Overlord().State()
   334  	appInfos, rsp := daemon.AppInfosFor(st, []string{"snap-a.svc1"}, daemon.AppInfoServiceTrue)
   335  	c.Assert(rsp, check.IsNil)
   336  	c.Assert(appInfos, check.HasLen, 1)
   337  	c.Check(appInfos[0].Snap, check.DeepEquals, s.infoA)
   338  	c.Check(appInfos[0].Name, check.Equals, "svc1")
   339  }
   340  
   341  func (s *appsSuite) TestAppInfosForAll(c *check.C) {
   342  	type T struct {
   343  		opts  daemon.AppInfoOptions
   344  		snaps []*snap.Info
   345  		names []string
   346  	}
   347  
   348  	for _, t := range []T{
   349  		{
   350  			opts:  daemon.AppInfoServiceTrue,
   351  			names: []string{"svc1", "svc2", "svc3", "svc4"},
   352  			snaps: []*snap.Info{s.infoA, s.infoA, s.infoB, s.infoE},
   353  		},
   354  		{
   355  			opts:  daemon.AppInfoServiceFalse,
   356  			names: []string{"svc1", "svc2", "cmd1", "svc3", "cmd2", "cmd3", "svc4"},
   357  			snaps: []*snap.Info{s.infoA, s.infoA, s.infoB, s.infoB, s.infoD, s.infoD, s.infoE},
   358  		},
   359  	} {
   360  		c.Assert(len(t.names), check.Equals, len(t.snaps), check.Commentf("%s", t.opts))
   361  
   362  		st := s.d.Overlord().State()
   363  		appInfos, rsp := daemon.AppInfosFor(st, nil, t.opts)
   364  		c.Assert(rsp, check.IsNil, check.Commentf("%s", t.opts))
   365  		names := make([]string, len(appInfos))
   366  		for i, appInfo := range appInfos {
   367  			names[i] = appInfo.Name
   368  		}
   369  		c.Assert(names, check.DeepEquals, t.names, check.Commentf("%s", t.opts))
   370  
   371  		for i := range appInfos {
   372  			c.Check(appInfos[i].Snap, check.DeepEquals, t.snaps[i], check.Commentf("%s: %s", t.opts, t.names[i]))
   373  		}
   374  	}
   375  }
   376  
   377  func (s *appsSuite) TestAppInfosForOneSnap(c *check.C) {
   378  	st := s.d.Overlord().State()
   379  	appInfos, rsp := daemon.AppInfosFor(st, []string{"snap-a"}, daemon.AppInfoServiceTrue)
   380  	c.Assert(rsp, check.IsNil)
   381  	c.Assert(appInfos, check.HasLen, 2)
   382  	sort.Sort(snap.AppInfoBySnapApp(appInfos))
   383  
   384  	c.Check(appInfos[0].Snap, check.DeepEquals, s.infoA)
   385  	c.Check(appInfos[0].Name, check.Equals, "svc1")
   386  	c.Check(appInfos[1].Snap, check.DeepEquals, s.infoA)
   387  	c.Check(appInfos[1].Name, check.Equals, "svc2")
   388  }
   389  
   390  func (s *appsSuite) TestAppInfosForMixedArgs(c *check.C) {
   391  	st := s.d.Overlord().State()
   392  	appInfos, rsp := daemon.AppInfosFor(st, []string{"snap-a", "snap-a.svc1"}, daemon.AppInfoServiceTrue)
   393  	c.Assert(rsp, check.IsNil)
   394  	c.Assert(appInfos, check.HasLen, 2)
   395  	sort.Sort(snap.AppInfoBySnapApp(appInfos))
   396  
   397  	c.Check(appInfos[0].Snap, check.DeepEquals, s.infoA)
   398  	c.Check(appInfos[0].Name, check.Equals, "svc1")
   399  	c.Check(appInfos[1].Snap, check.DeepEquals, s.infoA)
   400  	c.Check(appInfos[1].Name, check.Equals, "svc2")
   401  }
   402  
   403  func (s *appsSuite) TestAppInfosCleanupAndSorted(c *check.C) {
   404  	st := s.d.Overlord().State()
   405  	appInfos, rsp := daemon.AppInfosFor(st, []string{
   406  		"snap-b.svc3",
   407  		"snap-a.svc2",
   408  		"snap-a.svc1",
   409  		"snap-a.svc2",
   410  		"snap-b.svc3",
   411  		"snap-a.svc1",
   412  		"snap-b",
   413  		"snap-a",
   414  	}, daemon.AppInfoServiceTrue)
   415  	c.Assert(rsp, check.IsNil)
   416  	c.Assert(appInfos, check.HasLen, 3)
   417  	sort.Sort(snap.AppInfoBySnapApp(appInfos))
   418  
   419  	c.Check(appInfos[0].Snap, check.DeepEquals, s.infoA)
   420  	c.Check(appInfos[0].Name, check.Equals, "svc1")
   421  	c.Check(appInfos[1].Snap, check.DeepEquals, s.infoA)
   422  	c.Check(appInfos[1].Name, check.Equals, "svc2")
   423  	c.Check(appInfos[2].Snap, check.DeepEquals, s.infoB)
   424  	c.Check(appInfos[2].Name, check.Equals, "svc3")
   425  }
   426  
   427  func (s *appsSuite) TestAppInfosForAppless(c *check.C) {
   428  	st := s.d.Overlord().State()
   429  	appInfos, rsp := daemon.AppInfosFor(st, []string{"snap-c"}, daemon.AppInfoServiceTrue)
   430  	c.Assert(rsp, check.FitsTypeOf, &daemon.Resp{})
   431  	c.Check(rsp.(*daemon.Resp).Status, check.Equals, 404)
   432  	c.Check(rsp.(*daemon.Resp).Result.(*daemon.ErrorResult).Kind, check.Equals, client.ErrorKindAppNotFound)
   433  	c.Assert(appInfos, check.IsNil)
   434  }
   435  
   436  func (s *appsSuite) TestAppInfosForMissingApp(c *check.C) {
   437  	st := s.d.Overlord().State()
   438  	appInfos, rsp := daemon.AppInfosFor(st, []string{"snap-c.whatever"}, daemon.AppInfoServiceTrue)
   439  	c.Assert(rsp, check.FitsTypeOf, &daemon.Resp{})
   440  	c.Check(rsp.(*daemon.Resp).Status, check.Equals, 404)
   441  	c.Check(rsp.(*daemon.Resp).Result.(*daemon.ErrorResult).Kind, check.Equals, client.ErrorKindAppNotFound)
   442  	c.Assert(appInfos, check.IsNil)
   443  }
   444  
   445  func (s *appsSuite) TestAppInfosForMissingSnap(c *check.C) {
   446  	st := s.d.Overlord().State()
   447  	appInfos, rsp := daemon.AppInfosFor(st, []string{"snap-x"}, daemon.AppInfoServiceTrue)
   448  	c.Assert(rsp, check.FitsTypeOf, &daemon.Resp{})
   449  	c.Check(rsp.(*daemon.Resp).Status, check.Equals, 404)
   450  	c.Check(rsp.(*daemon.Resp).Result.(*daemon.ErrorResult).Kind, check.Equals, client.ErrorKindSnapNotFound)
   451  	c.Assert(appInfos, check.IsNil)
   452  }
   453  
   454  func (s *appsSuite) testPostApps(c *check.C, inst servicestate.Instruction, servicecmds []serviceControlArgs) *state.Change {
   455  	postBody, err := json.Marshal(inst)
   456  	c.Assert(err, check.IsNil)
   457  
   458  	req, err := http.NewRequest("POST", "/v2/apps", bytes.NewBuffer(postBody))
   459  	c.Assert(err, check.IsNil)
   460  
   461  	rsp := s.asyncReq(c, req, nil)
   462  	c.Assert(rsp.Status, check.Equals, 202)
   463  	c.Check(rsp.Change, check.Matches, `[0-9]+`)
   464  
   465  	st := s.d.Overlord().State()
   466  	st.Lock()
   467  	defer st.Unlock()
   468  	chg := st.Change(rsp.Change)
   469  	c.Assert(chg, check.NotNil)
   470  	c.Check(chg.Tasks(), check.HasLen, len(servicecmds))
   471  
   472  	st.Unlock()
   473  	<-chg.Ready()
   474  	st.Lock()
   475  
   476  	c.Check(s.serviceControlCalls, check.DeepEquals, servicecmds)
   477  	return chg
   478  }
   479  
   480  func (s *appsSuite) TestPostAppsStartOne(c *check.C) {
   481  	inst := servicestate.Instruction{Action: "start", Names: []string{"snap-a.svc2"}}
   482  	expected := []serviceControlArgs{
   483  		{action: "start", names: []string{"snap-a.svc2"}},
   484  	}
   485  	chg := s.testPostApps(c, inst, expected)
   486  	chg.State().Lock()
   487  	defer chg.State().Unlock()
   488  
   489  	var names []string
   490  	err := chg.Get("snap-names", &names)
   491  	c.Assert(err, check.IsNil)
   492  	c.Assert(names, check.DeepEquals, []string{"snap-a"})
   493  }
   494  
   495  func (s *appsSuite) TestPostAppsStartTwo(c *check.C) {
   496  	inst := servicestate.Instruction{Action: "start", Names: []string{"snap-a"}}
   497  	expected := []serviceControlArgs{
   498  		{action: "start", names: []string{"snap-a.svc1", "snap-a.svc2"}},
   499  	}
   500  	chg := s.testPostApps(c, inst, expected)
   501  
   502  	chg.State().Lock()
   503  	defer chg.State().Unlock()
   504  
   505  	var names []string
   506  	err := chg.Get("snap-names", &names)
   507  	c.Assert(err, check.IsNil)
   508  	c.Assert(names, check.DeepEquals, []string{"snap-a"})
   509  }
   510  
   511  func (s *appsSuite) TestPostAppsStartThree(c *check.C) {
   512  	inst := servicestate.Instruction{Action: "start", Names: []string{"snap-a", "snap-b"}}
   513  	expected := []serviceControlArgs{
   514  		{action: "start", names: []string{"snap-a.svc1", "snap-a.svc2", "snap-b.svc3"}},
   515  	}
   516  	chg := s.testPostApps(c, inst, expected)
   517  	// check the summary expands the snap into actual apps
   518  	c.Check(chg.Summary(), check.Equals, "Running service command")
   519  	chg.State().Lock()
   520  	defer chg.State().Unlock()
   521  
   522  	var names []string
   523  	err := chg.Get("snap-names", &names)
   524  	c.Assert(err, check.IsNil)
   525  	c.Assert(names, check.DeepEquals, []string{"snap-a", "snap-b"})
   526  }
   527  
   528  func (s *appsSuite) TestPostAppsStop(c *check.C) {
   529  	inst := servicestate.Instruction{Action: "stop", Names: []string{"snap-a.svc2"}}
   530  	expected := []serviceControlArgs{
   531  		{action: "stop", names: []string{"snap-a.svc2"}},
   532  	}
   533  	s.testPostApps(c, inst, expected)
   534  }
   535  
   536  func (s *appsSuite) TestPostAppsRestart(c *check.C) {
   537  	inst := servicestate.Instruction{Action: "restart", Names: []string{"snap-a.svc2"}}
   538  	expected := []serviceControlArgs{
   539  		{action: "restart", names: []string{"snap-a.svc2"}},
   540  	}
   541  	s.testPostApps(c, inst, expected)
   542  }
   543  
   544  func (s *appsSuite) TestPostAppsReload(c *check.C) {
   545  	inst := servicestate.Instruction{Action: "restart", Names: []string{"snap-a.svc2"}}
   546  	inst.Reload = true
   547  	expected := []serviceControlArgs{
   548  		{action: "restart", options: "reload", names: []string{"snap-a.svc2"}},
   549  	}
   550  	s.testPostApps(c, inst, expected)
   551  }
   552  
   553  func (s *appsSuite) TestPostAppsEnableNow(c *check.C) {
   554  	inst := servicestate.Instruction{Action: "start", Names: []string{"snap-a.svc2"}}
   555  	inst.Enable = true
   556  	expected := []serviceControlArgs{
   557  		{action: "start", options: "enable", names: []string{"snap-a.svc2"}},
   558  	}
   559  	s.testPostApps(c, inst, expected)
   560  }
   561  
   562  func (s *appsSuite) TestPostAppsDisableNow(c *check.C) {
   563  	inst := servicestate.Instruction{Action: "stop", Names: []string{"snap-a.svc2"}}
   564  	inst.Disable = true
   565  	expected := []serviceControlArgs{
   566  		{action: "stop", options: "disable", names: []string{"snap-a.svc2"}},
   567  	}
   568  	s.testPostApps(c, inst, expected)
   569  }
   570  
   571  func (s *appsSuite) TestPostAppsBadJSON(c *check.C) {
   572  	req, err := http.NewRequest("POST", "/v2/apps", bytes.NewBufferString(`'junk`))
   573  	c.Assert(err, check.IsNil)
   574  	rsp := s.errorReq(c, req, nil)
   575  	c.Check(rsp.Status, check.Equals, 400)
   576  	c.Check(rsp.Result.(*daemon.ErrorResult).Message, check.Matches, ".*cannot decode request body.*")
   577  }
   578  
   579  func (s *appsSuite) TestPostAppsBadOp(c *check.C) {
   580  	req, err := http.NewRequest("POST", "/v2/apps", bytes.NewBufferString(`{"random": "json"}`))
   581  	c.Assert(err, check.IsNil)
   582  	rsp := s.errorReq(c, req, nil)
   583  	c.Check(rsp.Status, check.Equals, 400)
   584  	c.Check(rsp.Result.(*daemon.ErrorResult).Message, check.Matches, ".*cannot perform operation on services without a list of services.*")
   585  }
   586  
   587  func (s *appsSuite) TestPostAppsBadSnap(c *check.C) {
   588  	req, err := http.NewRequest("POST", "/v2/apps", bytes.NewBufferString(`{"action": "stop", "names": ["snap-c"]}`))
   589  	c.Assert(err, check.IsNil)
   590  	rsp := s.errorReq(c, req, nil)
   591  	c.Check(rsp.Status, check.Equals, 404)
   592  	c.Check(rsp.Result.(*daemon.ErrorResult).Message, check.Equals, `snap "snap-c" has no services`)
   593  }
   594  
   595  func (s *appsSuite) TestPostAppsBadApp(c *check.C) {
   596  	req, err := http.NewRequest("POST", "/v2/apps", bytes.NewBufferString(`{"action": "stop", "names": ["snap-a.what"]}`))
   597  	c.Assert(err, check.IsNil)
   598  	rsp := s.errorReq(c, req, nil)
   599  	c.Check(rsp.Status, check.Equals, 404)
   600  	c.Check(rsp.Result.(*daemon.ErrorResult).Message, check.Equals, `snap "snap-a" has no service "what"`)
   601  }
   602  
   603  func (s *appsSuite) TestPostAppsServiceControlError(c *check.C) {
   604  	req, err := http.NewRequest("POST", "/v2/apps", bytes.NewBufferString(`{"action": "start", "names": ["snap-a.svc1"]}`))
   605  	c.Assert(err, check.IsNil)
   606  	s.serviceControlError = fmt.Errorf("total failure")
   607  	rsp := s.errorReq(c, req, nil)
   608  	c.Check(rsp.Status, check.Equals, 400)
   609  	c.Check(rsp.Result.(*daemon.ErrorResult).Message, check.Equals, `total failure`)
   610  }
   611  
   612  func (s *appsSuite) TestPostAppsConflict(c *check.C) {
   613  	req, err := http.NewRequest("POST", "/v2/apps", bytes.NewBufferString(`{"action": "start", "names": ["snap-a.svc1"]}`))
   614  	c.Assert(err, check.IsNil)
   615  	s.serviceControlError = &snapstate.ChangeConflictError{Snap: "snap-a", ChangeKind: "enable"}
   616  	rsp := s.errorReq(c, req, nil)
   617  	c.Check(rsp.Status, check.Equals, 400)
   618  	c.Check(rsp.Result.(*daemon.ErrorResult).Message, check.Equals, `snap "snap-a" has "enable" change in progress`)
   619  }
   620  
   621  func (s *appsSuite) TestLogs(c *check.C) {
   622  	s.jctlRCs = []io.ReadCloser{ioutil.NopCloser(strings.NewReader(`
   623  {"MESSAGE": "hello1", "SYSLOG_IDENTIFIER": "xyzzy", "_PID": "42", "__REALTIME_TIMESTAMP": "42"}
   624  {"MESSAGE": "hello2", "SYSLOG_IDENTIFIER": "xyzzy", "_PID": "42", "__REALTIME_TIMESTAMP": "44"}
   625  {"MESSAGE": "hello3", "SYSLOG_IDENTIFIER": "xyzzy", "_PID": "42", "__REALTIME_TIMESTAMP": "46"}
   626  {"MESSAGE": "hello4", "SYSLOG_IDENTIFIER": "xyzzy", "_PID": "42", "__REALTIME_TIMESTAMP": "48"}
   627  {"MESSAGE": "hello5", "SYSLOG_IDENTIFIER": "xyzzy", "_PID": "42", "__REALTIME_TIMESTAMP": "50"}
   628  	`))}
   629  
   630  	req, err := http.NewRequest("GET", "/v2/logs?names=snap-a.svc2&n=42&follow=false", nil)
   631  	c.Assert(err, check.IsNil)
   632  
   633  	rec := httptest.NewRecorder()
   634  	s.req(c, req, nil).ServeHTTP(rec, req)
   635  
   636  	c.Check(s.jctlSvcses, check.DeepEquals, [][]string{{"snap.snap-a.svc2.service"}})
   637  	c.Check(s.jctlNs, check.DeepEquals, []int{42})
   638  	c.Check(s.jctlFollows, check.DeepEquals, []bool{false})
   639  
   640  	c.Check(rec.Code, check.Equals, 200)
   641  	c.Check(rec.HeaderMap.Get("Content-Type"), check.Equals, "application/json-seq")
   642  	c.Check(rec.Body.String(), check.Equals, `
   643  {"timestamp":"1970-01-01T00:00:00.000042Z","message":"hello1","sid":"xyzzy","pid":"42"}
   644  {"timestamp":"1970-01-01T00:00:00.000044Z","message":"hello2","sid":"xyzzy","pid":"42"}
   645  {"timestamp":"1970-01-01T00:00:00.000046Z","message":"hello3","sid":"xyzzy","pid":"42"}
   646  {"timestamp":"1970-01-01T00:00:00.000048Z","message":"hello4","sid":"xyzzy","pid":"42"}
   647  {"timestamp":"1970-01-01T00:00:00.00005Z","message":"hello5","sid":"xyzzy","pid":"42"}
   648  `[1:])
   649  }
   650  
   651  func (s *appsSuite) TestLogsN(c *check.C) {
   652  	type T struct {
   653  		in  string
   654  		out int
   655  	}
   656  
   657  	for _, t := range []T{
   658  		{in: "", out: 10},
   659  		{in: "0", out: 0},
   660  		{in: "-1", out: -1},
   661  		{in: strconv.Itoa(math.MinInt32), out: math.MinInt32},
   662  		{in: strconv.Itoa(math.MaxInt32), out: math.MaxInt32},
   663  	} {
   664  
   665  		s.jctlRCs = []io.ReadCloser{ioutil.NopCloser(strings.NewReader(""))}
   666  		s.jctlNs = nil
   667  
   668  		req, err := http.NewRequest("GET", "/v2/logs?n="+t.in, nil)
   669  		c.Assert(err, check.IsNil)
   670  
   671  		rec := httptest.NewRecorder()
   672  		s.req(c, req, nil).ServeHTTP(rec, req)
   673  
   674  		c.Check(s.jctlNs, check.DeepEquals, []int{t.out})
   675  	}
   676  }
   677  
   678  func (s *appsSuite) TestLogsBadN(c *check.C) {
   679  	req, err := http.NewRequest("GET", "/v2/logs?n=hello", nil)
   680  	c.Assert(err, check.IsNil)
   681  
   682  	rsp := s.errorReq(c, req, nil)
   683  	c.Assert(rsp.Status, check.Equals, 400)
   684  }
   685  
   686  func (s *appsSuite) TestLogsFollow(c *check.C) {
   687  	s.jctlRCs = []io.ReadCloser{
   688  		ioutil.NopCloser(strings.NewReader("")),
   689  		ioutil.NopCloser(strings.NewReader("")),
   690  		ioutil.NopCloser(strings.NewReader("")),
   691  	}
   692  
   693  	reqT, err := http.NewRequest("GET", "/v2/logs?follow=true", nil)
   694  	c.Assert(err, check.IsNil)
   695  	reqF, err := http.NewRequest("GET", "/v2/logs?follow=false", nil)
   696  	c.Assert(err, check.IsNil)
   697  	reqN, err := http.NewRequest("GET", "/v2/logs", nil)
   698  	c.Assert(err, check.IsNil)
   699  
   700  	rec := httptest.NewRecorder()
   701  	s.req(c, reqT, nil).ServeHTTP(rec, reqT)
   702  	s.req(c, reqF, nil).ServeHTTP(rec, reqF)
   703  	s.req(c, reqN, nil).ServeHTTP(rec, reqN)
   704  
   705  	c.Check(s.jctlFollows, check.DeepEquals, []bool{true, false, false})
   706  }
   707  
   708  func (s *appsSuite) TestLogsBadFollow(c *check.C) {
   709  	req, err := http.NewRequest("GET", "/v2/logs?follow=hello", nil)
   710  	c.Assert(err, check.IsNil)
   711  
   712  	rsp := s.errorReq(c, req, nil)
   713  	c.Assert(rsp.Status, check.Equals, 400)
   714  }
   715  
   716  func (s *appsSuite) TestLogsBadName(c *check.C) {
   717  	req, err := http.NewRequest("GET", "/v2/logs?names=hello", nil)
   718  	c.Assert(err, check.IsNil)
   719  
   720  	rsp := s.errorReq(c, req, nil)
   721  	c.Assert(rsp.Status, check.Equals, 404)
   722  }
   723  
   724  func (s *appsSuite) TestLogsSad(c *check.C) {
   725  	s.jctlErrs = []error{errors.New("potato")}
   726  	req, err := http.NewRequest("GET", "/v2/logs", nil)
   727  	c.Assert(err, check.IsNil)
   728  
   729  	rsp := s.errorReq(c, req, nil)
   730  	c.Assert(rsp.Status, check.Equals, 500)
   731  }
   732  
   733  func (s *appsSuite) TestLogsNoServices(c *check.C) {
   734  	// no installed snaps with services
   735  	st := s.d.Overlord().State()
   736  	st.Lock()
   737  	st.Set("snaps", nil)
   738  	st.Unlock()
   739  
   740  	req, err := http.NewRequest("GET", "/v2/logs", nil)
   741  	c.Assert(err, check.IsNil)
   742  
   743  	rsp := s.errorReq(c, req, nil)
   744  	c.Assert(rsp.Status, check.Equals, 404)
   745  }