From f80ee05c34210f62c2264a6a42ac5aaa0bb3483b Mon Sep 17 00:00:00 2001 From: Michael Woolnough Date: Thu, 22 Jan 2026 15:34:12 +0000 Subject: [PATCH 1/5] Add option to run executable with a different group --- cmd/add.go | 7 ++++++- jobqueue/client.go | 11 ++++++++++- jobqueue/job.go | 4 ++++ jobqueue/serverCLI.go | 1 + jobqueue/serverREST.go | 8 ++++++++ 5 files changed, 29 insertions(+), 2 deletions(-) diff --git a/cmd/add.go b/cmd/add.go index 8ab90d58..d81a1bf2 100644 --- a/cmd/add.go +++ b/cmd/add.go @@ -66,6 +66,7 @@ var ( cmdCwdMatters bool cmdChangeHome bool cmdRepGroup string + cmdGroup string cmdLimitGroups string cmdModules string cmdDepGroups string @@ -175,6 +176,9 @@ a flag to disable this check: --disable_relative_check. the $HOME environment variable to the actual command working directory before running the cmd. +"group" specifies which group the command should run as; if no value is set, the +users default group is used. + "on_failure" determines what behaviours are triggered if your cmd exits non-0. Behaviours are described using an array of objects, where each object has a key corresponding to the name of the desired behaviour, and the relevant value. The @@ -568,7 +572,7 @@ func init() { addCmd.Flags().BoolVar(&cmdBsubMode, "bsub", false, "enable bsub emulation mode") addCmd.Flags().BoolVar(&cmdDisableRelativeCheck, "disable_relative_check", false, "disable the relative path checking when cwd_matters is false") - + addCmd.Flags().StringVar(&cmdGroup, "group", "", "group to start the command as") addCmd.Flags().IntVar(&timeoutint, "timeout", 120, "how long (seconds) to wait to get a reply from 'wr manager'") addCmd.Flags().IntVar(&rtimeoutint, "reserve_timeout", 1, "how long (seconds) to wait before a runner exits when there is no more work'") addCmd.Flags().BoolVarP(&simpleOutput, "simple", "s", false, "simplify output to only queued job ids") @@ -629,6 +633,7 @@ func parseCmdFile(jq *jobqueue.Client, diskSet bool) ([]*jobqueue.Job, bool, boo jd := &jobqueue.JobDefaults{ RepGrp: cmdRepGroup, ReqGrp: reqGroup, + Group: cmdGroup, Cwd: cmdCwd, CwdMatters: cmdCwdMatters, ChangeHome: cmdChangeHome, diff --git a/jobqueue/client.go b/jobqueue/client.go index 797ea801..ce4ef168 100644 --- a/jobqueue/client.go +++ b/jobqueue/client.go @@ -561,7 +561,15 @@ func (c *Client) Execute(ctx context.Context, job *Job, shell string) error { if strings.Contains(jc, " | ") { jc = "set -o pipefail; " + jc } - cmd := exec.Command(shell, "-c", jc) // #nosec Our whole purpose is to allow users to run arbitrary commands via us... + + var cmd *exec.Cmd + + if job.Group != "" { + cmd = exec.Command("newgrp", job.Group) + cmd.Stdin = strings.NewReader(jc) + } else { + cmd = exec.Command(shell, "-c", jc) // #nosec Our whole purpose is to allow users to run arbitrary commands via us... + } // we'll filter STDERR/OUT of the cmd to keep only the first and last line // of any contiguous block of \r terminated lines (to mostly eliminate @@ -851,6 +859,7 @@ func (c *Client) Execute(ctx context.Context, job *Job, shell string) error { "LSF_LIBDIR=/dev/null", "LSF_ENVDIR=/dev/null", "LSF_BINDIR=" + prependPath, + "SHELL=" + shell, }) } cmd.Env = env diff --git a/jobqueue/job.go b/jobqueue/job.go index 77657005..bc2894eb 100644 --- a/jobqueue/job.go +++ b/jobqueue/job.go @@ -132,6 +132,10 @@ type Job struct { // you expect to have similar resource requirements. ReqGroup string + // Group is the group name to run the executable as; a value of empty string + // will use the default group. + Group string + // Requirements describes the resources this Cmd needs to run, such as RAM, // Disk and time. These may be determined for you by the system (depending // on Override) based on past experience of running jobs with the same diff --git a/jobqueue/serverCLI.go b/jobqueue/serverCLI.go index f30173eb..a34e4e66 100644 --- a/jobqueue/serverCLI.go +++ b/jobqueue/serverCLI.go @@ -786,6 +786,7 @@ func (s *Server) itemToJob(ctx context.Context, item *queue.Item, getStd bool, g job := &Job{ RepGroup: sjob.RepGroup, ReqGroup: sjob.ReqGroup, + Group: sjob.Group, LimitGroups: sjob.LimitGroups, Modules: sjob.Modules, DepGroups: sjob.DepGroups, diff --git a/jobqueue/serverREST.go b/jobqueue/serverREST.go index a0b8f08c..2fa56c21 100644 --- a/jobqueue/serverREST.go +++ b/jobqueue/serverREST.go @@ -69,6 +69,7 @@ type JobViaJSON struct { Cmd string `json:"cmd"` Cwd string `json:"cwd"` ReqGrp string `json:"req_grp"` + Group string `json:"group"` // Memory is a number and unit suffix, eg. 1G for 1 Gigabyte. Memory string `json:"memory"` // Time is a duration with a unit suffix, eg. 1h for 1 hour. @@ -114,6 +115,7 @@ type JobDefaults struct { MountConfigs MountConfigs compressedEnv []byte RepGrp string + Group string // Cwd defaults to /tmp. Cwd string ReqGrp string @@ -270,6 +272,11 @@ func (jvj *JobViaJSON) Convert(jd *JobDefaults) (*Job, error) { rg = jvj.ReqGrp } + group := jd.Group + if jvj.Group != "" { + group = jvj.Group + } + if jvj.CPUs == nil { cpus = jd.DefaultCPUs() } else { @@ -517,6 +524,7 @@ func (jvj *JobViaJSON) Convert(jd *JobDefaults) (*Job, error) { CwdMatters: cwdMatters, ChangeHome: changeHome, ReqGroup: rg, + Group: group, Requirements: &jqs.Requirements{RAM: mb, Time: dur, Cores: cpus, Disk: disk, DiskSet: diskSet, Other: other}, Override: uint8(override), Priority: uint8(priority), From 8d7744c77035c1738d716eafba364f39514660fc Mon Sep 17 00:00:00 2001 From: Michael Woolnough Date: Thu, 22 Jan 2026 15:48:49 +0000 Subject: [PATCH 2/5] Delint'd --- jobqueue/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jobqueue/client.go b/jobqueue/client.go index ce4ef168..3c2ed723 100644 --- a/jobqueue/client.go +++ b/jobqueue/client.go @@ -565,7 +565,7 @@ func (c *Client) Execute(ctx context.Context, job *Job, shell string) error { var cmd *exec.Cmd if job.Group != "" { - cmd = exec.Command("newgrp", job.Group) + cmd = exec.Command("newgrp", job.Group) //nolint:gosec cmd.Stdin = strings.NewReader(jc) } else { cmd = exec.Command(shell, "-c", jc) // #nosec Our whole purpose is to allow users to run arbitrary commands via us... From 21d65f642f6a2d12ead66ec376b640316730646b Mon Sep 17 00:00:00 2001 From: Michael Woolnough Date: Thu, 22 Jan 2026 15:56:01 +0000 Subject: [PATCH 3/5] Add test for running as a different group --- jobqueue/jobqueue_test.go | 46 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/jobqueue/jobqueue_test.go b/jobqueue/jobqueue_test.go index ad360d19..79101af5 100644 --- a/jobqueue/jobqueue_test.go +++ b/jobqueue/jobqueue_test.go @@ -30,6 +30,7 @@ import ( "math" "os" "os/exec" + "os/user" "path/filepath" "runtime" "strconv" @@ -1300,6 +1301,51 @@ func TestJobqueueBasics(t *testing.T) { So(stdout, ShouldEqual, "c\nd") }) + Convey("You can execute a job as a different group", func() { + server.racmutex.Lock() + server.rc = "" + server.racmutex.Unlock() + + groups, err := os.Getgroups() + So(err, ShouldBeNil) + So(len(groups), ShouldBeGreaterThan, 1) + + second, err := user.LookupGroupId(strconv.Itoa(groups[1])) + So(err, ShouldBeNil) + + inserts, already, err := jq.Add([]*Job{ + {Cmd: "id", Cwd: t.TempDir(), Requirements: standardReqs, RepGroup: "manually_added"}, + {Cmd: "id ", Group: second.Name, Cwd: t.TempDir(), Requirements: standardReqs, RepGroup: "manually_added"}, + }, []string{}, true) + So(err, ShouldBeNil) + So(inserts, ShouldEqual, 2) + So(already, ShouldEqual, 0) + + job, err := jq.Reserve(0) + So(err, ShouldBeNil) + So(job, ShouldNotBeNil) + + err = jq.Execute(ctx, job, config.RunnerExecShell) + So(err, ShouldBeNil) + + stdoutA, err := job.StdOut() + So(err, ShouldBeNil) + So(stdoutA, ShouldNotBeEmpty) + + job, err = jq.Reserve(0) + So(err, ShouldBeNil) + So(job, ShouldNotBeNil) + + err = jq.Execute(ctx, job, config.RunnerExecShell) + So(err, ShouldBeNil) + + stdoutB, err := job.StdOut() + So(err, ShouldBeNil) + So(stdoutB, ShouldNotBeEmpty) + + So(stdoutA, ShouldNotEqual, stdoutB) + }) + Convey("You can stop the server by sending it a SIGTERM or SIGINT", func() { err := jq.Disconnect() So(err, ShouldBeNil) From ca52b309a506f44496f50ed96969c0414ae80bae Mon Sep 17 00:00:00 2001 From: Michael Woolnough Date: Thu, 22 Jan 2026 16:02:31 +0000 Subject: [PATCH 4/5] Changes following review --- cmd/add.go | 6 +++--- cmd/mod.go | 5 +++++ jobqueue/job.go | 10 ++++++++++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/cmd/add.go b/cmd/add.go index d81a1bf2..ac9922c6 100644 --- a/cmd/add.go +++ b/cmd/add.go @@ -176,8 +176,8 @@ a flag to disable this check: --disable_relative_check. the $HOME environment variable to the actual command working directory before running the cmd. -"group" specifies which group the command should run as; if no value is set, the -users default group is used. +"group" specifies which unix group the command should run as; if no value is +set, the users default unix group is used. "on_failure" determines what behaviours are triggered if your cmd exits non-0. Behaviours are described using an array of objects, where each object has a key @@ -572,7 +572,7 @@ func init() { addCmd.Flags().BoolVar(&cmdBsubMode, "bsub", false, "enable bsub emulation mode") addCmd.Flags().BoolVar(&cmdDisableRelativeCheck, "disable_relative_check", false, "disable the relative path checking when cwd_matters is false") - addCmd.Flags().StringVar(&cmdGroup, "group", "", "group to start the command as") + addCmd.Flags().StringVar(&cmdGroup, "group", "", "unix group to start the command as") addCmd.Flags().IntVar(&timeoutint, "timeout", 120, "how long (seconds) to wait to get a reply from 'wr manager'") addCmd.Flags().IntVar(&rtimeoutint, "reserve_timeout", 1, "how long (seconds) to wait before a runner exits when there is no more work'") addCmd.Flags().BoolVarP(&simpleOutput, "simple", "s", false, "simplify output to only queued job ids") diff --git a/cmd/mod.go b/cmd/mod.go index 8652095e..969d600f 100644 --- a/cmd/mod.go +++ b/cmd/mod.go @@ -173,6 +173,10 @@ new internal ids is printed.`, jm.SetReqGroup(reqGroup) } + if cobraCmd.Flags().Changed("group") { + jm.SetUnixGroup(cmdGroup) + } + req := &jqs.Requirements{} var setReq bool if cobraCmd.Flags().Changed("memory") { @@ -422,6 +426,7 @@ func init() { modCmd.Flags().StringVar(&cmdCloudConfigs, "cloud_config_files", "", "in the cloud, comma separated paths of config files to copy to servers created to run these commands") modCmd.Flags().BoolVar(&cmdCloudSharedDisk, "cloud_shared", false, "mount /shared") modCmd.Flags().BoolVar(&cmdCloudSharedDiskUnset, "unset_cloud_shared", false, "unset --cloud_shared") + modCmd.Flags().StringVar(&cmdGroup, "group", "", "unix group to start the command as") modCmd.Flags().StringVar(&cmdEnv, "env", "", "comma-separated list of key=value environment variables to set before running the commands") modCmd.Flags().StringVar(&cmdQueue, "queue", "", "name of queue to submit to, for schedulers with queues") modCmd.Flags().StringVar(&cmdQueuesAvoidMod, "queues_avoid", "", diff --git a/jobqueue/job.go b/jobqueue/job.go index bc2894eb..b4a54c3c 100644 --- a/jobqueue/job.go +++ b/jobqueue/job.go @@ -1046,6 +1046,7 @@ type JobModifier struct { Cmd string Cwd string ReqGroup string + Group string BsubMode string MonitorDocker string WithDocker string @@ -1057,6 +1058,7 @@ type JobModifier struct { ChangeHome bool ChangeHomeSet bool ReqGroupSet bool + GroupSet bool Override uint8 OverrideSet bool Priority uint8 @@ -1116,6 +1118,11 @@ func (j *JobModifier) SetReqGroup(newVal string) { j.ReqGroupSet = true } +func (j *JobModifier) SetUnixGroup(group string) { + j.Group = group + j.GroupSet = true +} + // SetRequirements notes that you want to modify the Requirements of Jobs. You // can't modify to a nil Requirements, so if req is nil, no set is done. // @@ -1323,6 +1330,9 @@ func (j *JobModifier) Modify(jobs []*Job, server *Server) (map[string]string, er if j.ReqGroupSet { job.ReqGroup = j.ReqGroup } + if j.GroupSet { + job.Group = j.Group + } if j.Requirements != nil { if j.Requirements.RAM != 0 { job.Requirements.RAM = j.Requirements.RAM From 89bfee8c0764c5f0ba5a0f4ce5a86607b86f9353 Mon Sep 17 00:00:00 2001 From: Michael Woolnough Date: Thu, 22 Jan 2026 16:08:45 +0000 Subject: [PATCH 5/5] Delint'd --- jobqueue/job.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/jobqueue/job.go b/jobqueue/job.go index b4a54c3c..e2fc889e 100644 --- a/jobqueue/job.go +++ b/jobqueue/job.go @@ -1330,9 +1330,11 @@ func (j *JobModifier) Modify(jobs []*Job, server *Server) (map[string]string, er if j.ReqGroupSet { job.ReqGroup = j.ReqGroup } + if j.GroupSet { job.Group = j.Group } + if j.Requirements != nil { if j.Requirements.RAM != 0 { job.Requirements.RAM = j.Requirements.RAM