Skip to content

Commit 1372382

Browse files
committed
o/h/ctlcmd, t/m/component-snapctl: improve snapctl install consistency
* cmd, daemon, o/h/ctlcmd: exit code 0 and message when all components are already installed * o/h/ctlcmd: exit with 0 and message on stderr when installing new and already installed components * c/snapctl, daemon: avoid marshalling error directly in daemon and parse marshalled data with type checks in client * c/snapctl, daemon: only include components in marshalled data * o/h/ctlcmd: test install with already installed components * c/snapctl: add test for already installed error in snapctl command * c/snapctl, o/hctlcmd, t/m/component-snapctl: add spread test to check install end to end * c/snapctl: use Fprintln instead of writing bytes * o/h/ctlcmd: contain all the logic for handling already installed components in installCommand.Execute * o/h/ctlcmd, t/m/component-snapctl: avoid passing managementCommand as a pointer and return affected components instead * o/h/ctlcmd: change variable name from affected to affectedComponents * o/h/ctlcmd: fix parallel install handling when checking if component is installed and add regression test for it * o/h/ctlcmd: add comment for vset presence check * o/h/ctlcmd, t/m/component-snapctl: fixups
1 parent 6ef8a8e commit 1372382

5 files changed

Lines changed: 230 additions & 17 deletions

File tree

overlord/hookstate/ctlcmd/helpers.go

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ func changeIDIfNotEphemeral(hctx *hookstate.Context) string {
391391
return ""
392392
}
393393

