github.com/chipaca/snappy@v0.0.0-20210104084008-1f06296fe8ad/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 *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  
   150  	d.Overlord().Loop()
   151  	s.AddCleanup(func() { d.Overlord().Stop() })
   152  }
   153  
   154  func (s *appsSuite) TestSplitAppName(c *check.C) {
   155  	type T struct {
   156  		name string
   157  		snap string
   158  		app  string
   159  	}
   160  
   161  	for _, x := range []T{
   162  		{name: "foo.bar", snap: "foo", app: "bar"},
   163  		{name: "foo", snap: "foo", app: ""},
   164  		{name: "foo.bar.baz", snap: "foo", app: "bar.baz"},
   165  		{name: ".", snap: "", app: ""}, // SISO
   166  	} {
   167  		snap, app := daemon.SplitAppName(x.name)
   168  		c.Check(x.snap, check.Equals, snap, check.Commentf(x.name))
   169  		c.Check(x.app, check.Equals, app, check.Commentf(x.name))
   170  	}
   171  }
   172  
   173  func (s *appsSuite) TestGetAppsInfo(c *check.C) {
   174  	svcNames := []string{"snap-a.svc1", "snap-a.svc2", "snap-b.svc3"}
   175  	for _, name := range svcNames {
   176  		s.SysctlBufs = append(s.SysctlBufs, []byte(fmt.Sprintf(`
   177  Id=snap.%s.service
   178  Type=simple
   179  ActiveState=active
   180  UnitFileState=enabled
   181  `[1:], name)))
   182  	}
   183  
   184  	req, err := http.NewRequest("GET", "/v2/apps", nil)
   185  	c.Assert(err, check.IsNil)
   186  
   187  	rsp := s.req(c, req, nil).(*daemon.Resp)
   188  	c.Assert(rsp.Status, check.Equals, 200)
   189  	c.Assert(rsp.Type, check.Equals, daemon.ResponseTypeSync)
   190  	c.Assert(rsp.Result, check.FitsTypeOf, []client.AppInfo{})
   191  	apps := rsp.Result.([]client.AppInfo)
   192  	c.Assert(apps, check.HasLen, 6)
   193  
   194  	for _, name := range svcNames {
   195  		snap, app := daemon.SplitAppName(name)
   196  		needle := client.AppInfo{
   197  			Snap:   snap,
   198  			Name:   app,
   199  			Daemon: "simple",
   200  		}
   201  		if snap != "snap-b" {
   202  			// snap-b is not active (all the others are)
   203  			needle.Active = true
   204  			needle.Enabled = true
   205  		}
   206  		c.Check(apps, testutil.DeepContains, needle)
   207  	}
   208  
   209  	for _, name := range []string{"snap-b.cmd1", "snap-d.cmd2", "snap-d.cmd3"} {
   210  		snap, app := daemon.SplitAppName(name)
   211  		c.Check(apps, testutil.DeepContains, client.AppInfo{
   212  			Snap: snap,
   213  			Name: app,
   214  		})
   215  	}
   216  
   217  	appNames := make([]string, len(apps))
   218  	for i, app := range apps {
   219  		appNames[i] = app.Snap + "." + app.Name
   220  	}
   221  	c.Check(sort.StringsAreSorted(appNames), check.Equals, true)
   222  }
   223  
   224  func (s *appsSuite) TestGetAppsInfoNames(c *check.C) {
   225  
   226  	req, err := http.NewRequest("GET", "/v2/apps?names=snap-d", nil)
   227  	c.Assert(err, check.IsNil)
   228  
   229  	rsp := s.req(c, req, nil).(*daemon.Resp)
   230  	c.Assert(rsp.Status, check.Equals, 200)
   231  	c.Assert(rsp.Type, check.Equals, daemon.ResponseTypeSync)
   232  	c.Assert(rsp.Result, check.FitsTypeOf, []client.AppInfo{})
   233  	apps := rsp.Result.([]client.AppInfo)
   234  	c.Assert(apps, check.HasLen, 2)
   235  
   236  	for _, name := range []string{"snap-d.cmd2", "snap-d.cmd3"} {
   237  		snap, app := daemon.SplitAppName(name)
   238  		c.Check(apps, testutil.DeepContains, client.AppInfo{
   239  			Snap: snap,
   240  			Name: app,
   241  		})
   242  	}
   243  
   244  	appNames := make([]string, len(apps))
   245  	for i, app := range apps {
   246  		appNames[i] = app.Snap + "." + app.Name
   247  	}
   248  	c.Check(sort.StringsAreSorted(appNames), check.Equals, true)
   249  }
   250  
   251  func (s *appsSuite) TestGetAppsInfoServices(c *check.C) {
   252  	svcNames := []string{"snap-a.svc1", "snap-a.svc2", "snap-b.svc3"}
   253  	for _, name := range svcNames {
   254  		s.SysctlBufs = append(s.SysctlBufs, []byte(fmt.Sprintf(`
   255  Id=snap.%s.service
   256  Type=simple
   257  ActiveState=active
   258  UnitFileState=enabled
   259  `[1:], name)))
   260  	}
   261  
   262  	req, err := http.NewRequest("GET", "/v2/apps?select=service", nil)
   263  	c.Assert(err, check.IsNil)
   264  
   265  	rsp := s.req(c, req, nil).(*daemon.Resp)
   266  	c.Assert(rsp.Status, check.Equals, 200)
   267  	c.Assert(rsp.Type, check.Equals, daemon.ResponseTypeSync)
   268  	c.Assert(rsp.Result, check.FitsTypeOf, []client.AppInfo{})
   269  	svcs := rsp.Result.([]client.AppInfo)
   270  	c.Assert(svcs, check.HasLen, 3)
   271  
   272  	for _, name := range svcNames {
   273  		snap, app := daemon.SplitAppName(name)
   274  		needle := client.AppInfo{
   275  			Snap:   snap,
   276  			Name:   app,
   277  			Daemon: "simple",
   278  		}
   279  		if snap != "snap-b" {
   280  			// snap-b is not active (all the others are)
   281  			needle.Active = true
   282  			needle.Enabled = true
   283  		}
   284  		c.Check(svcs, testutil.DeepContains, needle)
   285  	}
   286  
   287  	appNames := make([]string, len(svcs))
   288  	for i, svc := range svcs {
   289  		appNames[i] = svc.Snap + "." + svc.Name
   290  	}
   291  	c.Check(sort.StringsAreSorted(appNames), check.Equals, true)
   292  }
   293  
   294  func (s *appsSuite) TestGetAppsInfoBadSelect(c *check.C) {
   295  	req, err := http.NewRequest("GET", "/v2/apps?select=potato", nil)
   296  	c.Assert(err, check.IsNil)
   297  
   298  	rsp := s.req(c, req, nil).(*daemon.Resp)
   299  	c.Assert(rsp.Status, check.Equals, 400)
   300  	c.Assert(rsp.Type, check.Equals, daemon.ResponseTypeError)
   301  }
   302  
   303  func (s *appsSuite) TestGetAppsInfoBadName(c *check.C) {
   304  	req, err := http.NewRequest("GET", "/v2/apps?names=potato", nil)
   305  	c.Assert(err, check.IsNil)
   306  
   307  	rsp := s.req(c, req, nil).(*daemon.Resp)
   308  	c.Assert(rsp.Status, check.Equals, 404)
   309  	c.Assert(rsp.Type, check.Equals, daemon.ResponseTypeError)
   310  }
   311  
   312  func (s *appsSuite) TestAppInfosForOne(c *check.C) {
   313  	st := s.d.Overlord().State()
   314  	appInfos, rsp := daemon.AppInfosFor(st, []string{"snap-a.svc1"}, daemon.AppInfoServiceTrue)
   315  	c.Assert(rsp, check.IsNil)
   316  	c.Assert(appInfos, check.HasLen, 1)
   317  	c.Check(appInfos[0].Snap, check.DeepEquals, s.infoA)
   318  	c.Check(appInfos[0].Name, check.Equals, "svc1")
   319  }
   320  
   321  func (s *appsSuite) TestAppInfosForAll(c *check.C) {
   322  	type T struct {
   323  		opts  daemon.AppInfoOptions
   324  		snaps []*snap.Info
   325  		names []string
   326  	}
   327  
   328  	for _, t := range []T{
   329  		{
   330  			opts:  daemon.AppInfoServiceTrue,
   331  			names: []string{"svc1", "svc2", "svc3"},
   332  			snaps: []*snap.Info{s.infoA, s.infoA, s.infoB},
   333  		},
   334  		{
   335  			opts:  daemon.AppInfoServiceFalse,
   336  			names: []string{"svc1", "svc2", "cmd1", "svc3", "cmd2", "cmd3"},
   337  			snaps: []*snap.Info{s.infoA, s.infoA, s.infoB, s.infoB, s.infoD, s.infoD},
   338  		},
   339  	} {
   340  		c.Assert(len(t.names), check.Equals, len(t.snaps), check.Commentf("%s", t.opts))
   341  
   342  		st := s.d.Overlord().State()
   343  		appInfos, rsp := daemon.AppInfosFor(st, nil, t.opts)
   344  		c.Assert(rsp, check.IsNil, check.Commentf("%s", t.opts))
   345  		names := make([]string, len(appInfos))
   346  		for i, appInfo := range appInfos {
   347  			names[i] = appInfo.Name
   348  		}
   349  		c.Assert(names, check.DeepEquals, t.names, check.Commentf("%s", t.opts))
   350  
   351  		for i := range appInfos {
   352  			c.Check(appInfos[i].Snap, check.DeepEquals, t.snaps[i], check.Commentf("%s: %s", t.opts, t.names[i]))
   353  		}
   354  	}
   355  }
   356  
   357  func (s *appsSuite) TestAppInfosForOneSnap(c *check.C) {
   358  	st := s.d.Overlord().State()
   359  	appInfos, rsp := daemon.AppInfosFor(st, []string{"snap-a"}, daemon.AppInfoServiceTrue)
   360  	c.Assert(rsp, check.IsNil)
   361  	c.Assert(appInfos, check.HasLen, 2)
   362  	sort.Sort(snap.AppInfoBySnapApp(appInfos))
   363  
   364  	c.Check(appInfos[0].Snap, check.DeepEquals, s.infoA)
   365  	c.Check(appInfos[0].Name, check.Equals, "svc1")
   366  	c.Check(appInfos[1].Snap, check.DeepEquals, s.infoA)
   367  	c.Check(appInfos[1].Name, check.Equals, "svc2")
   368  }
   369  
   370  func (s *appsSuite) TestAppInfosForMixedArgs(c *check.C) {
   371  	st := s.d.Overlord().State()
   372  	appInfos, rsp := daemon.AppInfosFor(st, []string{"snap-a", "snap-a.svc1"}, daemon.AppInfoServiceTrue)
   373  	c.Assert(rsp, check.IsNil)
   374  	c.Assert(appInfos, check.HasLen, 2)
   375  	sort.Sort(snap.AppInfoBySnapApp(appInfos))
   376  
   377  	c.Check(appInfos[0].Snap, check.DeepEquals, s.infoA)
   378  	c.Check(appInfos[0].Name, check.Equals, "svc1")
   379  	c.Check(appInfos[1].Snap, check.DeepEquals, s.infoA)
   380  	c.Check(appInfos[1].Name, check.Equals, "svc2")
   381  }
   382  
   383  func (s *appsSuite) TestAppInfosCleanupAndSorted(c *check.C) {
   384  	st := s.d.Overlord().State()
   385  	appInfos, rsp := daemon.AppInfosFor(st, []string{
   386  		"snap-b.svc3",
   387  		"snap-a.svc2",
   388  		"snap-a.svc1",
   389  		"snap-a.svc2",
   390  		"snap-b.svc3",
   391  		"snap-a.svc1",
   392  		"snap-b",
   393  		"snap-a",
   394  	}, daemon.AppInfoServiceTrue)
   395  	c.Assert(rsp, check.IsNil)
   396  	c.Assert(appInfos, check.HasLen, 3)
   397  	sort.Sort(snap.AppInfoBySnapApp(appInfos))
   398  
   399  	c.Check(appInfos[0].Snap, check.DeepEquals, s.infoA)
   400  	c.Check(appInfos[0].Name, check.Equals, "svc1")
   401  	c.Check(appInfos[1].Snap, check.DeepEquals, s.infoA)
   402  	c.Check(appInfos[1].Name, check.Equals, "svc2")
   403  	c.Check(appInfos[2].Snap, check.DeepEquals, s.infoB)
   404  	c.Check(appInfos[2].Name, check.Equals, "svc3")
   405  }
   406  
   407  func (s *appsSuite) TestAppInfosForAppless(c *check.C) {
   408  	st := s.d.Overlord().State()
   409  	appInfos, rsp := daemon.AppInfosFor(st, []string{"snap-c"}, daemon.AppInfoServiceTrue)
   410  	c.Assert(rsp, check.FitsTypeOf, &daemon.Resp{})
   411  	c.Check(rsp.(*daemon.Resp).Status, check.Equals, 404)
   412  	c.Check(rsp.(*daemon.Resp).Result.(*daemon.ErrorResult).Kind, check.Equals, client.ErrorKindAppNotFound)
   413  	c.Assert(appInfos, check.IsNil)
   414  }
   415  
   416  func (s *appsSuite) TestAppInfosForMissingApp(c *check.C) {
   417  	st := s.d.Overlord().State()
   418  	appInfos, rsp := daemon.AppInfosFor(st, []string{"snap-c.whatever"}, daemon.AppInfoServiceTrue)
   419  	c.Assert(rsp, check.FitsTypeOf, &daemon.Resp{})
   420  	c.Check(rsp.(*daemon.Resp).Status, check.Equals, 404)
   421  	c.Check(rsp.(*daemon.Resp).Result.(*daemon.ErrorResult).Kind, check.Equals, client.ErrorKindAppNotFound)
   422  	c.Assert(appInfos, check.IsNil)
   423  }
   424  
   425  func (s *appsSuite) TestAppInfosForMissingSnap(c *check.C) {
   426  	st := s.d.Overlord().State()
   427  	appInfos, rsp := daemon.AppInfosFor(st, []string{"snap-x"}, daemon.AppInfoServiceTrue)
   428  	c.Assert(rsp, check.FitsTypeOf, &daemon.Resp{})
   429  	c.Check(rsp.(*daemon.Resp).Status, check.Equals, 404)
   430  	c.Check(rsp.(*daemon.Resp).Result.(*daemon.ErrorResult).Kind, check.Equals, client.ErrorKindSnapNotFound)
   431  	c.Assert(appInfos, check.IsNil)
   432  }
   433  
   434  func (s *appsSuite) testPostApps(c *check.C, inst servicestate.Instruction, servicecmds []serviceControlArgs) *state.Change {
   435  	postBody, err := json.Marshal(inst)
   436  	c.Assert(err, check.IsNil)
   437  
   438  	req, err := http.NewRequest("POST", "/v2/apps", bytes.NewBuffer(postBody))
   439  	c.Assert(err, check.IsNil)
   440  
   441  	rsp := s.req(c, req, nil).(*daemon.Resp)
   442  	c.Assert(rsp.Status, check.Equals, 202)
   443  	c.Assert(rsp.Type, check.Equals, daemon.ResponseTypeAsync)
   444  	c.Check(rsp.Change, check.Matches, `[0-9]+`)
   445  
   446  	st := s.d.Overlord().State()
   447  	st.Lock()
   448  	defer st.Unlock()
   449  	chg := st.Change(rsp.Change)
   450  	c.Assert(chg, check.NotNil)
   451  	c.Check(chg.Tasks(), check.HasLen, len(servicecmds))
   452  
   453  	st.Unlock()
   454  	<-chg.Ready()
   455  	st.Lock()
   456  
   457  	c.Check(s.serviceControlCalls, check.DeepEquals, servicecmds)
   458  	return chg
   459  }
   460  
   461  func (s *appsSuite) TestPostAppsStartOne(c *check.C) {
   462  	inst := servicestate.Instruction{Action: "start", Names: []string{"snap-a.svc2"}}
   463  	expected := []serviceControlArgs{
   464  		{action: "start", names: []string{"snap-a.svc2"}},
   465  	}
   466  	chg := s.testPostApps(c, inst, expected)
   467  	chg.State().Lock()
   468  	defer chg.State().Unlock()
   469  
   470  	var names []string
   471  	err := chg.Get("snap-names", &names)
   472  	c.Assert(err, check.IsNil)
   473  	c.Assert(names, check.DeepEquals, []string{"snap-a"})
   474  }
   475  
   476  func (s *appsSuite) TestPostAppsStartTwo(c *check.C) {
   477  	inst := servicestate.Instruction{Action: "start", Names: []string{"snap-a"}}
   478  	expected := []serviceControlArgs{
   479  		{action: "start", names: []string{"snap-a.svc1", "snap-a.svc2"}},
   480  	}
   481  	chg := s.testPostApps(c, inst, expected)
   482  
   483  	chg.State().Lock()
   484  	defer chg.State().Unlock()
   485  
   486  	var names []string
   487  	err := chg.Get("snap-names", &names)
   488  	c.Assert(err, check.IsNil)
   489  	c.Assert(names, check.DeepEquals, []string{"snap-a"})
   490  }
   491  
   492  func (s *appsSuite) TestPostAppsStartThree(c *check.C) {
   493  	inst := servicestate.Instruction{Action: "start", Names: []string{"snap-a", "snap-b"}}
   494  	expected := []serviceControlArgs{
   495  		{action: "start", names: []string{"snap-a.svc1", "snap-a.svc2", "snap-b.svc3"}},
   496  	}
   497  	chg := s.testPostApps(c, inst, expected)
   498  	// check the summary expands the snap into actual apps
   499  	c.Check(chg.Summary(), check.Equals, "Running service command")
   500  	chg.State().Lock()
   501  	defer chg.State().Unlock()
   502  
   503  	var names []string
   504  	err := chg.Get("snap-names", &names)
   505  	c.Assert(err, check.IsNil)
   506  	c.Assert(names, check.DeepEquals, []string{"snap-a", "snap-b"})
   507  }
   508  
   509  func (s *appsSuite) TestPostAppsStop(c *check.C) {
   510  	inst := servicestate.Instruction{Action: "stop", Names: []string{"snap-a.svc2"}}
   511  	expected := []serviceControlArgs{
   512  		{action: "stop", names: []string{"snap-a.svc2"}},
   513  	}
   514  	s.testPostApps(c, inst, expected)
   515  }
   516  
   517  func (s *appsSuite) TestPostAppsRestart(c *check.C) {
   518  	inst := servicestate.Instruction{Action: "restart", Names: []string{"snap-a.svc2"}}
   519  	expected := []serviceControlArgs{
   520  		{action: "restart", names: []string{"snap-a.svc2"}},
   521  	}
   522  	s.testPostApps(c, inst, expected)
   523  }
   524  
   525  func (s *appsSuite) TestPostAppsReload(c *check.C) {
   526  	inst := servicestate.Instruction{Action: "restart", Names: []string{"snap-a.svc2"}}
   527  	inst.Reload = true
   528  	expected := []serviceControlArgs{
   529  		{action: "restart", options: "reload", names: []string{"snap-a.svc2"}},
   530  	}
   531  	s.testPostApps(c, inst, expected)
   532  }
   533  
   534  func (s *appsSuite) TestPostAppsEnableNow(c *check.C) {
   535  	inst := servicestate.Instruction{Action: "start", Names: []string{"snap-a.svc2"}}
   536  	inst.Enable = true
   537  	expected := []serviceControlArgs{
   538  		{action: "start", options: "enable", names: []string{"snap-a.svc2"}},
   539  	}
   540  	s.testPostApps(c, inst, expected)
   541  }
   542  
   543  func (s *appsSuite) TestPostAppsDisableNow(c *check.C) {
   544  	inst := servicestate.Instruction{Action: "stop", Names: []string{"snap-a.svc2"}}
   545  	inst.Disable = true
   546  	expected := []serviceControlArgs{
   547  		{action: "stop", options: "disable", names: []string{"snap-a.svc2"}},
   548  	}
   549  	s.testPostApps(c, inst, expected)
   550  }
   551  
   552  func (s *appsSuite) TestPostAppsBadJSON(c *check.C) {
   553  	req, err := http.NewRequest("POST", "/v2/apps", bytes.NewBufferString(`'junk`))
   554  	c.Assert(err, check.IsNil)
   555  	rsp := s.req(c, req, nil).(*daemon.Resp)
   556  	c.Check(rsp.Status, check.Equals, 400)
   557  	c.Check(rsp.Type, check.Equals, daemon.ResponseTypeError)
   558  	c.Check(rsp.Result.(*daemon.ErrorResult).Message, check.Matches, ".*cannot decode request body.*")
   559  }
   560  
   561  func (s *appsSuite) TestPostAppsBadOp(c *check.C) {
   562  	req, err := http.NewRequest("POST", "/v2/apps", bytes.NewBufferString(`{"random": "json"}`))
   563  	c.Assert(err, check.IsNil)
   564  	rsp := s.req(c, req, nil).(*daemon.Resp)
   565  	c.Check(rsp.Status, check.Equals, 400)
   566  	c.Check(rsp.Type, check.Equals, daemon.ResponseTypeError)
   567  	c.Check(rsp.Result.(*daemon.ErrorResult).Message, check.Matches, ".*cannot perform operation on services without a list of services.*")
   568  }
   569  
   570  func (s *appsSuite) TestPostAppsBadSnap(c *check.C) {
   571  	req, err := http.NewRequest("POST", "/v2/apps", bytes.NewBufferString(`{"action": "stop", "names": ["snap-c"]}`))
   572  	c.Assert(err, check.IsNil)
   573  	rsp := s.req(c, req, nil).(*daemon.Resp)
   574  	c.Check(rsp.Status, check.Equals, 404)
   575  	c.Check(rsp.Type, check.Equals, daemon.ResponseTypeError)
   576  	c.Check(rsp.Result.(*daemon.ErrorResult).Message, check.Equals, `snap "snap-c" has no services`)
   577  }
   578  
   579  func (s *appsSuite) TestPostAppsBadApp(c *check.C) {
   580  	req, err := http.NewRequest("POST", "/v2/apps", bytes.NewBufferString(`{"action": "stop", "names": ["snap-a.what"]}`))
   581  	c.Assert(err, check.IsNil)
   582  	rsp := s.req(c, req, nil).(*daemon.Resp)
   583  	c.Check(rsp.Status, check.Equals, 404)
   584  	c.Check(rsp.Type, check.Equals, daemon.ResponseTypeError)
   585  	c.Check(rsp.Result.(*daemon.ErrorResult).Message, check.Equals, `snap "snap-a" has no service "what"`)
   586  }
   587  
   588  func (s *appsSuite) TestPostAppsServiceControlError(c *check.C) {
   589  	req, err := http.NewRequest("POST", "/v2/apps", bytes.NewBufferString(`{"action": "start", "names": ["snap-a.svc1"]}`))
   590  	c.Assert(err, check.IsNil)
   591  	s.serviceControlError = fmt.Errorf("total failure")
   592  	rsp := s.req(c, req, nil).(*daemon.Resp)
   593  	c.Check(rsp.Status, check.Equals, 400)
   594  	c.Check(rsp.Type, check.Equals, daemon.ResponseTypeError)
   595  	c.Check(rsp.Result.(*daemon.ErrorResult).Message, check.Equals, `total failure`)
   596  }
   597  
   598  func (s *appsSuite) TestPostAppsConflict(c *check.C) {
   599  	req, err := http.NewRequest("POST", "/v2/apps", bytes.NewBufferString(`{"action": "start", "names": ["snap-a.svc1"]}`))
   600  	c.Assert(err, check.IsNil)
   601  	s.serviceControlError = &snapstate.ChangeConflictError{Snap: "snap-a", ChangeKind: "enable"}
   602  	rsp := s.req(c, req, nil).(*daemon.Resp)
   603  	c.Check(rsp.Status, check.Equals, 400)
   604  	c.Check(rsp.Type, check.Equals, daemon.ResponseTypeError)
   605  	c.Check(rsp.Result.(*daemon.ErrorResult).Message, check.Equals, `snap "snap-a" has "enable" change in progress`)
   606  }
   607  
   608  func (s *appsSuite) TestLogs(c *check.C) {
   609  	s.jctlRCs = []io.ReadCloser{ioutil.NopCloser(strings.NewReader(`
   610  {"MESSAGE": "hello1", "SYSLOG_IDENTIFIER": "xyzzy", "_PID": "42", "__REALTIME_TIMESTAMP": "42"}
   611  {"MESSAGE": "hello2", "SYSLOG_IDENTIFIER": "xyzzy", "_PID": "42", "__REALTIME_TIMESTAMP": "44"}
   612  {"MESSAGE": "hello3", "SYSLOG_IDENTIFIER": "xyzzy", "_PID": "42", "__REALTIME_TIMESTAMP": "46"}
   613  {"MESSAGE": "hello4", "SYSLOG_IDENTIFIER": "xyzzy", "_PID": "42", "__REALTIME_TIMESTAMP": "48"}
   614  {"MESSAGE": "hello5", "SYSLOG_IDENTIFIER": "xyzzy", "_PID": "42", "__REALTIME_TIMESTAMP": "50"}
   615  	`))}
   616  
   617  	req, err := http.NewRequest("GET", "/v2/logs?names=snap-a.svc2&n=42&follow=false", nil)
   618  	c.Assert(err, check.IsNil)
   619  
   620  	rec := httptest.NewRecorder()
   621  	s.req(c, req, nil).ServeHTTP(rec, req)
   622  
   623  	c.Check(s.jctlSvcses, check.DeepEquals, [][]string{{"snap.snap-a.svc2.service"}})
   624  	c.Check(s.jctlNs, check.DeepEquals, []int{42})
   625  	c.Check(s.jctlFollows, check.DeepEquals, []bool{false})
   626  
   627  	c.Check(rec.Code, check.Equals, 200)
   628  	c.Check(rec.HeaderMap.Get("Content-Type"), check.Equals, "application/json-seq")
   629  	c.Check(rec.Body.String(), check.Equals, `
   630  {"timestamp":"1970-01-01T00:00:00.000042Z","message":"hello1","sid":"xyzzy","pid":"42"}
   631  {"timestamp":"1970-01-01T00:00:00.000044Z","message":"hello2","sid":"xyzzy","pid":"42"}
   632  {"timestamp":"1970-01-01T00:00:00.000046Z","message":"hello3","sid":"xyzzy","pid":"42"}
   633  {"timestamp":"1970-01-01T00:00:00.000048Z","message":"hello4","sid":"xyzzy","pid":"42"}
   634  {"timestamp":"1970-01-01T00:00:00.00005Z","message":"hello5","sid":"xyzzy","pid":"42"}
   635  `[1:])
   636  }
   637  
   638  func (s *appsSuite) TestLogsN(c *check.C) {
   639  	type T struct {
   640  		in  string
   641  		out int
   642  	}
   643  
   644  	for _, t := range []T{
   645  		{in: "", out: 10},
   646  		{in: "0", out: 0},
   647  		{in: "-1", out: -1},
   648  		{in: strconv.Itoa(math.MinInt32), out: math.MinInt32},
   649  		{in: strconv.Itoa(math.MaxInt32), out: math.MaxInt32},
   650  	} {
   651  
   652  		s.jctlRCs = []io.ReadCloser{ioutil.NopCloser(strings.NewReader(""))}
   653  		s.jctlNs = nil
   654  
   655  		req, err := http.NewRequest("GET", "/v2/logs?n="+t.in, nil)
   656  		c.Assert(err, check.IsNil)
   657  
   658  		rec := httptest.NewRecorder()
   659  		s.req(c, req, nil).ServeHTTP(rec, req)
   660  
   661  		c.Check(s.jctlNs, check.DeepEquals, []int{t.out})
   662  	}
   663  }
   664  
   665  func (s *appsSuite) TestLogsBadN(c *check.C) {
   666  	req, err := http.NewRequest("GET", "/v2/logs?n=hello", nil)
   667  	c.Assert(err, check.IsNil)
   668  
   669  	rsp := s.req(c, req, nil).(*daemon.Resp)
   670  	c.Assert(rsp.Status, check.Equals, 400)
   671  	c.Assert(rsp.Type, check.Equals, daemon.ResponseTypeError)
   672  }
   673  
   674  func (s *appsSuite) TestLogsFollow(c *check.C) {
   675  	s.jctlRCs = []io.ReadCloser{
   676  		ioutil.NopCloser(strings.NewReader("")),
   677  		ioutil.NopCloser(strings.NewReader("")),
   678  		ioutil.NopCloser(strings.NewReader("")),
   679  	}
   680  
   681  	reqT, err := http.NewRequest("GET", "/v2/logs?follow=true", nil)
   682  	c.Assert(err, check.IsNil)
   683  	reqF, err := http.NewRequest("GET", "/v2/logs?follow=false", nil)
   684  	c.Assert(err, check.IsNil)
   685  	reqN, err := http.NewRequest("GET", "/v2/logs", nil)
   686  	c.Assert(err, check.IsNil)
   687  
   688  	rec := httptest.NewRecorder()
   689  	s.req(c, reqT, nil).ServeHTTP(rec, reqT)
   690  	s.req(c, reqF, nil).ServeHTTP(rec, reqF)
   691  	s.req(c, reqN, nil).ServeHTTP(rec, reqN)
   692  
   693  	c.Check(s.jctlFollows, check.DeepEquals, []bool{true, false, false})
   694  }
   695  
   696  func (s *appsSuite) TestLogsBadFollow(c *check.C) {
   697  	req, err := http.NewRequest("GET", "/v2/logs?follow=hello", nil)
   698  	c.Assert(err, check.IsNil)
   699  
   700  	rsp := s.req(c, req, nil).(*daemon.Resp)
   701  	c.Assert(rsp.Status, check.Equals, 400)
   702  	c.Assert(rsp.Type, check.Equals, daemon.ResponseTypeError)
   703  }
   704  
   705  func (s *appsSuite) TestLogsBadName(c *check.C) {
   706  	req, err := http.NewRequest("GET", "/v2/logs?names=hello", nil)
   707  	c.Assert(err, check.IsNil)
   708  
   709  	rsp := s.req(c, req, nil).(*daemon.Resp)
   710  	c.Assert(rsp.Status, check.Equals, 404)
   711  	c.Assert(rsp.Type, check.Equals, daemon.ResponseTypeError)
   712  }
   713  
   714  func (s *appsSuite) TestLogsSad(c *check.C) {
   715  	s.jctlErrs = []error{errors.New("potato")}
   716  	req, err := http.NewRequest("GET", "/v2/logs", nil)
   717  	c.Assert(err, check.IsNil)
   718  
   719  	rsp := s.req(c, req, nil).(*daemon.Resp)
   720  	c.Assert(rsp.Status, check.Equals, 500)
   721  	c.Assert(rsp.Type, check.Equals, daemon.ResponseTypeError)
   722  }