Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
177790a
Add optional arguments in Pebble layer schema
rebornplusplus Feb 1, 2023
4f0ea60
feat: Support --args functionality in CLI
rebornplusplus Feb 7, 2023
fde8729
Fix failing tests
rebornplusplus Feb 7, 2023
994d5fe
Point to go-flags fork
rebornplusplus Feb 7, 2023
14b98c8
Add comments for reference, fix a few minor things
rebornplusplus Feb 8, 2023
906b6df
Add tests for ``pebble run --args``
rebornplusplus Feb 8, 2023
de12f37
WIP: Address Ben's comments, Update README
rebornplusplus Feb 16, 2023
c467e33
WIP: Accept Ben's suggestions
rebornplusplus Feb 17, 2023
c4c0e57
Point to go-flags fork
rebornplusplus Feb 27, 2023
3cdd9fe
WIP: Address Gustavo's comments
rebornplusplus Mar 14, 2023
ba40a3a
WIP: Address Tomas' comments
rebornplusplus Mar 16, 2023
47995ae
WIP: Address Cris' comments
rebornplusplus Mar 30, 2023
d0427e9
Point to canonical/go-flags
rebornplusplus Mar 30, 2023
8e6ef62
Add new layer of service args on top of plan
rebornplusplus Apr 6, 2023
2d8ac71
Remove leftover newlines
rebornplusplus Apr 6, 2023
a511c6b
Merge remote-tracking branch 'upstream/master' into svc-args
rebornplusplus Apr 11, 2023
6369d78
Add label for the new --args layer on top
rebornplusplus May 3, 2023
0b7d3cc
Add tests for servstate.ServiceManager.SetServiceArgs
rebornplusplus May 3, 2023
241bdf7
Update TODO comment on reserved labels
rebornplusplus May 4, 2023
62af8fe
Include updated strutil/shlex commit in go.mod, go.sum
rebornplusplus May 11, 2023
d686b6f
update error messages and TODO comments
rebornplusplus May 16, 2023
53939c3
revert go.mod and go.sum to upstream/master
rebornplusplus May 16, 2023
43ef4ef
Update canonical/x-go module to newer version
rebornplusplus May 22, 2023
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
30 changes: 26 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,28 @@ are marked as `startup: enabled` (if you don't want that, use `--hold`). Then
other Pebble commands may be used to interact with the running daemon, for example,
in another terminal window.

To provide additional arguments to a service, use `--args <service> <args> ...`.
If the `command` field in the service's plan has a `[ <default-arguments...> ]`
list, the `--args` arguments will replace the defaults. If not, they will be
appended to the command.

To indicate the end of an `--args` list, use a `;` (semicolon) terminator,
which must be backslash-escaped if used in the shell. The terminator
may be omitted if there are no other Pebble options that follow.

For example:

```
# Start the daemon and pass additional arguments to "myservice".
$ pebble run --args myservice --verbose --foo "multi str arg"

# Use args terminator to pass --hold to Pebble at the end of the line.
$ pebble run --args myservice --verbose \; --hold

# Start the daemon and pass arguments to multiple services.
$ pebble run --args myservice1 --arg1 \; --args myservice2 --arg2
```

To override the default configuration directory, set the `PEBBLE` environment variable when running:

```
Expand Down Expand Up @@ -423,10 +445,10 @@ services:
# override the existing service spec in the plan with the same name.
override: merge | replace

# (Required in combined layer) The command to run the service. The
# command is executed directly, not interpreted by a shell.
#
# Example: /usr/bin/somecommand -b -t 30
# (Required in combined layer) The command to run the service. It is executed
# directly, not interpreted by a shell, and may be optionally suffixed by default
# arguments within "[" and "]" which may be overriden via --args.
# Example: /usr/bin/somedaemon --db=/db/path [ --port 8080 ]
command: <commmand>

# (Optional) A short summary of the service.
Expand Down
46 changes: 42 additions & 4 deletions cmd/pebble/cmd_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,29 @@ import (
var shortRunHelp = "Run the pebble environment"
var longRunHelp = `
The run command starts pebble and runs the configured environment.

Additional arguments may be provided to the service command with the --args option, which
must be terminated with ";" unless there are no further Pebble options. These arguments
are appended to the end of the service command, and replace any default arguments defined
in the service plan. For example:

$ pebble run --args myservice --port 8080 \; --hold
`

type sharedRunEnterOpts struct {
CreateDirs bool `long:"create-dirs"`
Hold bool `long:"hold"`
HTTP string `long:"http"`
Verbose bool `short:"v" long:"verbose"`
CreateDirs bool `long:"create-dirs"`
Hold bool `long:"hold"`
HTTP string `long:"http"`
Verbose bool `short:"v" long:"verbose"`
Args [][]string `long:"args" terminator:";"`
}

var sharedRunEnterOptsHelp = map[string]string{
"create-dirs": "Create pebble directory on startup if it doesn't exist",
"hold": "Do not start default services automatically",
"http": `Start HTTP API listening on this address (e.g., ":4000")`,
"verbose": "Log all output from services to stdout",
"args": `Provide additional arguments to a service`,
}

type cmdRun struct {
Expand Down Expand Up @@ -149,6 +158,16 @@ func runDaemon(rcmd *cmdRun, ch chan os.Signal, ready chan<- func()) error {
return err
}

if rcmd.Args != nil {
mappedArgs, err := convertArgs(rcmd.Args)
if err != nil {
return err
}
if err := d.SetServiceArgs(mappedArgs); err != nil {
return err
}
}

// Run sanity check now, if anything goes wrong with the
// check we go into "degraded" mode where we always report
// the given error to any client.
Expand Down Expand Up @@ -217,3 +236,22 @@ out:

return d.Stop(ch)
}

// convert args from [][]string type to map[string][]string
// and check for empty or duplicated --args usage
func convertArgs(args [][]string) (map[string][]string, error) {
mappedArgs := make(map[string][]string)

for _, arg := range args {
if len(arg) < 1 {
return nil, fmt.Errorf("--args requires a service name")
}
name := arg[0]
if _, ok := mappedArgs[name]; ok {
return nil, fmt.Errorf("--args provided more than once for %q service", name)
}
mappedArgs[name] = arg[1:]
}

return mappedArgs, nil
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.14

require (
github.com/canonical/go-flags v0.0.0-20230403090104-105d09a091b8
github.com/canonical/x-go v0.0.0-20230113154138-0ccdb0b57a43
github.com/canonical/x-go v0.0.0-20230522092633-7947a7587f5b
github.com/gorilla/mux v1.8.0
github.com/gorilla/websocket v1.4.2
github.com/pkg/term v1.1.0
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
github.com/canonical/go-flags v0.0.0-20230403090104-105d09a091b8 h1:zGaJEJI9qPVyM+QKFJagiyrM91Ke5S9htoL1D470g6E=
github.com/canonical/go-flags v0.0.0-20230403090104-105d09a091b8/go.mod h1:ZZFeR9K9iGgpwOaLYF9PdT44/+lfSJ9sQz3B+SsGsYU=
github.com/canonical/x-go v0.0.0-20230113154138-0ccdb0b57a43 h1:bey1JgA3D2EBabr2a7kWKj+JlEPxX1akv8rcRromotA=
github.com/canonical/x-go v0.0.0-20230113154138-0ccdb0b57a43/go.mod h1:A0/Jvt7qKuCDss37TYRNXSscVyS+tLWM5kBYipQOpWQ=
github.com/canonical/x-go v0.0.0-20230522092633-7947a7587f5b h1:Da2fardddn+JDlVEYtrzBLTtyzoyU3nIS0Cf0GvjmwU=
github.com/canonical/x-go v0.0.0-20230522092633-7947a7587f5b/go.mod h1:upTK9n6rlqITN9rCN69hdreI37dRDFUk2thlGGD5Cg8=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
Expand Down Expand Up @@ -31,7 +31,7 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 h1:yiW+nvdHb9LVqSHQBXfZCieqV4fzYhNBql77zY0ykqs=
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637/go.mod h1:BHsqpu/nsuzkT5BpiH1EMZPLyqSMM8JbIavyFACoFNk=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
6 changes: 6 additions & 0 deletions internal/daemon/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,12 @@ func (d *Daemon) RebootIsMissing(st *state.State) error {
return errExpectedReboot
}

// SetServiceArgs sets the provided service arguments to their respective services,
// by passing the arguments to the service manager responsible under daemon overlord.
func (d *Daemon) SetServiceArgs(serviceArgs map[string][]string) error {
return d.overlord.ServiceManager().SetServiceArgs(serviceArgs)
}

func New(opts *Options) (*Daemon, error) {
d := &Daemon{
pebbleDir: opts.Dir,
Expand Down
8 changes: 3 additions & 5 deletions internal/overlord/servstate/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"syscall"
"time"

"github.com/canonical/x-go/strutil/shlex"
"golang.org/x/sys/unix"
"gopkg.in/tomb.v2"

Expand Down Expand Up @@ -334,12 +333,11 @@ func logError(err error) {
// command. It assumes the caller has ensures the service is in a valid state,
// and it sets s.cmd and other relevant fields.
func (s *serviceData) startInternal() error {
args, err := shlex.Split(s.config.Command)
base, extra, err := s.config.ParseCommand()
if err != nil {
// Shouldn't happen as it should have failed on parsing, but
// it does not hurt to double check and report.
return fmt.Errorf("cannot parse service command: %s", err)
return err
}
args := append(base, extra...)
s.cmd = exec.Command(args[0], args[1:]...)
s.cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}

Expand Down
35 changes: 35 additions & 0 deletions internal/overlord/servstate/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,41 @@ func (m *ServiceManager) CheckFailed(name string) {
}
}

// SetServiceArgs sets the service arguments provided by "pebble run --args"
// to their respective services. It adds a new layer in the plan, the layer
// consisting of services with commands having their arguments changed.
func (m *ServiceManager) SetServiceArgs(serviceArgs map[string][]string) error {
releasePlan, err := m.acquirePlan()
if err != nil {
return err
}
defer releasePlan()

newLayer := &plan.Layer{
// TODO: Consider making any labels starting with
// the "pebble-" prefix reserved.
Label: "pebble-service-args",
Services: make(map[string]*plan.Service),
}

for name, args := range serviceArgs {
service, ok := m.plan.Services[name]
if !ok {
return fmt.Errorf("service %q not found in plan", name)
}
base, _, err := service.ParseCommand()
if err != nil {
return err
}
newLayer.Services[name] = &plan.Service{
Override: plan.MergeOverride,
Command: plan.CommandString(base, args),
}
}

return m.appendLayer(newLayer)
}

// servicesToStop returns a slice of service names to stop, in dependency order.
func servicesToStop(m *ServiceManager) ([]string, error) {
releasePlan, err := m.acquirePlan()
Expand Down
48 changes: 48 additions & 0 deletions internal/overlord/servstate/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -851,6 +851,54 @@ services:
s.planLayersHasLen(c, manager, 3)
}

func (s *S) TestSetServiceArgs(c *C) {
dir := c.MkDir()
os.Mkdir(filepath.Join(dir, "layers"), 0755)
runner := state.NewTaskRunner(s.st)
manager, err := servstate.NewManager(s.st, runner, dir, nil, nil, fakeLogManager{})
c.Assert(err, IsNil)
defer manager.Stop()

// Append a layer with a few services having default args.
layer := parseLayer(c, 0, "base-layer", `
services:
svc1:
override: replace
command: foo [ --bar ]
svc2:
override: replace
command: foo
svc3:
override: replace
command: foo
`)
err = manager.AppendLayer(layer)
c.Assert(err, IsNil)
c.Assert(layer.Order, Equals, 1)
s.planLayersHasLen(c, manager, 1)

// Set arguments to services.
serviceArgs := map[string][]string{
"svc1": {"-abc", "--xyz"},
"svc2": {"--bar"},
}
err = manager.SetServiceArgs(serviceArgs)
c.Assert(err, IsNil)
c.Assert(planYAML(c, manager), Equals, `
services:
svc1:
override: replace
command: foo [ -abc --xyz ]
svc2:
override: replace
command: foo [ --bar ]
svc3:
override: replace
command: foo
`[1:])
s.planLayersHasLen(c, manager, 2)
}

func (s *S) TestServices(c *C) {
started := time.Now()
services, err := s.manager.Services(nil)
Expand Down
61 changes: 60 additions & 1 deletion internal/plan/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,65 @@ func (s *Service) Equal(other *Service) bool {
return reflect.DeepEqual(s, other)
}

// ParseCommand returns a service command as two stream of strings.
// The base command is returned as a stream and the default arguments
// in [ ... ] group is returned as another stream.
func (s *Service) ParseCommand() (base, extra []string, err error) {
defer func() {
if err != nil {
err = fmt.Errorf("cannot parse service %q command: %w", s.Name, err)
}
}()

args, err := shlex.Split(s.Command)
if err != nil {
return nil, nil, err
}

var inBrackets, gotBrackets bool

for idx, arg := range args {
if inBrackets {
if arg == "[" {
return nil, nil, fmt.Errorf("cannot nest [ ... ] groups")
}
if arg == "]" {
inBrackets = false
continue
}
extra = append(extra, arg)
continue
}
if gotBrackets {
return nil, nil, fmt.Errorf("cannot have any arguments after [ ... ] group")
}
if arg == "[" {
if idx == 0 {
return nil, nil, fmt.Errorf("cannot start command with [ ... ] group")
}
inBrackets = true
gotBrackets = true
continue
}
if arg == "]" {
return nil, nil, fmt.Errorf("cannot have ] outside of [ ... ] group")
}
base = append(base, arg)
}

return base, extra, nil
}

// CommandString returns a service command as a string after
// appending the arguments in "extra" to the command in "base"
func CommandString(base, extra []string) string {
output := shlex.Join(base)
if len(extra) > 0 {
output = output + " [ " + shlex.Join(extra) + " ]"
}
return output
}

// LogsTo returns true if the logs from s should be forwarded to target t.
// This happens if:
// - t.Selection is "opt-out" or empty, and s.LogTargets is empty; or
Expand Down Expand Up @@ -603,7 +662,7 @@ func CombineLayers(layers ...*Layer) (*Layer, error) {
Message: fmt.Sprintf(`plan must define "command" for service %q`, name),
}
}
_, err := shlex.Split(service.Command)
_, _, err := service.ParseCommand()
if err != nil {
return nil, &FormatError{
Message: fmt.Sprintf("plan service %q command invalid: %v", name, err),
Expand Down
Loading