394-
func createSnapctlInstallTasks(hctx *hookstate.Context, cmd managementCommand) (tss []*state.TaskSet, err error) {
394+
func createSnapctlInstallTasks(hctx *hookstate.Context, cmd managementCommand) (affectedComponents []string, tss []*state.TaskSet, err error) {
395395
st := hctx.State()
396396
st.Lock()
397397
defer st.Unlock()
@@ -400,15 +400,45 @@ func createSnapctlInstallTasks(hctx *hookstate.Context, cmd managementCommand) (
400400
// by the current change
401401
vsets, err := hctx.PendingValidationSets()
402402
if err != nil {
403-
return nil, err
403+
return nil, nil, err
404404
}
405405

406-
info, err := currentSnapInfo(st, hctx.InstanceName())
406+
instanceName := hctx.InstanceName()
407+
var snapst snapstate.SnapState
408+
if err := snapstate.Get(st, instanceName, &snapst); err != nil {
409+
return nil, nil, err
410+
}
411+
412+
info, err := snapst.CurrentInfo()
407413
if err != nil {
408-
return nil, err
414+
return nil, nil, err
415+
}
416+
417+
snapName := snap.InstanceSnap(instanceName)
418+
if vsets != nil {
419+
// When validation sets are provided, we install all components
420+
// regardless of their current installation state. thus, all components
421+
// will be affected.
422+
affectedComponents = cmd.components
423+
} else {
424+
for _, comp := range cmd.components {
425+
if snapst.CurrentComponentSideInfo(naming.NewComponentRef(snapName, comp)) == nil {
426+
affectedComponents = append(affectedComponents, comp)
427+
}
428+
}
409429
}
410-
return snapstateInstallComponents(context.TODO(), st, cmd.components, info, vsets,
430+
431+
if len(affectedComponents) == 0 {
432+
return nil, nil, snap.NewAlreadyInstalledComponentsError(instanceName, cmd.components)
433+
}
434+
435+
tss, err = snapstateInstallComponents(context.TODO(), st, affectedComponents, info, vsets,
411436
snapstate.Options{ExpectOneSnap: true, ConflictOptions: snapstate.ConflictOptions{FromChange: changeIDIfNotEphemeral(hctx)}})
437+
438+
if err != nil {
439+
return nil, nil, err
440+
}
441+
return affectedComponents, tss, nil
412442
}
413443

414444
func createSnapctlRemoveTasks(hctx *hookstate.Context, cmd managementCommand) (tss []*state.TaskSet, err error) {
@@ -421,21 +451,20 @@ func createSnapctlRemoveTasks(hctx *hookstate.Context, cmd managementCommand) (t
421451
ConflictOptions: snapstate.ConflictOptions{FromChange: changeIDIfNotEphemeral(hctx)}})
422452
}
423453

424-
func runSnapManagementCommand(hctx *hookstate.Context, cmd managementCommand) (id string, err error) {
425-
st := hctx.State()
454+
func runSnapManagementCommand(hctx *hookstate.Context, cmd managementCommand) (id string, affectedComponents []string, err error) {
426455
var tss []*state.TaskSet
427456
var cmdStr, cmdVerb string
428457
var changeKind string
429458

430459
// If the context is non-ephemeral, we don't support async because we are just queuing a change in the first place.
431460
// In the future this could be made non-queuing, but for now we just return the error.
432461
if cmd.async && !hctx.IsEphemeral() {
433-
return "", fmt.Errorf("internal error: cannot run snap management command asynchronously from a non-ephemeral context")
462+
return "", nil, fmt.Errorf("internal error: cannot run snap management command asynchronously from a non-ephemeral context")
434463
}
435464

436465
switch cmd.operation {
437466
case installManagementCommand:
438-
tss, err = createSnapctlInstallTasks(hctx, cmd)
467+
affectedComponents, tss, err = createSnapctlInstallTasks(hctx, cmd)
439468
cmdStr = "install"
440469
cmdVerb = "Installing"
441470
changeKind = snapctlInstallChangeKind
@@ -448,15 +477,19 @@ func runSnapManagementCommand(hctx *hookstate.Context, cmd managementCommand) (i
448477
err = fmt.Errorf("internal error: %q is not a valid snap management command", cmd.operation)
449478
}
450479
if err != nil {
451-
return "", err
480+
return "", nil, err
452481
}
453482

454483
if !hctx.IsEphemeral() {
455484
// Differently to service control commands, we always queue the
456485
// management tasks if run from a hook.
457-
return "", queueCommand(hctx, tss)
486+
if err := queueCommand(hctx, tss); err != nil {
487+
return "", nil, err
488+
}
489+
return "", affectedComponents, nil
458490
}
459491

492+
st := hctx.State()
460493
st.Lock()
461494
chg := st.NewChange(changeKind,
462495
fmt.Sprintf("%s components %v for snap %s",
@@ -469,16 +502,19 @@ func runSnapManagementCommand(hctx *hookstate.Context, cmd managementCommand) (i
469502
st.Unlock()
470503

471504
if cmd.async {
472-
return chg.ID(), nil
505+
return chg.ID(), affectedComponents, nil
473506
}
474507

475508
select {
476509
case <-chg.Ready():
477510
st.Lock()
478511
defer st.Unlock()
479-
return "", chg.Err()
512+
if err := chg.Err(); err != nil {
513+
return "", nil, err
514+
}
515+
return "", affectedComponents, nil
480516
case <-time.After(10 * time.Minute):
481-
return "", fmt.Errorf("snapctl %s command is taking too long", cmdStr)
517+
return "", nil, fmt.Errorf("snapctl %s command is taking too long", cmdStr)
482518
}
483519
}
484520

overlord/hookstate/ctlcmd/install.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import (
2323
"fmt"
2424

2525
"github.com/snapcore/snapd/i18n"
26+
"github.com/snapcore/snapd/snap"
27+
"github.com/snapcore/snapd/strutil"
2628
)
2729

2830
var (
@@ -55,10 +57,21 @@ func (c *installCommand) Execute([]string) error {
5557
return err
5658
}
5759

58-
id, err := runSnapManagementCommand(ctx, managementCommand{operation: installManagementCommand, components: comps, async: c.NoWait})
60+
id, affectedComponents, err := runSnapManagementCommand(ctx, managementCommand{operation: installManagementCommand, components: comps, async: c.NoWait})
5961

6062
if err != nil {
61-
return err
63+
if _, ok := err.(*snap.AlreadyInstalledError); !ok {
64+
return err
65+
}
66+
}
67+
68+
if len(affectedComponents) < len(comps) {
69+
for _, comp := range comps {
70+
if !strutil.ListContains(affectedComponents, comp) {
71+
msg := fmt.Sprintf(i18n.G(`snapctl: component %q is already installed`), comp)
72+
fmt.Fprintln(c.stderr, msg)
73+
}
74+
}
6275
}
6376

6477
if c.NoWait {

overlord/hookstate/ctlcmd/remove.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ func (c *removeCommand) Execute([]string) error {
5555
return err
5656
}
5757

58-
id, err := runSnapManagementCommand(ctx, managementCommand{operation: removeManagementCommand, components: comps, async: c.NoWait})
58+
id, _, err := runSnapManagementCommand(ctx, managementCommand{operation: removeManagementCommand, components: comps, async: c.NoWait})
5959
if err != nil {
6060
return err
6161
}

overlord/hookstate/ctlcmd/snap_management_test.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,11 @@ import (
3535
"github.com/snapcore/snapd/overlord/hookstate/hooktest"
3636
"github.com/snapcore/snapd/overlord/ifacestate/ifacerepo"
3737
"github.com/snapcore/snapd/overlord/snapstate"
38+
"github.com/snapcore/snapd/overlord/snapstate/sequence"
3839
"github.com/snapcore/snapd/overlord/snapstate/snapstatetest"
3940
"github.com/snapcore/snapd/overlord/state"
4041
"github.com/snapcore/snapd/snap"
42+
"github.com/snapcore/snapd/snap/naming"
4143
"github.com/snapcore/snapd/snap/snaptest"
4244
"github.com/snapcore/snapd/testutil"
4345
)
@@ -362,3 +364,141 @@ func (s *installSuite) TestNoWaitInstallAndRemoveCommands(c *C) {
362364
s.st.Unlock()
363365
}
364366
}
367+
368+
func (s *installSuite) TestInstallWithParallelInstalledSnap(c *C) {
369+
s.st.Lock()
370+
s.chg = s.st.NewChange("install change", "install change")
371+
task := s.st.NewTask("test-task", "my test task")
372+
s.chg.AddTask(task)
373+
setup := &hookstate.HookSetup{Snap: "test-snap_foo", Revision: snap.R(1), Hook: "test-hook"}
374+
375+
// create a context for the parallel installed snap
376+
var err error
377+
s.mockContext, err = hookstate.NewContext(task, task.State(), setup, s.mockHandler, "")
378+
c.Assert(err, IsNil)
379+
380+
installTask := s.st.NewTask("queued", "queued task")
381+
s.st.Unlock()
382+
383+
restore := ctlcmd.MockSnapstateInstallComponentsFunc(func(ctx context.Context, st *state.State, names []string, info *snap.Info, vsets *snapasserts.ValidationSets, opts snapstate.Options) ([]*state.TaskSet, error) {
384+
c.Check(names, DeepEquals, []string{"two"})
385+
c.Check(opts, DeepEquals, snapstate.Options{ExpectOneSnap: true,
386+
ConflictOptions: snapstate.ConflictOptions{FromChange: s.mockContext.ChangeID()}})
387+
var ts state.TaskSet
388+
ts.AddTask(installTask)
389+
return []*state.TaskSet{&ts}, nil
390+
})
391+
defer restore()
392+
393+
rev := snap.R(1)
394+
si := &snap.SideInfo{
395+
RealName: "test-snap",
396+
Revision: rev,
397+
SnapID: "test-snap-id",
398+
}
399+
400+
seq := snapstatetest.NewSequenceFromRevisionSideInfos([]*sequence.RevisionSideState{
401+
sequence.NewRevisionSideState(si, nil),
402+
})
403+
404+
// component +one is already installed
405+
seq.AddComponentForRevision(snap.R(1), sequence.NewComponentState(&snap.ComponentSideInfo{
406+
Component: naming.NewComponentRef("test-snap", "one"),
407+
Revision: snap.R(1),
408+
}, snap.StandardComponent))
409+
410+
s.st.Lock()
411+
snapstate.Set(s.st, "test-snap_foo", &snapstate.SnapState{
412+
Active: true,
413+
Sequence: seq,
414+
Current: rev,
415+
})
416+
s.st.Unlock()
417+
418+
stdout, stderr, err := ctlcmd.Run(s.mockContext, []string{"install", "+one", "+two"}, 0, nil)
419+
c.Check(err, IsNil)
420+
c.Check(stdout, HasLen, 0)
421+
c.Check(string(stderr), Matches, `(?sm).*snapctl: component "one" is already installed`)
422+
}
423+
424+
func (s *installSuite) TestInstallAllAlreadyInstalled(c *C) {
425+
rev := snap.R(1)
426+
si := &snap.SideInfo{
427+
RealName: "test-snap",
428+
Revision: rev,
429+
SnapID: "test-snap-id",
430+
}
431+
432+
seq := snapstatetest.NewSequenceFromRevisionSideInfos([]*sequence.RevisionSideState{
433+
sequence.NewRevisionSideState(si, nil),
434+
})
435+
436+
seq.AddComponentForRevision(snap.R(1), sequence.NewComponentState(&snap.ComponentSideInfo{
437+
Component: naming.NewComponentRef("test-snap", "one"),
438+
Revision: snap.R(1),
439+
}, snap.StandardComponent))
440+
441+
seq.AddComponentForRevision(snap.R(1), sequence.NewComponentState(&snap.ComponentSideInfo{
442+
Component: naming.NewComponentRef("test-snap", "two"),
443+
Revision: snap.R(1),
444+
}, snap.StandardComponent))
445+
446+
s.st.Lock()
447+
snapstate.Set(s.st, "test-snap", &snapstate.SnapState{
448+
Active: true,
449+
Sequence: seq,
450+
Current: rev,
451+
})
452+
s.st.Unlock()
453+
454+
stdout, stderr, err := ctlcmd.Run(s.mockContext, []string{"install", "+one", "+two"}, 0, nil)
455+
c.Check(err, IsNil)
456+
c.Check(stdout, HasLen, 0)
457+
c.Check(string(stderr), Matches, `(?sm).*snapctl: component "one" is already installed`)
458+
c.Check(string(stderr), Matches, `(?sm).*snapctl: component "two" is already installed`)
459+
}
460+
461+
func (s *installSuite) TestInstallSomeAlreadyInstalled(c *C) {
462+
s.st.Lock()
463+
task := s.st.NewTask("queued", "queued task")
464+
s.st.Unlock()
465+
466+
restore := ctlcmd.MockSnapstateInstallComponentsFunc(func(ctx context.Context, st *state.State, names []string, info *snap.Info, vsets *snapasserts.ValidationSets, opts snapstate.Options) ([]*state.TaskSet, error) {
467+
c.Check(names, DeepEquals, []string{"two"})
468+
c.Check(opts, DeepEquals, snapstate.Options{ExpectOneSnap: true,
469+
ConflictOptions: snapstate.ConflictOptions{FromChange: s.mockContext.ChangeID()}})
470+
var ts state.TaskSet
471+
ts.AddTask(task)
472+
return []*state.TaskSet{&ts}, nil
473+
})
474+
defer restore()
475+
476+
rev := snap.R(1)
477+
si := &snap.SideInfo{
478+
RealName: "test-snap",
479+
Revision: rev,
480+
SnapID: "test-snap-id",
481+
}
482+
483+
seq := snapstatetest.NewSequenceFromRevisionSideInfos([]*sequence.RevisionSideState{
484+
sequence.NewRevisionSideState(si, nil),
485+
})
486+
487+
seq.AddComponentForRevision(snap.R(1), sequence.NewComponentState(&snap.ComponentSideInfo{
488+
Component: naming.NewComponentRef("test-snap", "one"),
489+
Revision: snap.R(1),
490+
}, snap.StandardComponent))
491+
492+
s.st.Lock()
493+
snapstate.Set(s.st, "test-snap", &snapstate.SnapState{
494+
Active: true,
495+
Sequence: seq,
496+
Current: rev,
497+
})
498+
499+
s.st.Unlock()
500+
stdout, stderr, err := ctlcmd.Run(s.mockContext, []string{"install", "+one", "+two"}, 0, nil)
501+
c.Check(err, IsNil)
502+
c.Check(stdout, HasLen, 0)
503+
c.Check(string(stderr), Matches, `snapctl: component "one" is already installed\n`)
504+
}

tests/main/component-snapctl/task.yaml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,27 @@ execute: |
2222
2323
snap set test-snap-components-snapctl command="remove +one+two"
2424
test ! -d "$SNAP_MOUNT_DIR"/test-snap-components-snapctl/components/
25+
26+
echo "Check already installed snap exits with code 0"
27+
snap run --shell test-snap-components-snapctl -c "snapctl install +one" 1>stdout.out 2>stderr.out
28+
test ! -s stdout.out
29+
test ! -s stderr.out
30+
snap components test-snap-components-snapctl | MATCH "test-snap-components-snapctl\+one\s+installed\s+test"
31+
snap components test-snap-components-snapctl | MATCH "test-snap-components-snapctl\+two\s+available\s+test"
32+
33+
snap run --shell test-snap-components-snapctl -c "snapctl install +one" 1>stdout.out 2>stderr.out
34+
test ! -s stdout.out
35+
MATCH "snapctl: component \"one\" is already installed" < stderr.out
36+
37+
echo "Check new component can be installed when listed with already installed component"
38+
snap run --shell test-snap-components-snapctl -c "snapctl install +one +two" 1>stdout.out 2>stderr.out
39+
test ! -s stdout.out
40+
MATCH "snapctl: component \"one\" is already installed" < stderr.out
41+
snap components test-snap-components-snapctl | MATCH "test-snap-components-snapctl\+one\s+installed\s+test"
42+
snap components test-snap-components-snapctl | MATCH "test-snap-components-snapctl\+two\s+installed\s+test"
43+
44+
snap run --shell test-snap-components-snapctl -c "snapctl install +one +two" 1>stdout.out 2>stderr.out
45+
test ! -s stdout.out
46+
MATCH "snapctl: component \"one\" is already installed" < stderr.out
47+
MATCH "snapctl: component \"two\" is already installed" < stderr.out
48+

0 commit comments

Comments
 (0)