From 8f5a8aa8b53456353c8d42e87df8515546350274 Mon Sep 17 00:00:00 2001 From: Andrew Phelps <136256549+andrewphelpsj@users.noreply.github.com> Date: Fri, 8 Nov 2024 09:48:41 -0500 Subject: [PATCH] o/snapstate, daemon: respect lanes that are passed to snapstate.InstallComponents, snapstate.InstallComponentPath (#14695) --- daemon/api_sideload_n_try.go | 4 +- daemon/api_sideload_n_try_test.go | 4 +- daemon/export_test.go | 2 +- overlord/snapstate/component.go | 39 +++- overlord/snapstate/component_install_test.go | 222 +++++++++++++++++-- overlord/snapstate/target.go | 26 ++- 6 files changed, 262 insertions(+), 35 deletions(-) diff --git a/daemon/api_sideload_n_try.go b/daemon/api_sideload_n_try.go index ad4afa837b9..e5e4ad8c35f 100644 --- a/daemon/api_sideload_n_try.go +++ b/daemon/api_sideload_n_try.go @@ -334,7 +334,9 @@ func sideloadSnap(_ context.Context, st *state.State, snapFile *uploadedSnap, fl contType = "component" message = fmt.Sprintf("%q component for %q snap", compInfo.Component.ComponentName, instanceName) - tset, err = snapstateInstallComponentPath(st, snap.NewComponentSideInfo(compInfo.Component, snap.Revision{}), snapInfo, snapFile.tmpPath, flags.Flags) + tset, err = snapstateInstallComponentPath(st, snap.NewComponentSideInfo(compInfo.Component, snap.Revision{}), snapInfo, snapFile.tmpPath, snapstate.Options{ + Flags: flags.Flags, + }) } if err != nil { return nil, errToResponse(err, []string{sideInfo.RealName}, InternalError, "cannot install %s file: %v", contType) diff --git a/daemon/api_sideload_n_try_test.go b/daemon/api_sideload_n_try_test.go index db42088eb54..b97fb1a8eb0 100644 --- a/daemon/api_sideload_n_try_test.go +++ b/daemon/api_sideload_n_try_test.go @@ -453,9 +453,9 @@ func (s *sideloadSuite) sideloadComponentCheck(c *check.C, content string, })() defer daemon.MockSnapstateInstallComponentPath(func(st *state.State, csi *snap.ComponentSideInfo, info *snap.Info, - path string, flags snapstate.Flags) (*state.TaskSet, error) { + path string, opts snapstate.Options) (*state.TaskSet, error) { c.Check(csi, check.DeepEquals, expectedCompSideInfo) - c.Check(flags, check.DeepEquals, expectedFlags) + c.Check(opts.Flags, check.DeepEquals, expectedFlags) c.Check(path, testutil.FileEquals, "xyzzy") installQueue = append(installQueue, csi.Component.String()+"::"+path) diff --git a/daemon/export_test.go b/daemon/export_test.go index 0113fa2330c..37bc96a2673 100644 --- a/daemon/export_test.go +++ b/daemon/export_test.go @@ -246,7 +246,7 @@ func MockSnapstateInstallPathMany(f func(context.Context, *state.State, []*snap. } } -func MockSnapstateInstallComponentPath(f func(st *state.State, csi *snap.ComponentSideInfo, info *snap.Info, path string, flags snapstate.Flags) (*state.TaskSet, error)) func() { +func MockSnapstateInstallComponentPath(f func(st *state.State, csi *snap.ComponentSideInfo, info *snap.Info, path string, opts snapstate.Options) (*state.TaskSet, error)) func() { old := snapstateInstallComponentPath snapstateInstallComponentPath = f return func() { diff --git a/overlord/snapstate/component.go b/overlord/snapstate/component.go index f3a1a78b548..2cf11b1e620 100644 --- a/overlord/snapstate/component.go +++ b/overlord/snapstate/component.go @@ -37,9 +37,15 @@ import ( // InstallComponents installs all of the components in the given names list. The // snap represented by info must already be installed, and all of the components // in names should not be installed prior to calling this function. -// -// TODO:COMPS: respect the transaction that is passed to this function func InstallComponents(ctx context.Context, st *state.State, names []string, info *snap.Info, opts Options) ([]*state.TaskSet, error) { + if err := opts.setDefaultLane(st); err != nil { + return nil, err + } + + if err := setDefaultSnapstateOptions(st, &opts); err != nil { + return nil, err + } + var snapst SnapState err := Get(st, info.InstanceName(), &snapst) if err != nil { @@ -83,6 +89,8 @@ func InstallComponents(ctx context.Context, st *state.State, names []string, inf kmodSetup.Set("snap-setup-task", setupSecurity.ID()) } + lane := generateLane(st, opts) + tss := make([]*state.TaskSet, 0, len(compsups)) compSetupIDs := make([]string, 0, len(compsups)) for _, compsup := range compsups { @@ -100,7 +108,11 @@ func InstallComponents(ctx context.Context, st *state.State, names []string, inf } compSetupIDs = append(compSetupIDs, componentTS.compSetupTaskID) - tss = append(tss, componentTS.taskSet()) + + ts := componentTS.taskSet() + ts.JoinLane(lane) + + tss = append(tss, ts) } setupSecurity.Set("component-setup-tasks", compSetupIDs) @@ -109,6 +121,10 @@ func InstallComponents(ctx context.Context, st *state.State, names []string, inf if kmodSetup != nil { ts.AddTask(kmodSetup) } + + // note that this must come after all tasks are added to the task set + ts.JoinLane(lane) + return append(tss, ts), nil } @@ -201,7 +217,15 @@ func installComponentAction(st *state.State, snapst SnapState, snapRev snap.Revi // full metadata in which case the component will appear as installed from the // store. func InstallComponentPath(st *state.State, csi *snap.ComponentSideInfo, info *snap.Info, - path string, flags Flags) (*state.TaskSet, error) { + path string, opts Options) (*state.TaskSet, error) { + if err := opts.setDefaultLane(st); err != nil { + return nil, err + } + + if err := setDefaultSnapstateOptions(st, &opts); err != nil { + return nil, err + } + var snapst SnapState // owner snap must be already installed err := Get(st, info.InstanceName(), &snapst) @@ -223,7 +247,7 @@ func InstallComponentPath(st *state.State, csi *snap.ComponentSideInfo, info *sn Base: info.Base, SideInfo: &info.SideInfo, Channel: info.Channel, - Flags: flags.ForSnapSetup(), + Flags: opts.Flags.ForSnapSetup(), Type: info.Type(), Version: info.Version, PlugsOnly: len(info.Slots) == 0, @@ -244,7 +268,10 @@ func InstallComponentPath(st *state.State, csi *snap.ComponentSideInfo, info *sn return nil, err } - return componentTS.taskSet(), nil + ts := componentTS.taskSet() + ts.JoinLane(generateLane(st, opts)) + + return ts, nil } type ComponentInstallFlags struct { diff --git a/overlord/snapstate/component_install_test.go b/overlord/snapstate/component_install_test.go index 40379fdc9de..cd8f389a123 100644 --- a/overlord/snapstate/component_install_test.go +++ b/overlord/snapstate/component_install_test.go @@ -25,6 +25,7 @@ import ( "fmt" "strings" + "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/snapstate/sequence" @@ -267,6 +268,34 @@ func setStateWithComponents(st *state.State, snapName string, } func (s *snapmgrTestSuite) TestInstallComponentPath(c *C) { + s.testInstallComponentPath(c, testInstallComponentPathOpts{}) +} + +func (s *snapmgrTestSuite) TestInstallComponentPathWithLane(c *C) { + s.testInstallComponentPath(c, testInstallComponentPathOpts{ + lane: 1, + transaction: client.TransactionAllSnaps, + }) +} + +func (s *snapmgrTestSuite) TestInstallComponentPathTransactionAllSnaps(c *C) { + s.testInstallComponentPath(c, testInstallComponentPathOpts{ + transaction: client.TransactionAllSnaps, + }) +} + +func (s *snapmgrTestSuite) TestInstallComponentPathTransactionPerSnap(c *C) { + s.testInstallComponentPath(c, testInstallComponentPathOpts{ + transaction: client.TransactionPerSnap, + }) +} + +type testInstallComponentPathOpts struct { + lane int + transaction client.TransactionType +} + +func (s *snapmgrTestSuite) testInstallComponentPath(c *C, opts testInstallComponentPathOpts) { const snapName = "mysnap" const compName = "mycomp" snapRev := snap.R(1) @@ -280,10 +309,27 @@ func (s *snapmgrTestSuite) TestInstallComponentPath(c *C) { csi := snap.NewComponentSideInfo(naming.ComponentRef{ SnapName: snapName, ComponentName: compName}, snap.R(33)) - ts, err := snapstate.InstallComponentPath(s.state, csi, info, compPath, - snapstate.Flags{}) + + installOpts := snapstate.Options{ + Flags: snapstate.Flags{ + Lane: opts.lane, + Transaction: opts.transaction, + }, + } + + ts, err := snapstate.InstallComponentPath(s.state, csi, info, compPath, installOpts) + c.Assert(err, IsNil) + expectedLane := opts.lane + if opts.transaction != "" && opts.lane == 0 { + expectedLane = 1 + } + + for _, t := range ts.Tasks() { + c.Assert(t.Lanes(), DeepEquals, []int{expectedLane}) + } + verifyComponentInstallTasks(c, compOptIsLocal, ts) c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks())) // File is not deleted @@ -305,7 +351,7 @@ func (s *snapmgrTestSuite) TestInstallUnassertedComponentFailsWithAssertedSnap(c csi := snap.NewComponentSideInfo(naming.ComponentRef{ SnapName: snapName, ComponentName: compName}, snap.Revision{}) _, err := snapstate.InstallComponentPath(s.state, csi, info, compPath, - snapstate.Flags{}) + snapstate.Options{}) c.Assert(err, ErrorMatches, `cannot mix asserted snap and unasserted components`) } @@ -324,7 +370,7 @@ func (s *snapmgrTestSuite) TestInstallAssertedComponentFailsWithUnassertedSnap(c csi := snap.NewComponentSideInfo(naming.ComponentRef{ SnapName: snapName, ComponentName: compName}, snap.R(1)) _, err := snapstate.InstallComponentPath(s.state, csi, info, compPath, - snapstate.Flags{}) + snapstate.Options{}) c.Assert(err, ErrorMatches, `cannot mix unasserted snap and asserted components`) } @@ -344,7 +390,7 @@ func (s *snapmgrTestSuite) TestInstallComponentPathWrongComponent(c *C) { csi := snap.NewComponentSideInfo(naming.ComponentRef{ SnapName: snapName, ComponentName: compName}, snap.R(33)) ts, err := snapstate.InstallComponentPath(s.state, csi, info, compPath, - snapstate.Flags{}) + snapstate.Options{}) c.Assert(ts, IsNil) c.Assert(err, ErrorMatches, `.*"mycomp" is not a component for snap "mysnap"`) } @@ -370,7 +416,7 @@ func (s *snapmgrTestSuite) TestInstallComponentPathWrongType(c *C) { csi := snap.NewComponentSideInfo(naming.ComponentRef{ SnapName: snapName, ComponentName: compName}, snap.R(33)) ts, err := snapstate.InstallComponentPath(s.state, csi, info, compPath, - snapstate.Flags{}) + snapstate.Options{}) c.Assert(ts, IsNil) c.Assert(err.Error(), Equals, `inconsistent component type ("random-comp-type" in snap, "test" in component)`) @@ -403,7 +449,7 @@ func (s *snapmgrTestSuite) TestInstallComponentPathForParallelInstall(c *C) { csi := snap.NewComponentSideInfo(naming.ComponentRef{ SnapName: snapName, ComponentName: compName}, snap.R(33)) ts, err := snapstate.InstallComponentPath(s.state, csi, info, compPath, - snapstate.Flags{}) + snapstate.Options{}) c.Assert(err, IsNil) verifyComponentInstallTasks(c, compOptIsLocal, ts) @@ -434,7 +480,7 @@ func (s *snapmgrTestSuite) TestInstallComponentPathWrongSnap(c *C) { csi := snap.NewComponentSideInfo(naming.ComponentRef{ SnapName: snapName, ComponentName: compName}, snap.R(33)) ts, err := snapstate.InstallComponentPath(s.state, csi, otherInfo, compPath, - snapstate.Flags{}) + snapstate.Options{}) c.Assert(ts, IsNil) c.Assert(err, ErrorMatches, `component "mysnap\+mycomp" is not a component for snap "other-snap"`) @@ -457,7 +503,7 @@ func (s *snapmgrTestSuite) TestInstallComponentPathCompRevisionPresent(c *C) { csi := snap.NewComponentSideInfo(naming.ComponentRef{ SnapName: snapName, ComponentName: compName}, compRev) ts, err := snapstate.InstallComponentPath(s.state, csi, info, compPath, - snapstate.Flags{}) + snapstate.Options{}) c.Assert(err, IsNil) // note that we don't discard the component here, since the component @@ -499,7 +545,7 @@ func (s *snapmgrTestSuite) TestInstallComponentPathCompRevisionPresentDiffSnapRe }) ts, err := snapstate.InstallComponentPath(s.state, csi, info, compPath, - snapstate.Flags{}) + snapstate.Options{}) c.Assert(err, IsNil) // In this case there is no unlink-current-component, as the component @@ -527,7 +573,7 @@ func (s *snapmgrTestSuite) TestInstallComponentPathCompAlreadyInstalled(c *C) { csi := snap.NewComponentSideInfo(naming.ComponentRef{ SnapName: snapName, ComponentName: compName}, compRev) ts, err := snapstate.InstallComponentPath(s.state, csi, info, compPath, - snapstate.Flags{}) + snapstate.Options{}) c.Assert(err, IsNil) verifyComponentInstallTasks(c, compOptIsLocal|compOptIsActive|compCurrentIsDiscarded, ts) @@ -558,7 +604,7 @@ func (s *snapmgrTestSuite) TestInstallComponentPathSnapNotActive(c *C) { }) ts, err := snapstate.InstallComponentPath(s.state, csi, info, compPath, - snapstate.Flags{}) + snapstate.Options{}) c.Assert(err.Error(), Equals, `cannot install component "mysnap+mycomp" for disabled snap "mysnap"`) c.Assert(ts, IsNil) c.Assert(osutil.FileExists(compPath), Equals, true) @@ -583,7 +629,7 @@ func (s *snapmgrTestSuite) TestInstallComponentPathRemodelConflict(c *C) { csi := snap.NewComponentSideInfo(naming.ComponentRef{ SnapName: snapName, ComponentName: compName}, snap.R(33)) ts, err := snapstate.InstallComponentPath(s.state, csi, info, compPath, - snapstate.Flags{}) + snapstate.Options{}) c.Assert(ts, IsNil) c.Assert(err.Error(), Equals, `remodeling in progress, no other changes allowed until this is done`) @@ -611,7 +657,7 @@ func (s *snapmgrTestSuite) TestInstallComponentPathUpdateConflict(c *C) { csi := snap.NewComponentSideInfo(naming.ComponentRef{ SnapName: snapName, ComponentName: compName}, snap.R(33)) ts, err := snapstate.InstallComponentPath(s.state, csi, info, compPath, - snapstate.Flags{}) + snapstate.Options{}) c.Assert(ts, IsNil) c.Assert(err.Error(), Equals, `snap "some-snap" has "update" change in progress`) @@ -752,7 +798,7 @@ func (s *snapmgrTestSuite) TestInstallKernelModulesComponentPath(c *C) { csi := snap.NewComponentSideInfo(naming.ComponentRef{ SnapName: snapName, ComponentName: compName}, snap.R(33)) ts, err := snapstate.InstallComponentPath(s.state, csi, info, compPath, - snapstate.Flags{}) + snapstate.Options{}) c.Assert(err, IsNil) verifyComponentInstallTasks(c, compOptIsLocal|compTypeIsKernMods, ts) @@ -795,7 +841,7 @@ func (s *snapmgrTestSuite) TestInstallComponentPathCompRevisionPresentInTwoSeqPt csi := snap.NewComponentSideInfo(naming.ComponentRef{ SnapName: snapName, ComponentName: compName}, compRev) ts, err := snapstate.InstallComponentPath(s.state, csi, info, compPath, - snapstate.Flags{}) + snapstate.Options{}) c.Assert(err, IsNil) verifyComponentInstallTasks(c, compOptIsLocal|compOptIsActive, ts) @@ -823,7 +869,7 @@ func (s *snapmgrTestSuite) TestInstallComponentPathRun(c *C) { cref := naming.NewComponentRef(snapName, compName) csi := snap.NewComponentSideInfo(cref, snap.R(33)) ts, err := snapstate.InstallComponentPath(s.state, csi, info, compPath, - snapstate.Flags{}) + snapstate.Options{}) c.Assert(err, IsNil) c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks())) @@ -846,6 +892,34 @@ func (s *snapmgrTestSuite) TestInstallComponentPathRun(c *C) { } func (s *snapmgrTestSuite) TestInstallComponents(c *C) { + s.testInstallComponents(c, testInstallComponentsOpts{}) +} + +func (s *snapmgrTestSuite) TestInstallComponentsWithLane(c *C) { + s.testInstallComponents(c, testInstallComponentsOpts{ + lane: 1, + transaction: client.TransactionAllSnaps, + }) +} + +func (s *snapmgrTestSuite) TestInstallComponentsTransactionAllSnaps(c *C) { + s.testInstallComponents(c, testInstallComponentsOpts{ + transaction: client.TransactionAllSnaps, + }) +} + +func (s *snapmgrTestSuite) TestInstallComponentsTransactionPerSnap(c *C) { + s.testInstallComponents(c, testInstallComponentsOpts{ + transaction: client.TransactionPerSnap, + }) +} + +type testInstallComponentsOpts struct { + lane int + transaction client.TransactionType +} + +func (s *snapmgrTestSuite) testInstallComponents(c *C, opts testInstallComponentsOpts) { const snapName = "some-snap" snapRev := snap.R(1) @@ -902,7 +976,14 @@ func (s *snapmgrTestSuite) TestInstallComponents(c *C) { return results } - tss, err := snapstate.InstallComponents(context.Background(), s.state, components, info, snapstate.Options{}) + installOpts := snapstate.Options{ + Flags: snapstate.Flags{ + Lane: opts.lane, + Transaction: opts.transaction, + }, + } + + tss, err := snapstate.InstallComponents(context.Background(), s.state, components, info, installOpts) c.Assert(err, IsNil) setupProfiles := tss[len(tss)-1].Tasks()[0] @@ -911,10 +992,19 @@ func (s *snapmgrTestSuite) TestInstallComponents(c *C) { prepareKmodComps := tss[len(tss)-1].Tasks()[1] c.Assert(prepareKmodComps.Kind(), Equals, "prepare-kernel-modules-components") + expectedLane := opts.lane + if opts.transaction != "" && opts.lane == 0 { + expectedLane = 1 + } + // add to change so that we can use TaskComponentSetup chg := s.state.NewChange("install", "...") for _, ts := range tss { chg.AddAll(ts) + + for _, t := range ts.Tasks() { + c.Assert(t.Lanes(), DeepEquals, []int{expectedLane}) + } } snapsup, err := snapstate.TaskSnapSetup(prepareKmodComps) @@ -991,3 +1081,99 @@ func (s *snapmgrTestSuite) TestInstallComponentsAlreadyInstalledError(c *C) { c.Assert(err, testutil.ErrorIs, snap.AlreadyInstalledComponentError{Component: "one"}) } + +func (s *snapmgrTestSuite) TestInstallComponentsInvalidFlagAndTransaction(c *C) { + const snapName = "some-snap" + snapRev := snap.R(1) + compNamesToType := map[string]string{ + "one": "standard", + "two": "standard", + } + + info := createTestSnapInfoForComponents(c, snapName, snapRev, compNamesToType) + + s.state.Lock() + defer s.state.Unlock() + + _, err := snapstate.InstallComponents(context.TODO(), s.state, []string{"one", "two"}, info, snapstate.Options{ + Flags: snapstate.Flags{Lane: 1}, + }) + c.Assert(err, ErrorMatches, `cannot specify a lane without setting transaction to "all-snaps"`) +} + +func (s *snapmgrTestSuite) TestInstallComponentPathInvalidFlagAndTransaction(c *C) { + const snapName = "some-snap" + snapRev := snap.R(1) + compNamesToType := map[string]string{ + "one": "standard", + } + + info := createTestSnapInfoForComponents(c, snapName, snapRev, compNamesToType) + _, compPath := createTestComponentWithType(c, snapName, "one", "standard", info) + + s.state.Lock() + defer s.state.Unlock() + + setStateWithOneSnap(s.state, snapName, snapRev) + + csi := snap.NewComponentSideInfo(naming.ComponentRef{ + SnapName: snapName, + ComponentName: "one", + }, snap.R(33)) + + _, err := snapstate.InstallComponentPath(s.state, csi, info, compPath, snapstate.Options{ + Flags: snapstate.Flags{Lane: 1}, + }) + c.Assert(err, ErrorMatches, `cannot specify a lane without setting transaction to "all-snaps"`) +} + +func (s *snapmgrTestSuite) TestInstallComponentsTooEarly(c *C) { + const snapName = "some-snap" + snapRev := snap.R(1) + compNamesToType := map[string]string{ + "one": "standard", + "two": "standard", + } + + info := createTestSnapInfoForComponents(c, snapName, snapRev, compNamesToType) + + restore := snapstatetest.MockDeviceModel(nil) + defer restore() + + s.state.Lock() + defer s.state.Unlock() + + _, err := snapstate.InstallComponents(context.TODO(), s.state, []string{"one", "two"}, info, snapstate.Options{ + Seed: true, + }) + c.Assert(err, ErrorMatches, `.*too early for operation, device model not yet acknowledged`) +} + +func (s *snapmgrTestSuite) TestInstallComponentPathTooEarly(c *C) { + const snapName = "some-snap" + snapRev := snap.R(1) + compNamesToType := map[string]string{ + "one": "standard", + } + + info := createTestSnapInfoForComponents(c, snapName, snapRev, compNamesToType) + _, compPath := createTestComponentWithType(c, snapName, "one", "standard", info) + + restore := snapstatetest.MockDeviceModel(nil) + defer restore() + + s.state.Lock() + defer s.state.Unlock() + + setStateWithOneSnap(s.state, snapName, snapRev) + + csi := snap.NewComponentSideInfo(naming.ComponentRef{ + SnapName: snapName, + ComponentName: "one", + }, snap.R(33)) + + _, err := snapstate.InstallComponentPath(s.state, csi, info, compPath, snapstate.Options{ + Seed: true, + }) + c.Assert(err, ErrorMatches, `.*too early for operation, device model not yet acknowledged`) +} diff --git a/overlord/snapstate/target.go b/overlord/snapstate/target.go index 4c2ff2353f2..b18b240fc93 100644 --- a/overlord/snapstate/target.go +++ b/overlord/snapstate/target.go @@ -66,6 +66,18 @@ type Options struct { ExpectOneSnap bool } +func (opts *Options) setDefaultLane(st *state.State) error { + if opts.Flags.Transaction != client.TransactionAllSnaps && opts.Flags.Lane != 0 { + return errors.New("cannot specify a lane without setting transaction to \"all-snaps\"") + } + + if opts.Flags.Transaction == client.TransactionAllSnaps && opts.Flags.Lane == 0 { + opts.Flags.Lane = st.NewLane() + } + + return nil +} + // target represents the data needed to setup a snap for installation. type target struct { // setup is a partially initialized SnapSetup that contains the data needed @@ -565,13 +577,8 @@ func sortComponentsOnTargets(targets []target) { // TODO: rename this to Install once the API is settled, and we can rename or // remove the old Install function. func InstallWithGoal(ctx context.Context, st *state.State, goal InstallGoal, opts Options) ([]*snap.Info, []*state.TaskSet, error) { - // can only specify a lane when running multiple operations transactionally - if opts.Flags.Transaction != client.TransactionAllSnaps && opts.Flags.Lane != 0 { - return nil, nil, errors.New("cannot specify a lane without setting transaction to \"all-snaps\"") - } - - if opts.Flags.Transaction == client.TransactionAllSnaps && opts.Flags.Lane == 0 { - opts.Flags.Lane = st.NewLane() + if err := opts.setDefaultLane(st); err != nil { + return nil, nil, err } if err := setDefaultSnapstateOptions(st, &opts); err != nil { @@ -953,6 +960,11 @@ func UpdateWithGoal(ctx context.Context, st *state.State, goal UpdateGoal, filte return nil, nil, errors.New("internal error: auto-refresh is not supported when updating a single snap") } + // TODO: note that we cannot use opts.setDefaultLane here, since there is an + // inconsistency between how the various functions in snapstate handle lanes + // and transactions (update is the unique case). consider fixing this once + // we remove the old snapstate.Install/Update functions + // // can only specify a lane when running multiple operations transactionally if opts.Flags.Transaction != client.TransactionAllSnaps && opts.Flags.Lane != 0 { return nil, nil, errors.New("cannot specify a lane without setting transaction to \"all-snaps\"")