Skip to content

Commit

Permalink
o/h/ctlcmd: add a snapctl fail command
Browse files Browse the repository at this point in the history
The snapctl fail commands rejects changes in a registry transaction.

Signed-off-by: Miguel Pires <[email protected]>
  • Loading branch information
MiguelPires committed Sep 20, 2024
1 parent ac897ee commit dfed99a
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 2 deletions.
74 changes: 74 additions & 0 deletions overlord/hookstate/ctlcmd/fail.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2024 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

package ctlcmd

import (
"errors"
"fmt"
"strings"

"github.com/snapcore/snapd/i18n"
"github.com/snapcore/snapd/overlord/registrystate"
)

type failCommand struct {
baseCommand

Positional struct {
Reason string `positional-args:"true" positional-arg-name:":<rejection-reason>"`
} `positional-args:"yes" required:"yes"`
}

var shortFailHelp = i18n.G("Fail a registry change")
var longFailHelp = i18n.G(`
The fail command rejects the registry changes currently being proposed,
providing a reason for the rejection. It may only be used in a
change-view-<plug> hook.
`)

func init() {
addCommand("fail", shortFailHelp, longFailHelp, func() command {
return &failCommand{}
})
}

func (c *failCommand) Execute(args []string) error {
ctx, err := c.ensureContext()
if err != nil {
return err
}

ctx.Lock()
defer ctx.Unlock()

if ctx.IsEphemeral() || !strings.HasPrefix(ctx.HookName(), "change-view-") {
return errors.New(i18n.G("cannot use `snapctl fail` outside of a \"change-view\" hook"))
}

t, _ := ctx.Task()
tx, commitTask, err := registrystate.GetStoredTransaction(t)
if err != nil {
return fmt.Errorf(i18n.G("internal error: cannot get registry transaction to fail: %v"), err)
}

tx.Abort(ctx.InstanceName(), c.Positional.Reason)
commitTask.Set("registry-transaction", tx)
return nil
}
98 changes: 98 additions & 0 deletions overlord/hookstate/ctlcmd/fail_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2024 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

package ctlcmd_test

import (
. "gopkg.in/check.v1"

"github.com/snapcore/snapd/i18n"
"github.com/snapcore/snapd/overlord/hookstate"
"github.com/snapcore/snapd/overlord/hookstate/ctlcmd"
"github.com/snapcore/snapd/overlord/registrystate"
"github.com/snapcore/snapd/snap"
)

func (s *registrySuite) TestFailAbortsRegistryTransaction(c *C) {
s.state.Lock()
chg := s.state.NewChange("test", "")
commitTask := s.state.NewTask("commit-registry-tx", "")
chg.AddTask(commitTask)
tx, err := registrystate.NewTransaction(s.state, "my-acc", "my-reg")
c.Assert(err, IsNil)

err = tx.Set("foo", "bar")
c.Assert(err, IsNil)

commitTask.Set("registry-transaction", tx)

task := s.state.NewTask("run-hook", "")
chg.AddTask(task)
setup := &hookstate.HookSetup{Snap: "test-snap", Revision: snap.R(1), Hook: "change-view-plug"}
task.Set("commit-task", commitTask.ID())
s.state.Unlock()

mockContext, err := hookstate.NewContext(task, s.state, setup, s.mockHandler, "")
c.Assert(err, IsNil)

stdout, stderr, err := ctlcmd.Run(mockContext, []string{"fail", "don't like changes"}, 0)
c.Assert(err, IsNil)
c.Check(stdout, IsNil)
c.Check(stderr, IsNil)

tx = nil
s.state.Lock()
err = commitTask.Get("registry-transaction", &tx)
s.state.Unlock()
c.Assert(err, IsNil)

snap, reason := tx.AbortInfo()
c.Assert(reason, Equals, "don't like changes")
c.Assert(snap, Equals, "test-snap")
}

func (s *registrySuite) TestFailErrors(c *C) {
stdout, stderr, err := ctlcmd.Run(s.mockContext, []string{"fail"}, 0)
c.Assert(err, ErrorMatches, i18n.G("the required argument `:<rejection-reason>` was not provided"))
c.Check(stdout, IsNil)
c.Check(stderr, IsNil)

s.state.Lock()
task := s.state.NewTask("run-hook", "my test task")
setup := &hookstate.HookSetup{Snap: "test-snap", Revision: snap.R(1), Hook: "other-hook"}
s.state.Unlock()

s.mockContext, err = hookstate.NewContext(task, s.state, setup, s.mockHandler, "")
c.Assert(err, IsNil)

stdout, stderr, err = ctlcmd.Run(s.mockContext, []string{"fail", "reason"}, 0)
c.Assert(err, ErrorMatches, i18n.G("cannot use `snapctl fail` outside of a \"change-view\" hook"))
c.Check(stdout, IsNil)
c.Check(stderr, IsNil)

setup = &hookstate.HookSetup{Snap: "test-snap", Revision: snap.R(1), Hook: "change-view-plug"}
s.mockContext, err = hookstate.NewContext(task, s.state, setup, s.mockHandler, "")
c.Assert(err, IsNil)

stdout, stderr, err = ctlcmd.Run(s.mockContext, []string{"fail", "reason"}, 0)
// this shouldn't happen but check we handle it well anyway
c.Assert(err, ErrorMatches, i18n.G("internal error: cannot get registry transaction to fail: no state entry for key \"commit-task\""))
c.Check(stdout, IsNil)
c.Check(stderr, IsNil)
}
8 changes: 6 additions & 2 deletions overlord/registrystate/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,8 +279,8 @@ func (t *Transaction) Data() ([]byte, error) {
return t.modified.Data()
}

// Abort prevents any further writes or reads to the transaction. It takes an
// abortingSnap and reason that can be used to surface information to the user.
// Abort prevents any further writes or reads to the transaction. It takes a
// snap and reason that can be used to surface information to the user.
func (t *Transaction) Abort(abortingSnap, reason string) {
t.mu.Lock()
defer t.mu.Unlock()
Expand All @@ -292,3 +292,7 @@ func (t *Transaction) Abort(abortingSnap, reason string) {
func (t *Transaction) aborted() bool {
return t.abortReason != ""
}

func (t *Transaction) AbortInfo() (snap, reason string) {
return t.abortingSnap, t.abortReason
}
4 changes: 4 additions & 0 deletions overlord/registrystate/transaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,10 @@ func (s *transactionTestSuite) TestAbortPreventsReadsAndWrites(c *C) {

tx.Abort("my-snap", "don't like the changes")

snap, reason := tx.AbortInfo()
c.Assert(reason, Equals, "don't like the changes")
c.Assert(snap, Equals, "my-snap")

err = tx.Set("foo", "bar")
c.Assert(err, ErrorMatches, "cannot write to aborted transaction")

Expand Down

0 comments on commit dfed99a

Please sign in to comment.