Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

o/h/ctlcmd: add a snapctl fail command #14525

Merged
merged 4 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions overlord/hookstate/ctlcmd/fail.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// -*- 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/features"
"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() {
info := addCommand("fail", shortFailHelp, longFailHelp, func() command {
return &failCommand{}
})
info.hidden = true
}

func (c *failCommand) Execute(args []string) error {
if !features.Registries.IsEnabled() {
_, confName := features.Registries.ConfigOption()
return fmt.Errorf(`cannot use "snapctl fail" without enabling the %q feature`, confName)
}

ctx, err := c.ensureContext()
if err != nil {
return err
}

Check warning on line 63 in overlord/hookstate/ctlcmd/fail.go

View check run for this annotation

Codecov / codecov/patch

overlord/hookstate/ctlcmd/fail.go#L62-L63

Added lines #L62 - L63 were not covered by tests

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
}
127 changes: 127 additions & 0 deletions overlord/hookstate/ctlcmd/fail_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// -*- 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 (
"os"

"gopkg.in/check.v1"
. "gopkg.in/check.v1"

"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/features"
"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) mockRegistriesFeature(c *C) (restore func()) {
oldDir := dirs.FeaturesDir
dirs.FeaturesDir = c.MkDir()

registryCtlFile := features.Registries.ControlFile()
c.Assert(os.WriteFile(registryCtlFile, []byte(nil), 0644), check.IsNil)

return func() {
c.Assert(os.Remove(registryCtlFile), IsNil)
dirs.FeaturesDir = oldDir
}
}

func (s *registrySuite) TestFailAbortsRegistryTransaction(c *C) {
restore := s.mockRegistriesFeature(c)
defer restore()

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", "reason"}, 0)
c.Assert(err, ErrorMatches, i18n.G(`cannot use "snapctl fail" without enabling the "experimental.registries" feature`))
c.Check(stdout, IsNil)
c.Check(stderr, IsNil)

restore := s.mockRegistriesFeature(c)
defer restore()

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`))
MiguelPires marked this conversation as resolved.
Show resolved Hide resolved
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
Loading