Skip to content

Commit

Permalink
client,cmd/snap: introduce --user, --system and --users switches for …
Browse files Browse the repository at this point in the history
…snap service operations (#13368)

* client,cmd/snap: introduce --user, --system and --users switches for snap service operations

* client,o/servicestate: move Scope/UserSelection to client for reuse in client

* client,cmd/snap: improve handling of user and scope args

* NEWS: update news to reflect that we now support user daemons in start/stop/restart

* cmd/snap: some review feedback on allowed input

* t/main/services-user: add additional user to verify services are correctly affected

* cmd/snap: do not allow --system --user together, do not allow --users with =all

* tests,cmd: use --users=all in test, dont mark --users optional, enforce a value for it, add case for --system --users=all in spread test

* cmd/snap: add a comment for unreachable code, and correct a couple of messages
  • Loading branch information
Meulengracht authored Mar 13, 2024
1 parent c3fb515 commit 261d1c2
Show file tree
Hide file tree
Showing 11 changed files with 935 additions and 475 deletions.
3 changes: 2 additions & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
# In progress:
* Installation of local snap components
* Started improving support for user daemons by introducing new control switches --user/--users/--system for service operations
* Started support for snap services to show real status of user daemons

# Next:
* state: add support for notices (from pebble)
* daemon: add notices to the snapd API under `/v2/notices` and `/v2/notice`
* Mandatory device cgroup for all snaps using bare and core24 base as well as future bases
* Added API route for creating recovery systems: POST to `/v2/systems` with action `create`
* Added API route for removing recovery systems: POST to `/v2/systems/{label}` with action `remove`
* Support for user daemons by introducing new control switches --user/--system/--users for service start/stop/restart

# New in snapd 2.61.3:
* Install systemd files in correct location for 24.04
Expand Down
123 changes: 118 additions & 5 deletions client/apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"errors"
"fmt"
"net/url"
"os/user"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -183,13 +184,119 @@ func (client *Client) Logs(names []string, opts LogOptions) (<-chan Log, error)
return ch, nil
}

type UserSelection int

const (
UserSelectionList UserSelection = iota
UserSelectionSelf
UserSelectionAll
)

// UserSelector is a support structure for correctly translating a way of
// representing both a list of user-names, and specific keywords like "self"
// and "all" for JSON marshalling.
//
// When "Selector == UserSelectionList" then Names is used as the data source and
// the data is treated like a list of strings.
// When "Selector == UserSelectionSelf|UserSelectionAll", then the data source will
// be a single string that represent this in the form of "self|all".
type UserSelector struct {
Names []string
Selector UserSelection
}

// UserList returns a decoded list of users which takes any keyword into account.
// Takes the current user to be able to handle special keywords like 'user'.
func (us *UserSelector) UserList(currentUser *user.User) ([]string, error) {
switch us.Selector {
case UserSelectionList:
return us.Names, nil
case UserSelectionSelf:
if currentUser == nil {
return nil, fmt.Errorf(`internal error: for "self" the current user must be provided`)
}
if currentUser.Uid == "0" {
return nil, fmt.Errorf(`cannot use "self" for root user`)
}
return []string{currentUser.Username}, nil
case UserSelectionAll:
// Empty list indicates all.
return nil, nil
}
return nil, fmt.Errorf("internal error: unsupported selector %d specified", us.Selector)
}

func (us UserSelector) MarshalJSON() ([]byte, error) {
switch us.Selector {
case UserSelectionList:
return json.Marshal(us.Names)
case UserSelectionSelf:
return json.Marshal("self")
case UserSelectionAll:
return json.Marshal("all")
default:
return nil, fmt.Errorf("internal error: unsupported selector %d specified", us.Selector)
}
}

func (us *UserSelector) UnmarshalJSON(b []byte) error {
// Try treating it as a list of usernames first
var users []string
if err := json.Unmarshal(b, &users); err == nil {
us.Names = users
us.Selector = UserSelectionList
return nil
}

// Fallback to string, which would indicate a keyword
var s string
if err := json.Unmarshal(b, &s); err != nil {
return fmt.Errorf("cannot unmarshal, expected a string or a list of strings")
}

switch s {
case "self":
us.Selector = UserSelectionSelf
case "all":
us.Selector = UserSelectionAll
default:
return fmt.Errorf(`cannot unmarshal, expected one of: "self", "all"`)
}
return nil
}

type ScopeSelector []string

func (ss *ScopeSelector) UnmarshalJSON(b []byte) error {
var scopes []string
if err := json.Unmarshal(b, &scopes); err != nil {
return fmt.Errorf("cannot unmarshal, expected a list of strings")
}

if len(scopes) > 2 {
return fmt.Errorf("unexpected number of scopes: %v", scopes)
}

for _, s := range scopes {
switch s {
case "system", "user":
default:
return fmt.Errorf(`cannot unmarshal, expected one of: "system", "user"`)
}
}
*ss = scopes
return nil
}

// ErrNoNames is returned by Start, Stop, or Restart, when the given
// list of things on which to operate is empty.
var ErrNoNames = errors.New(`"names" must not be empty`)

type appInstruction struct {
Action string `json:"action"`
Names []string `json:"names"`
Action string `json:"action"`
Names []string `json:"names"`
Scope ScopeSelector `json:"scope,omitempty"`
Users UserSelector `json:"users,omitempty"`
StartOptions
StopOptions
RestartOptions
Expand All @@ -207,14 +314,16 @@ type StartOptions struct {
// It takes a list of names that can be snaps, of which all their
// services are started, or snap.service which are individual
// services to start; it shouldn't be empty.
func (client *Client) Start(names []string, opts StartOptions) (changeID string, err error) {
func (client *Client) Start(names []string, scope ScopeSelector, users UserSelector, opts StartOptions) (changeID string, err error) {
if len(names) == 0 {
return "", ErrNoNames
}

buf, err := json.Marshal(appInstruction{
Action: "start",
Names: names,
Scope: scope,
Users: users,
StartOptions: opts,
})
if err != nil {
Expand All @@ -235,14 +344,16 @@ type StopOptions struct {
// It takes a list of names that can be snaps, of which all their
// services are stopped, or snap.service which are individual
// services to stop; it shouldn't be empty.
func (client *Client) Stop(names []string, opts StopOptions) (changeID string, err error) {
func (client *Client) Stop(names []string, scope ScopeSelector, users UserSelector, opts StopOptions) (changeID string, err error) {
if len(names) == 0 {
return "", ErrNoNames
}

buf, err := json.Marshal(appInstruction{
Action: "stop",
Names: names,
Scope: scope,
Users: users,
StopOptions: opts,
})
if err != nil {
Expand All @@ -264,14 +375,16 @@ type RestartOptions struct {
// services are restarted, or snap.service which are individual
// services to restart; it shouldn't be empty. If the service is not
// running, starts it.
func (client *Client) Restart(names []string, opts RestartOptions) (changeID string, err error) {
func (client *Client) Restart(names []string, scope ScopeSelector, users UserSelector, opts RestartOptions) (changeID string, err error) {
if len(names) == 0 {
return "", ErrNoNames
}

buf, err := json.Marshal(appInstruction{
Action: "restart",
Names: names,
Scope: scope,
Users: users,
RestartOptions: opts,
})
if err != nil {
Expand Down
Loading

0 comments on commit 261d1c2

Please sign in to comment.