diff --git a/README.md b/README.md index a0fe9e3e..f0ab8e42 100644 --- a/README.md +++ b/README.md @@ -523,6 +523,10 @@ services: # group and group-id are specified, the group's GID must match group-id. group-id: + # (Optional) Working directory to run command in. By default, the + # command is run in the service manager's current directory. + working-dir: + # (Optional) Defines what happens when the service exits with a zero # exit code. Possible values are: "restart" (default) which restarts # the service after the backoff delay, "shutdown" which shuts down and @@ -653,7 +657,8 @@ checks: # match group-id. group-id: - # (Optional) Working directory to run command in. + # (Optional) Working directory to run command in. By default, the + # command is run in the service manager's current directory. working-dir: ``` diff --git a/internals/overlord/servstate/handlers.go b/internals/overlord/servstate/handlers.go index 1beb7d50..32348a25 100644 --- a/internals/overlord/servstate/handlers.go +++ b/internals/overlord/servstate/handlers.go @@ -347,6 +347,8 @@ func (s *serviceData) startInternal() error { environment[k] = v } + s.cmd.Dir = s.config.WorkingDir + // Start as another user if specified in plan. uid, gid, err := osutil.NormalizeUidGid(s.config.UserID, s.config.GroupID, s.config.User, s.config.Group) if err != nil { diff --git a/internals/overlord/servstate/manager_test.go b/internals/overlord/servstate/manager_test.go index 58a4d920..629cf029 100644 --- a/internals/overlord/servstate/manager_test.go +++ b/internals/overlord/servstate/manager_test.go @@ -1763,3 +1763,62 @@ type fakeLogManager struct{} func (f fakeLogManager) ServiceStarted(serviceName string, logs *servicelog.RingBuffer) { // no-op } + +func (s *S) TestNoWorkingDir(c *C) { + // Service command should run in current directory (package directory) + // if "working-dir" config option not set. + dir := c.MkDir() + err := os.Mkdir(filepath.Join(dir, "layers"), 0755) + c.Assert(err, IsNil) + manager, err := servstate.NewManager(s.st, s.runner, dir, nil, nil, fakeLogManager{}) + c.Assert(err, IsNil) + defer manager.Stop() + + outputPath := filepath.Join(dir, "output") + layer := parseLayer(c, 0, "layer", fmt.Sprintf(` +services: + nowrkdir: + override: replace + command: /bin/sh -c "pwd >%s; sleep %g" +`, outputPath, shortOkayDelay.Seconds()+0.01)) + err = manager.AppendLayer(layer) + c.Assert(err, IsNil) + + chg := s.startServices(c, []string{"nowrkdir"}, 1) + s.st.Lock() + c.Assert(chg.Err(), IsNil) + s.st.Unlock() + + output, err := ioutil.ReadFile(outputPath) + c.Assert(err, IsNil) + c.Check(string(output), Matches, ".*/overlord/servstate\n") +} + +func (s *S) TestWorkingDir(c *C) { + dir := c.MkDir() + err := os.Mkdir(filepath.Join(dir, "layers"), 0755) + c.Assert(err, IsNil) + manager, err := servstate.NewManager(s.st, s.runner, dir, nil, nil, fakeLogManager{}) + c.Assert(err, IsNil) + defer manager.Stop() + + outputPath := filepath.Join(dir, "output") + layer := parseLayer(c, 0, "layer", fmt.Sprintf(` +services: + wrkdir: + override: replace + command: /bin/sh -c "pwd >%s; sleep %g" + working-dir: %s +`, outputPath, shortOkayDelay.Seconds()+0.01, dir)) + err = manager.AppendLayer(layer) + c.Assert(err, IsNil) + + chg := s.startServices(c, []string{"wrkdir"}, 1) + s.st.Lock() + c.Assert(chg.Err(), IsNil) + s.st.Unlock() + + output, err := ioutil.ReadFile(outputPath) + c.Assert(err, IsNil) + c.Check(string(output), Equals, dir+"\n") +} diff --git a/internals/plan/plan.go b/internals/plan/plan.go index d142e184..76fc1feb 100644 --- a/internals/plan/plan.go +++ b/internals/plan/plan.go @@ -79,6 +79,7 @@ type Service struct { User string `yaml:"user,omitempty"` GroupID *int `yaml:"group-id,omitempty"` Group string `yaml:"group,omitempty"` + WorkingDir string `yaml:"working-dir,omitempty"` // Auto-restart and backoff functionality OnSuccess ServiceAction `yaml:"on-success,omitempty"` @@ -154,6 +155,9 @@ func (s *Service) Merge(other *Service) { if other.Group != "" { s.Group = other.Group } + if other.WorkingDir != "" { + s.WorkingDir = other.WorkingDir + } s.After = append(s.After, other.After...) s.Before = append(s.Before, other.Before...) s.Requires = append(s.Requires, other.Requires...) diff --git a/internals/plan/plan_test.go b/internals/plan/plan_test.go index bc76513a..2fe9f6e7 100644 --- a/internals/plan/plan_test.go +++ b/internals/plan/plan_test.go @@ -107,12 +107,14 @@ var planTests = []planTest{{ backoff-delay: 1s backoff-factor: 1.5 backoff-limit: 10s + working-dir: /workdir/srv1 srv2: override: replace startup: enabled command: cmd before: - srv3 + working-dir: /workdir/srv2 srv3: override: replace command: cmd @@ -131,6 +133,7 @@ var planTests = []planTest{{ - srv4 before: - srv5 + working-dir: /workdir/srv1/override srv2: override: replace startup: disabled @@ -173,16 +176,18 @@ var planTests = []planTest{{ "var0": "val0", "var2": "val2", }, + WorkingDir: "/workdir/srv1", BackoffDelay: plan.OptionalDuration{Value: time.Second, IsSet: true}, BackoffFactor: plan.OptionalFloat{Value: 1.5, IsSet: true}, BackoffLimit: plan.OptionalDuration{Value: 10 * time.Second, IsSet: true}, }, "srv2": { - Name: "srv2", - Override: "replace", - Command: "cmd", - Startup: plan.StartupEnabled, - Before: []string{"srv3"}, + Name: "srv2", + Override: "replace", + Command: "cmd", + WorkingDir: "/workdir/srv2", + Startup: plan.StartupEnabled, + Before: []string{"srv3"}, }, "srv3": { Name: "srv3", @@ -213,6 +218,7 @@ var planTests = []planTest{{ Environment: map[string]string{ "var3": "val3", }, + WorkingDir: "/workdir/srv1/override", }, "srv2": { Name: "srv2", @@ -269,6 +275,7 @@ var planTests = []planTest{{ "var2": "val2", "var3": "val3", }, + WorkingDir: "/workdir/srv1/override", BackoffDelay: plan.OptionalDuration{Value: time.Second, IsSet: true}, BackoffFactor: plan.OptionalFloat{Value: 1.5, IsSet: true}, BackoffLimit: plan.OptionalDuration{Value: 10 * time.Second, IsSet: true},