Skip to content

Commit

Permalink
Add exec support for service context and "pebble exec --context" arg
Browse files Browse the repository at this point in the history
  • Loading branch information
benhoyt committed Jun 26, 2023
1 parent 0abe106 commit 7a0a68f
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 3 deletions.
8 changes: 8 additions & 0 deletions client/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ type ExecOptions struct {
// Required: command and arguments (first element is the executable).
Command []string

// Optional service name to run the command in the context of this service,
// that is, inherit its environment variables, user/group settings,
// and working directory. The other options in this struct will override
// the service context; Environment will be merged on top of the service's.
Context string

// Optional environment variables.
Environment map[string]string

Expand Down Expand Up @@ -75,6 +81,7 @@ type ExecOptions struct {

type execPayload struct {
Command []string `json:"command"`
Context string `json:"context,omitempty"`
Environment map[string]string `json:"environment,omitempty"`
WorkingDir string `json:"working-dir,omitempty"`
Timeout string `json:"timeout,omitempty"`
Expand Down Expand Up @@ -123,6 +130,7 @@ func (client *Client) Exec(opts *ExecOptions) (*ExecProcess, error) {
}
payload := execPayload{
Command: opts.Command,
Context: opts.Context,
Environment: opts.Environment,
WorkingDir: opts.WorkingDir,
Timeout: timeoutStr,
Expand Down
3 changes: 3 additions & 0 deletions internals/cli/cmd_exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type cmdExec struct {
GroupID *int `long:"gid"`
Group string `long:"group"`
Timeout time.Duration `long:"timeout"`
Context string `long:"context"`
Terminal bool `short:"t"`
NoTerminal bool `short:"T"`
Interactive bool `short:"i"`
Expand All @@ -56,6 +57,7 @@ var execDescs = map[string]string{
"gid": "Group ID to run command as",
"group": "Group name to run command as (group's GID must match gid if both present)",
"timeout": "Timeout after which to terminate command",
"context": "Run the command in the context of this service (overridden by -w, --env, --user, --uid, --group, --gid)",
"t": "Allocate remote pseudo-terminal and connect stdout to it (default if stdout is a TTY)",
"T": "Disable remote pseudo-terminal allocation",
"i": "Interactive mode: connect stdin to the pseudo-terminal (default if stdin and stdout are TTYs)",
Expand Down Expand Up @@ -144,6 +146,7 @@ func (cmd *cmdExec) Execute(args []string) error {

opts := &client.ExecOptions{
Command: command,
Context: cmd.Context,
Environment: env,
WorkingDir: cmd.WorkingDir,
Timeout: cmd.Timeout,
Expand Down
25 changes: 22 additions & 3 deletions internals/daemon/api_exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ import (
"github.com/canonical/pebble/internals/osutil"
"github.com/canonical/pebble/internals/overlord/cmdstate"
"github.com/canonical/pebble/internals/overlord/state"
"github.com/canonical/pebble/internals/plan"
)

type execPayload struct {
Command []string `json:"command"`
Context string `json:"context"`
Environment map[string]string `json:"environment"`
WorkingDir string `json:"working-dir"`
Timeout string `json:"timeout"`
Expand Down Expand Up @@ -67,8 +69,25 @@ func v1PostExec(c *Command, req *http.Request, _ *userState) Response {
return statusBadRequest("%v", err)
}

p, err := c.d.overlord.ServiceManager().Plan()
if err != nil {
return statusBadRequest("%v", err)
}
overrides := plan.ContextOptions{
Environment: payload.Environment,
UserID: payload.UserID,
User: payload.User,
GroupID: payload.GroupID,
Group: payload.Group,
WorkingDir: payload.WorkingDir,
}
merged, err := plan.MergeServiceContext(p, payload.Context, overrides)
if err != nil {
return statusBadRequest("%v", err)
}

// Convert User/UserID and Group/GroupID combinations into raw uid/gid.
uid, gid, err := osutil.NormalizeUidGid(payload.UserID, payload.GroupID, payload.User, payload.Group)
uid, gid, err := osutil.NormalizeUidGid(merged.UserID, merged.GroupID, merged.User, merged.Group)
if err != nil {
return statusBadRequest("%v", err)
}
Expand All @@ -79,8 +98,8 @@ func v1PostExec(c *Command, req *http.Request, _ *userState) Response {

args := &cmdstate.ExecArgs{
Command: payload.Command,
Environment: payload.Environment,
WorkingDir: payload.WorkingDir,
Environment: merged.Environment,
WorkingDir: merged.WorkingDir,
Timeout: timeout,
UserID: uid,
GroupID: gid,
Expand Down
49 changes: 49 additions & 0 deletions internals/daemon/api_exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (

"github.com/canonical/pebble/client"
"github.com/canonical/pebble/internals/logger"
"github.com/canonical/pebble/internals/plan"
)

var _ = Suite(&execSuite{})
Expand Down Expand Up @@ -164,6 +165,54 @@ func (s *execSuite) TestTimeout(c *C) {
c.Check(stderr, Equals, "")
}

func (s *execSuite) TestContextNoOverrides(c *C) {
dir := c.MkDir()
err := s.daemon.overlord.ServiceManager().AppendLayer(&plan.Layer{
Label: "layer1",
Services: map[string]*plan.Service{"svc1": {
Name: "svc1",
Override: "replace",
Command: "dummy",
Environment: map[string]string{"FOO": "foo", "BAR": "bar"},
WorkingDir: dir,
}},
})
c.Assert(err, IsNil)

stdout, stderr, err := s.exec(c, "", &client.ExecOptions{
Command: []string{"/bin/sh", "-c", "echo FOO=$FOO BAR=$BAR; pwd"},
Context: "svc1",
})
c.Assert(err, IsNil)
c.Check(stdout, Equals, "FOO=foo BAR=bar\n"+dir+"\n")
c.Check(stderr, Equals, "")
}

func (s *execSuite) TestContextOverrides(c *C) {
err := s.daemon.overlord.ServiceManager().AppendLayer(&plan.Layer{
Label: "layer1",
Services: map[string]*plan.Service{"svc1": {
Name: "svc1",
Override: "replace",
Command: "dummy",
Environment: map[string]string{"FOO": "foo", "BAR": "bar"},
WorkingDir: c.MkDir(),
}},
})
c.Assert(err, IsNil)

overrideDir := c.MkDir()
stdout, stderr, err := s.exec(c, "", &client.ExecOptions{
Command: []string{"/bin/sh", "-c", "echo FOO=$FOO BAR=$BAR; pwd"},
Context: "svc1",
Environment: map[string]string{"FOO": "oof"},
WorkingDir: overrideDir,
})
c.Assert(err, IsNil)
c.Check(stdout, Equals, "FOO=oof BAR=bar\n"+overrideDir+"\n")
c.Check(stderr, Equals, "")
}

func (s *execSuite) TestCurrentUserGroup(c *C) {
current, err := user.Current()
c.Assert(err, IsNil)
Expand Down

0 comments on commit 7a0a68f

Please sign in to comment.