├── configs └── images.yaml ├── Makefile ├── scripts └── epilog.sh ├── .gitignore ├── go.mod ├── pkg ├── socker │ ├── socker_test.go │ └── socker.go ├── user │ └── user.go └── su │ └── su.go ├── LICENSE ├── cmd └── socker │ └── socker.go ├── go.sum └── README.md /configs/images.yaml: -------------------------------------------------------------------------------- 1 | image-name: 2 | id: image-id 3 | desc: image description 4 | repository: image repo 5 | tag: image tag 6 | created_since: image created duration 7 | created_at: image created time 8 | size: image size -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build-linux64: 2 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o socker cmd/socker/socker.go 3 | install: 4 | go build -o socker cmd/socker/socker.go 5 | chown root:root socker 6 | chmod +s socker 7 | mv socker /usr/bin/ 8 | mkdir -p /var/lib/socker 9 | cp configs/images.yaml /var/lib/socker/ 10 | .PHONY: clean 11 | clean: 12 | -rm socker 13 | -------------------------------------------------------------------------------- /scripts/epilog.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## You should configure Slurm to enable epilog. This script will be excuted 4 | ## after each Slurm job termnated to delete corresponding container. 5 | recordFile=/var/lib/socker/epilog/$SLURM_JOB_ID 6 | if [ -f $recordFile ];then 7 | echo "clean docker container for job: $SLURM_JOB_ID" 8 | containerName=`cat $recordFile` 9 | ownerRecord=/var/lib/socker/epilog/$containerName 10 | pidRecord=$ownerRecord"-pids" 11 | docker rm -f $containerName 12 | for pid in `cat $pidRecord`; do 13 | kill $pid 14 | done 15 | rm -f $recordFile $ownerRecord $pidRecord 16 | fi 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | 5 | # Hot recomplie files 6 | *.tmp 7 | 8 | # Auto generate folders 9 | .idea 10 | # Folders 11 | _obj 12 | _test 13 | # Architecture specific extensions/prefixes 14 | *.[568vq] 15 | [568vq].out 16 | 17 | *.cgo1.go 18 | *.cgo2.c 19 | _cgo_defun.c 20 | _cgo_gotypes.go 21 | _cgo_export.* 22 | 23 | _testmain.go 24 | 25 | *.exe 26 | *.test 27 | *.prof 28 | 29 | # Output of the go coverage tool, specifically when used with LiteIDE 30 | *.out 31 | # ignore binary program 32 | cmd/socker/socker 33 | # external packages folder 34 | vendor/ 35 | github.com/ 36 | swagger/ 37 | bin/ 38 | .DS_Store 39 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/China-HPC/go-socker 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/Sirupsen/logrus v1.0.5 7 | github.com/gopherjs/gopherjs v0.0.0-20210202160940-bed99a852dfe // indirect 8 | github.com/jessevdk/go-flags v1.4.0 9 | github.com/jtolds/gls v4.20.0+incompatible // indirect 10 | github.com/kr/pty v1.1.3 11 | github.com/satori/go.uuid v1.2.0 12 | github.com/sirupsen/logrus v1.8.1 // indirect 13 | github.com/smartystreets/assertions v1.2.0 // indirect 14 | github.com/smartystreets/goconvey v1.6.3 15 | github.com/stretchr/testify v1.7.0 // indirect 16 | github.com/urfave/cli v1.20.0 17 | golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44 18 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 19 | gopkg.in/airbrake/gobrake.v2 v2.0.9 // indirect 20 | gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 // indirect 21 | gopkg.in/yaml.v2 v2.2.1 22 | ) 23 | -------------------------------------------------------------------------------- /pkg/socker/socker_test.go: -------------------------------------------------------------------------------- 1 | package socker 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "testing" 8 | 9 | . "github.com/smartystreets/goconvey/convey" 10 | ) 11 | 12 | func TestQueryChildPIDs(t *testing.T) { 13 | Convey("Test QueryChildPIDs", t, func() { 14 | pid := fmt.Sprintf("%d", os.Getpid()) 15 | pids, err := QueryChildPIDs(pid) 16 | So(err, ShouldBeNil) 17 | So(pids, ShouldBeNil) 18 | go func() { 19 | exec.Command("bash", "-c", "sleep 1").Run() 20 | }() 21 | pids, err = QueryChildPIDs(pid) 22 | So(err, ShouldBeNil) 23 | So(len(pids), ShouldEqual, 1) 24 | }) 25 | } 26 | 27 | func TestListImagesData(t *testing.T) { 28 | Convey("Test listImagesData", t, func() { 29 | contents, err := listImagesData(".") 30 | So(err, ShouldBeNil) 31 | So(contents, ShouldNotBeNil) 32 | content, err := listImagesData("socker_test.go") 33 | So(err, ShouldBeNil) 34 | So(content, ShouldNotBeNil) 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 China-HPC & Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /pkg/user/user.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "os/user" 5 | "strconv" 6 | "syscall" 7 | ) 8 | 9 | // User represents a user account. 10 | type User struct { 11 | UID int 12 | GID int 13 | Name string 14 | Home string 15 | Shell string 16 | } 17 | 18 | // Group represents a group account. 19 | type Group struct { 20 | GID int 21 | Name string 22 | Users []string 23 | } 24 | 25 | // UserCred contains user's credential and user info. 26 | type UserCred struct { 27 | User *user.User 28 | Cred *syscall.Credential 29 | } 30 | 31 | // GetUserCred returns a credential of specified user. 32 | func GetUserCred(username string) (*UserCred, error) { 33 | u, err := user.Lookup(username) 34 | if err != nil { 35 | return nil, err 36 | } 37 | return getUserCredByUID(u) 38 | } 39 | 40 | // GetUserCredByUID returns a credential of specified user. 41 | func GetUserCredByUID(uid string) (*UserCred, error) { 42 | u, err := user.LookupId(uid) 43 | if err != nil { 44 | return nil, err 45 | } 46 | return getUserCredByUID(u) 47 | } 48 | 49 | func getUserCredByUID(u *user.User) (*UserCred, error) { 50 | unitUID, err := strconv.ParseUint(u.Uid, 10, 32) 51 | if err != nil { 52 | return nil, err 53 | } 54 | unitGID, err := strconv.ParseUint(u.Uid, 10, 32) 55 | if err != nil { 56 | return nil, err 57 | } 58 | gids, err := u.GroupIds() 59 | if err != nil { 60 | return nil, err 61 | } 62 | var groups []uint32 63 | for _, gid := range gids { 64 | uintGid, err := strconv.ParseUint(gid, 10, 32) 65 | if err != nil { 66 | return nil, err 67 | } 68 | groups = append(groups, uint32(uintGid)) 69 | } 70 | return &UserCred{ 71 | User: u, 72 | Cred: &syscall.Credential{ 73 | Uid: uint32(unitUID), 74 | Gid: uint32(unitGID), 75 | Groups: groups, 76 | }, 77 | }, nil 78 | } 79 | -------------------------------------------------------------------------------- /pkg/su/su.go: -------------------------------------------------------------------------------- 1 | package su 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os/exec" 7 | "syscall" 8 | 9 | "github.com/China-HPC/go-socker/pkg/user" 10 | ) 11 | 12 | // Command creates a new exec.Cmd that will run with user privilege. 13 | func Command(uid, command string, args ...string) (*exec.Cmd, error) { 14 | ucred, err := user.GetUserCredByUID(uid) 15 | if err != nil { 16 | return nil, err 17 | } 18 | cmd := exec.Command(command, args...) 19 | cmd.SysProcAttr = &syscall.SysProcAttr{} 20 | cmd.SysProcAttr.Credential = ucred.Cred 21 | return cmd, nil 22 | } 23 | 24 | // Run creates and runs command with user privilege. 25 | func Run(uid, command string, args ...string) error { 26 | cmd, err := Command(uid, command, args...) 27 | if err != nil { 28 | return err 29 | } 30 | var stderr bytes.Buffer 31 | cmd.Stderr = &stderr 32 | err = cmd.Run() 33 | if err != nil { 34 | return fmt.Errorf("command(su %d) %s: %v: %s", 35 | uid, cmd.Path, err, stderr.String()) 36 | } 37 | return nil 38 | } 39 | 40 | // Output creates and runs command with user privilege and returns the output. 41 | func Output(uid, command string, args ...string) ([]byte, error) { 42 | cmd, err := Command(uid, command, args...) 43 | if err != nil { 44 | return nil, err 45 | } 46 | var stderr bytes.Buffer 47 | cmd.Stderr = &stderr 48 | out, err := cmd.Output() 49 | if err != nil { 50 | return nil, fmt.Errorf("command(su %d) %s: %v: %s (output: %s)", 51 | uid, cmd.Path, err, stderr.String(), string(out)) 52 | } 53 | return out, nil 54 | } 55 | 56 | // CombinedOutput creates and runs command with user privilege and returns the 57 | // combined output. 58 | func CombinedOutput(uid, command string, args ...string) ([]byte, error) { 59 | cmd, err := Command(uid, command, args...) 60 | if err != nil { 61 | return nil, err 62 | } 63 | out, err := cmd.CombinedOutput() 64 | if err != nil { 65 | return nil, fmt.Errorf("command(su %d) %s: %v: %s", 66 | uid, cmd.Path, err, string(out)) 67 | } 68 | return out, nil 69 | } 70 | -------------------------------------------------------------------------------- /cmd/socker/socker.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 China-HPC. 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "log" 8 | "os" 9 | 10 | "github.com/China-HPC/go-socker/pkg/socker" 11 | "github.com/urfave/cli" 12 | ) 13 | 14 | var ( 15 | verbose bool 16 | epilogEnabled bool 17 | insecure bool 18 | s *socker.Socker 19 | ) 20 | 21 | func main() { 22 | app := cli.NewApp() 23 | app.Name = "socker" 24 | app.Usage = "Secure runner for Docker containers" 25 | app.Version = "0.1.0" 26 | app.Before = appInit 27 | app.Flags = []cli.Flag{ 28 | cli.BoolFlag{ 29 | Name: "verbose", 30 | Destination: &verbose, 31 | Usage: "run in verbose mode", 32 | }, 33 | cli.BoolFlag{ 34 | Name: "epilog", 35 | Destination: &epilogEnabled, 36 | Usage: "run with Slurm epilog enabled", 37 | }, 38 | cli.BoolFlag{ 39 | Name: "insecure", 40 | Destination: &insecure, 41 | Usage: "run in insecure mode, strongly not recommended", 42 | }, 43 | } 44 | app.Commands = []cli.Command{ 45 | { 46 | Name: "images", 47 | Usage: "List images that defined in image.yaml file or sync images from Docker to socker.", 48 | Subcommands: []cli.Command{ 49 | { 50 | Name: "list", 51 | Usage: "list all images", 52 | Flags: []cli.Flag{ 53 | cli.StringFlag{ 54 | Name: "config, c", 55 | Usage: "images config file", 56 | }, 57 | }, 58 | Action: func(c *cli.Context) error { 59 | err := s.PrintImages(c.String("config")) 60 | if err != nil { 61 | return cli.NewExitError(err.Error(), 1) 62 | } 63 | return nil 64 | }, 65 | }, 66 | { 67 | Name: "sync", 68 | Usage: "sync images from docker (NOTE:common user have no permission to do this operation)", 69 | Flags: []cli.Flag{ 70 | cli.StringFlag{ 71 | Name: "config, c", 72 | Usage: "images config file", 73 | }, 74 | cli.StringFlag{ 75 | Name: "repo, r", 76 | Usage: "filter the repository", 77 | }, 78 | cli.StringFlag{ 79 | Name: "filter, f", 80 | Usage: "filter the images", 81 | }, 82 | }, 83 | Before: func(c *cli.Context) error { 84 | if s.CurrentUID != "0" { 85 | log.Fatal("You have no permission to do this.") 86 | } 87 | return nil 88 | }, 89 | Action: func(c *cli.Context) error { 90 | err := s.SyncImages(c.String("config"), 91 | c.String("repo"), c.String("filter")) 92 | if err != nil { 93 | return cli.NewExitError(err.Error(), 1) 94 | } 95 | return nil 96 | }, 97 | }, 98 | }, 99 | }, 100 | { 101 | Name: "run", 102 | Usage: "run a container from IMAGE executing COMMAND as regular user", 103 | SkipFlagParsing: true, 104 | Action: func(c *cli.Context) error { 105 | err := s.RunImage(c.Args()) 106 | if err != nil { 107 | return cli.NewExitError(err, 1) 108 | } 109 | return nil 110 | }, 111 | }, 112 | { 113 | Name: "exec", 114 | Usage: "run a command in a running container as regular user", 115 | SkipFlagParsing: true, 116 | Action: func(c *cli.Context) error { 117 | err := s.Exec(c.Args()) 118 | if err != nil { 119 | return cli.NewExitError(err, 1) 120 | } 121 | return nil 122 | }, 123 | }, 124 | } 125 | err := app.Run(os.Args) 126 | if err != nil { 127 | log.Fatal(err) 128 | } 129 | } 130 | 131 | func appInit(ctx *cli.Context) error { 132 | var err error 133 | conf := &socker.Config{ 134 | Verbose: verbose, 135 | EpilogEnabled: epilogEnabled, 136 | Insecure: insecure, 137 | } 138 | s, err = socker.New(conf) 139 | if err != nil { 140 | log.Fatal(fmt.Sprintf("init socker failed: %v", err)) 141 | os.Exit(2) 142 | } 143 | return nil 144 | } 145 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Sirupsen/logrus v1.0.5 h1:447dy9LxSj+Iaa2uN3yoFHOzU9yJcJYiQPtNz8OXtv0= 2 | github.com/Sirupsen/logrus v1.0.5/go.mod h1:rmk17hk6i8ZSAJkSDa7nOxamrG+SP4P0mm+DAvExv4U= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/gopherjs/gopherjs v0.0.0-20210202160940-bed99a852dfe h1:rcf1P0fm+1l0EjG16p06mYLj9gW9X36KgdHJ/88hS4g= 7 | github.com/gopherjs/gopherjs v0.0.0-20210202160940-bed99a852dfe/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 8 | github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= 9 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 10 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 11 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 12 | github.com/kr/pty v1.1.3 h1:/Um6a/ZmD5tF7peoOJ5oN5KMQ0DrGVQSXLNwyckutPk= 13 | github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 14 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 15 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 16 | github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= 17 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 18 | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= 19 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 20 | github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= 21 | github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= 22 | github.com/smartystreets/goconvey v1.6.3 h1:QdmJJYlDQhMDFrFP8IvVnx66D8mCbaQM4TsxKf7BXzo= 23 | github.com/smartystreets/goconvey v1.6.3/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= 24 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 25 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 26 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 27 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 28 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 29 | github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= 30 | github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= 31 | golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44 h1:9lP3x0pW80sDI6t1UMSLA4to18W7R7imwAI/sWS9S8Q= 32 | golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 33 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= 34 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 35 | gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo= 36 | gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= 37 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 38 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 39 | gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 h1:OAj3g0cR6Dx/R07QgQe8wkA9RNjB2u4i700xBkIT4e0= 40 | gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= 41 | gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= 42 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 43 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 44 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-socker 2 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FChina-HPC%2Fgo-socker.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2FChina-HPC%2Fgo-socker?ref=badge_shield) 3 | 4 | 5 | A wrapper for secure running of Docker containers on Slurm implement in Golang. Inspired by the paper _[Enabling Docker Containers for High-Performance and Many-Task Computing](https://ieeexplore.ieee.org/document/7923813/)_ and [socker](https://github.com/unioslo/socker). 6 | 7 | ## Introduction 8 | 9 | Socker is secure for enabling unprivileged users to run Docker containers. It mainly does two things: 10 | 11 | - It enforces running containers within as the user not as root 12 | - When it is called inside a Slurm job, it enforces the inclusion of containers in the [cgroups assigned by Slurm to the parent jobs](https://slurm.schedmd.com/cgroups.html) 13 | 14 | ## Prerequisite 15 | 16 | ### MUST 17 | 18 | - CentOS/Redhat and Debian have been tested 19 | - Docker 18.06+ 20 | - You MUST have a group docker and a user dockerroot who is member of ONLY the docker group. The docker run command will be executed as dockerroot. 21 | - You SHOULD enable Linux namespaces and Docker `userns-remap` feature to make `socker` safer. Read the [Docker document](https://docs.docker.com/engine/security/userns-remap/) to know more about `userns-remap` please. 22 | - Golang 1.6+(For development ONLY) 23 | 24 | To add the dockerroot user to docker group: 25 | 26 | ```bash 27 | usermod -aG docker dockerroot 28 | ``` 29 | 30 | ### Optional 31 | 32 | - Slurm is not a prerequisite, but if you run socker inside a Slurm job, it will put the container under Slurm's control. 33 | - `libcgroup-tools` should be installed for cgroup limit set. 34 | 35 | ## Installation 36 | 37 | ### Build from source 38 | 39 | make sure you have installed [`go`](https://golang.org/dl/) then: 40 | 41 | ```bash 42 | make install 43 | ``` 44 | 45 | ### Configure images 46 | 47 | You should run command to sync `Docker` images to `socker`, simple usage: 48 | 49 | ```bash 50 | socker images sync 51 | ``` 52 | 53 | #### sync filter 54 | 55 | - Docker filter: use `--filter` or `-f` flag to specify filter to sync Docker filtered images,[read the document to know more](https://docs.docker.com/engine/reference/commandline/images/#filtering) 56 | - Repository filter: use `--repo` or `-r` flag to specify repo filter to sync the images which contain specific keyword 57 | 58 | ```bash 59 | ## Example 60 | ## sync harbor.hpc.com/* images. 61 | socker images sync --repo "harbor.hpc.com" 62 | 63 | ## sync docker filtered images. 64 | socker images sync --filter "reference=ubuntu*" 65 | ``` 66 | 67 | #### customized images 68 | 69 | Or define your images config in `/var/lib/socker/images.yaml` file manually before using `socker images` command. 70 | 71 | ### Configure with slurm (Optional) 72 | 73 | If you want to delete containers after Slurm job terminated, you should use the `epilog.sh` script in scripts directory as Slurm epilog script. 74 | 75 | ## Quick Start 76 | 77 | Use socker just like docker, for example: 78 | 79 | ```bash 80 | socker run -it ubuntu bash 81 | ``` 82 | 83 | Run socker --help to know more: 84 | 85 | ```txt 86 | NAME: 87 | socker - Secure runner for Docker containers 88 | 89 | USAGE: 90 | socker [global options] command [command options] [arguments...] 91 | 92 | VERSION: 93 | 0.1.0 94 | 95 | COMMANDS: 96 | images List images that defined in image.yaml file or sync images from Docker to socker. 97 | run run a container from IMAGE executing COMMAND as regular user 98 | help, h Shows a list of commands or help for one command 99 | 100 | GLOBAL OPTIONS: 101 | --verbose run in verbose mode 102 | --epilog run with Slurm epilog enabled 103 | --help, -h show help 104 | --version, -v print the version 105 | ``` 106 | 107 | ## Security 108 | 109 | Socker should work with Docker daemon which `userns-remap` feature has enbaled. 110 | 111 | > The best way to prevent privilege-escalation attacks from within a container is to configure your container’s applications to run as unprivileged users, For containers whose processes must run as the root user within the container, you can re-map this user to a less-privileged user on the Docker host. 112 | 113 | `socker` will default mount a swap directory(`$HOME/container`) to container, the root user of container can write data into this safe directory with `userns-remap` specified user's permission. 114 | 115 | You can also use `socker` with Docker daemon without `userns-remap`, but this is dangerous. Safe or convenient, you can only choose one of them at present. 116 | 117 | ## Support and Bug Reports 118 | 119 | ## License 120 | 121 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FChina-HPC%2Fgo-socker.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FChina-HPC%2Fgo-socker?ref=badge_large) 122 | -------------------------------------------------------------------------------- /pkg/socker/socker.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 China-HPC. 2 | 3 | // Package socker implements a secure runner for docker containers. 4 | package socker 5 | 6 | import ( 7 | "bufio" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "os" 12 | "os/exec" 13 | "os/signal" 14 | "os/user" 15 | "path" 16 | "path/filepath" 17 | "strings" 18 | "syscall" 19 | "time" 20 | 21 | "github.com/China-HPC/go-socker/pkg/su" 22 | log "github.com/Sirupsen/logrus" 23 | flags "github.com/jessevdk/go-flags" 24 | "github.com/kr/pty" 25 | uuid "github.com/satori/go.uuid" 26 | "github.com/urfave/cli" 27 | "golang.org/x/crypto/ssh/terminal" 28 | "golang.org/x/sys/unix" 29 | yaml "gopkg.in/yaml.v2" 30 | ) 31 | 32 | const ( 33 | cmdDocker = "docker" 34 | cmdCgclassify = "cgclassify" 35 | cmdPs = "ps" 36 | cmdPgrep = "pgrep" 37 | sepColon = ":" 38 | sepPipe = "|" 39 | lineBrk = "\n" 40 | envSlurmJobID = "SLURM_JOBID" 41 | 42 | containerRunTimeout = time.Second * 30 43 | epilogDir = "/var/lib/socker/epilog" 44 | permEpilogDir = 0700 45 | permRecordFile = 0600 46 | 47 | dftImageConfigFile = "/var/lib/socker/images.yaml" 48 | layoutImageFormat = `{{.ID}}|{{.Repository}}|{{.Tag}}|{{.CreatedSince}}|{{.CreatedAt}}|{{.Size}}` 49 | ) 50 | 51 | // Socker provides a runner for docker. 52 | type Socker struct { 53 | dockerUID string 54 | dockerGID string 55 | CurrentUID string 56 | currentUser string 57 | currentGID string 58 | currentGroup string 59 | homeDir string 60 | containerUUID string 61 | isInsideJob bool 62 | slurmJobID string 63 | *Config 64 | } 65 | 66 | // Config represents the socker configurations. 67 | type Config struct { 68 | Verbose bool 69 | EpilogEnabled bool 70 | Insecure bool 71 | } 72 | 73 | // Opts represents the socker supported docker options. 74 | type Opts struct { 75 | Volumes []string `short:"v" long:"volume"` 76 | TTY bool `short:"t" long:"tty"` 77 | Interactive bool `short:"i" long:"interactive"` 78 | Detach bool `short:"d" long:"detach"` 79 | Runtime string `long:"runtime"` 80 | Network string `long:"network"` 81 | Name string `long:"name"` 82 | Hostname string `short:"h" long:"hostname"` 83 | User string `short:"u" long:"user"` 84 | StorageOpt string `long:"storage-opt"` 85 | ShmSize string `long:"shm-size"` 86 | } 87 | 88 | // ExecOpts represents the socker supported docker exec options. 89 | type ExecOpts struct { 90 | TTY bool `short:"t" long:"tty"` 91 | Interactive bool `short:"i" long:"interactive"` 92 | Detach bool `short:"d" long:"detach"` 93 | User string `short:"u" long:"user"` 94 | } 95 | 96 | // New creates a socker instance. 97 | func New(conf *Config) (*Socker, error) { 98 | if conf.Verbose { 99 | log.SetLevel(log.DebugLevel) 100 | } 101 | log.SetOutput(os.Stdout) 102 | s := &Socker{ 103 | Config: conf, 104 | } 105 | err := s.checkPrerequisite() 106 | if err != nil { 107 | return nil, err 108 | } 109 | return s, nil 110 | } 111 | 112 | // Image represents the socker/socker availible image format 113 | type Image struct { 114 | ID string `yaml:"id"` 115 | Desc string `yaml:"desc"` 116 | Repository string `yaml:"repository"` 117 | Tag string `yaml:"tag"` 118 | CreatedScince string `yaml:"created_since"` 119 | CreatedAt string `yaml:"created_at"` 120 | Size string `yaml:"size"` 121 | } 122 | 123 | // FormatImages lists all available images from registry by map. 124 | func (s *Socker) FormatImages(config string) (map[string]Image, error) { 125 | data, err := listImagesData(config) 126 | if err != nil { 127 | log.Fatal(err) 128 | return nil, err 129 | } 130 | var images map[string]Image 131 | err = yaml.Unmarshal(data, &images) 132 | if err != nil { 133 | log.Fatal(err) 134 | return nil, err 135 | } 136 | return images, nil 137 | } 138 | 139 | // PrintImages prints available images for CLI. 140 | func (s *Socker) PrintImages(config string) error { 141 | images, err := s.FormatImages(config) 142 | if err != nil { 143 | log.Fatal(err) 144 | return err 145 | } 146 | for k := range images { 147 | fmt.Println(k) 148 | } 149 | return nil 150 | } 151 | 152 | // SyncImages syncs available images for CLI. 153 | func (s *Socker) SyncImages(configFile, repoFilter, filter string) error { 154 | if configFile == "" { 155 | configFile = dftImageConfigFile 156 | } 157 | images, err := ParseImages(repoFilter, filter) 158 | if err != nil { 159 | return err 160 | } 161 | data, err := yaml.Marshal(images) 162 | if err != nil { 163 | log.Errorf("marshal yaml data failed: %v", err) 164 | return err 165 | } 166 | return ioutil.WriteFile(configFile, data, permRecordFile) 167 | } 168 | 169 | // ParseImages parses images from docker. 170 | func ParseImages(repoFilter, filter string) (map[string]Image, error) { 171 | args := []string{"images", "--format", layoutImageFormat} 172 | if filter != "" { 173 | args = append(args, fmt.Sprintf("--filter=%s", filter)) 174 | } 175 | out, err := exec.Command(cmdDocker, args...).CombinedOutput() 176 | if err != nil { 177 | log.Errorf("list images from Docker failed: %v:%s", err, out) 178 | return nil, err 179 | } 180 | images := make(map[string]Image) 181 | for _, line := range strings.Split(strings. 182 | TrimSpace(string(out)), lineBrk) { 183 | image, err := parseImage(line) 184 | if err != nil { 185 | log.Errorf("parse image failed: %v", err) 186 | return nil, err 187 | } 188 | if repoFilter == "" { 189 | images[fmt.Sprintf("%s:%s", image.Repository, image.Tag)] = *image 190 | continue 191 | } 192 | if strings.Contains(image.Repository, repoFilter) { 193 | images[fmt.Sprintf("%s:%s", image.Repository, image.Tag)] = *image 194 | } 195 | } 196 | return images, nil 197 | } 198 | 199 | func parseImage(text string) (*Image, error) { 200 | fields := strings.Split(text, sepPipe) 201 | if len(fields) != 6 { 202 | return nil, fmt.Errorf("parse image failed due to fields mismatch") 203 | } 204 | return &Image{ 205 | ID: fields[0], 206 | Repository: fields[1], 207 | Tag: fields[2], 208 | CreatedScince: fields[3], 209 | CreatedAt: fields[4], 210 | Size: fields[5], 211 | }, nil 212 | } 213 | 214 | func listImagesData(config string) ([]byte, error) { 215 | if config == "" { 216 | config = dftImageConfigFile 217 | } 218 | info, err := os.Stat(config) 219 | if err != nil { 220 | log.Error(err) 221 | return nil, err 222 | } 223 | var data []byte 224 | if !info.IsDir() { 225 | data, err = ioutil.ReadFile(config) 226 | if err != nil { 227 | log.Error(err) 228 | return nil, err 229 | } 230 | return data, nil 231 | } 232 | files, err := ioutil.ReadDir(config) 233 | if err != nil { 234 | log.Error(err) 235 | return nil, err 236 | } 237 | for _, file := range files { 238 | if file.IsDir() { 239 | continue 240 | } 241 | content, err := ioutil.ReadFile(path.Join(config, file.Name())) 242 | if err != nil { 243 | return nil, err 244 | } 245 | data = append(data, content...) 246 | } 247 | return data, nil 248 | } 249 | 250 | // Exec runs a command in a running container as regular user. 251 | func (s *Socker) Exec(command []string) error { 252 | opts := ExecOpts{} 253 | remainedArgs, err := flags.ParseArgs(&opts, command) 254 | if err != nil { 255 | log.Errorf("parse command args failed: %v", err) 256 | return err 257 | } 258 | if len(remainedArgs) < 2 { 259 | return fmt.Errorf("you must specifiy container name and command") 260 | } 261 | containerUID, err := ioutil.ReadFile(path.Join(epilogDir, remainedArgs[0])) 262 | if err != nil { 263 | return fmt.Errorf("container owner check error: %v", err) 264 | } 265 | if strings.TrimSpace(string(containerUID)) != s.CurrentUID { 266 | return fmt.Errorf("you have no permission to exec command in this container") 267 | } 268 | args := []string{"exec"} 269 | args = append(args, command...) 270 | log.Debugf("docker exec args: %v", args) 271 | cmd, err := su.Command(s.dockerUID, cmdDocker, args...) 272 | if err != nil { 273 | return err 274 | } 275 | if opts.TTY { 276 | return s.runWithPty(cmd) 277 | } 278 | output, err := cmd.CombinedOutput() 279 | if err != nil { 280 | return err 281 | } 282 | fmt.Fprintf(os.Stdout, "%s", output) 283 | return nil 284 | } 285 | 286 | // RunImage runs container. 287 | func (s *Socker) RunImage(command []string) error { 288 | opts := Opts{} 289 | _, err := flags.ParseArgs(&opts, command) 290 | if err != nil { 291 | log.Errorf("parse command args failed: %v", err) 292 | return err 293 | } 294 | // specified name has a higher priority, uniqueness is guaranteed by the 295 | // user, it will automatically generate UUID as the name if it is empty. 296 | if opts.Name != "" { 297 | s.containerUUID = opts.Name 298 | } else { 299 | s.containerUUID = uuid.NewV4().String() 300 | } 301 | args := []string{"run", "--name", s.containerUUID} 302 | // refuse to mount a directory that is not authorized to access 303 | if err := s.isVolumePermit(opts.Volumes); err != nil { 304 | return err 305 | } 306 | // create security swap directory and mount into container. 307 | if !s.Insecure { 308 | swapDir := path.Join(s.homeDir, "container") 309 | args = append(args, "-v", fmt.Sprintf("%s:%s", swapDir, swapDir)) 310 | err = os.MkdirAll(swapDir, 0777) 311 | if err != nil { 312 | return err 313 | } 314 | err = os.Chmod(swapDir, 0777) 315 | if err != nil { 316 | return err 317 | } 318 | err = os.Chmod(s.homeDir, 0755) 319 | if err != nil { 320 | return err 321 | } 322 | } else { 323 | args = append(args, "-v", fmt.Sprintf("%s:%s", s.homeDir, s.homeDir)) 324 | } 325 | 326 | go s.containerMonitor() 327 | 328 | log.Debugf("epilog enabled: %t", s.EpilogEnabled) 329 | if s.EpilogEnabled { 330 | err := ioutil.WriteFile(path.Join(epilogDir, s.slurmJobID), 331 | []byte(s.containerUUID), permEpilogDir) 332 | if err != nil { 333 | return err 334 | } 335 | err = ioutil.WriteFile(path.Join(epilogDir, s.containerUUID), 336 | []byte(s.CurrentUID), permEpilogDir) 337 | if err != nil { 338 | return err 339 | } 340 | } 341 | args = append(args, command...) 342 | log.Debugf("docker run args: %v", args) 343 | cmd, err := su.Command(s.dockerUID, cmdDocker, args...) 344 | if err != nil { 345 | return err 346 | } 347 | if opts.TTY { 348 | return s.runWithPty(cmd) 349 | } 350 | output, err := cmd.CombinedOutput() 351 | if err != nil { 352 | return err 353 | } 354 | fmt.Fprintf(os.Stdout, "%s", output) 355 | return nil 356 | } 357 | 358 | func isContainerRan(containerName string) (bool, error) { 359 | cmd := exec.Command(cmdDocker, "events", 360 | "--filter", "event=start", 361 | "--filter", fmt.Sprintf("container=%s", containerName)) 362 | reader, err := cmd.StdoutPipe() 363 | if err != nil { 364 | return false, err 365 | } 366 | defer reader.Close() 367 | err = cmd.Start() 368 | if err != nil { 369 | return false, err 370 | } 371 | b := bufio.NewScanner(reader) 372 | isStarted := make(chan bool, 1) 373 | select { 374 | case isStarted <- b.Scan(): 375 | log.Debugf("container started") 376 | return true, nil 377 | case <-time.After(containerRunTimeout): 378 | log.Errorf("container start timeout") 379 | return false, fmt.Errorf("container start timeout") 380 | } 381 | } 382 | 383 | func queryContainerPID(containerName string) (string, error) { 384 | args := []string{"inspect", "-f", "{{ .State.Pid }}", containerName} 385 | output, err := exec.Command(cmdDocker, args...).CombinedOutput() 386 | if err != nil { 387 | log.Errorf("query container pid failed: %v:%s", err, output) 388 | return "", err 389 | } 390 | cmdPid := strings.TrimSpace(string(output)) 391 | output, err = exec.Command(cmdPs, "-o", "ppid=", 392 | "-p", cmdPid).CombinedOutput() 393 | log.Debugf("find cmdPid command: ps -o ppid= -p %s", cmdPid) 394 | if err != nil { 395 | log.Errorf("can't find docker-containe pid: %v,%s", err, output) 396 | return "", err 397 | } 398 | containerPID := strings.TrimSpace(string(output)) 399 | log.Debugf("container PID is: %s", containerPID) 400 | return containerPID, nil 401 | } 402 | 403 | func (s *Socker) containerMonitor() error { 404 | started, err := isContainerRan(s.containerUUID) 405 | if err != nil { 406 | log.Errorf("detect container status failed: %v", err) 407 | return err 408 | } 409 | if started && !s.Insecure { 410 | // container has ran, change user's home dir permission. 411 | defer changeDirPerm(s.homeDir) 412 | } 413 | if !s.isInsideJob { 414 | log.Debugf("not inside of job") 415 | return nil 416 | } 417 | err = s.enforceLimit() 418 | if err != nil { 419 | log.Errorf("enforce limit failed: %v", err) 420 | } 421 | return nil 422 | } 423 | 424 | func changeDirPerm(dir string) error { 425 | fileStat, err := os.Stat(dir) 426 | if err != nil { 427 | log.Errorf("get directory info failed: %v", err) 428 | return err 429 | } 430 | if fileStat.Mode() == 0755 { 431 | return nil 432 | } 433 | err = os.Chmod(dir, 0750) 434 | if err != nil { 435 | log.Errorf("change home dir permission error: %v", err) 436 | } 437 | return nil 438 | } 439 | 440 | func (s *Socker) enforceLimit() error { 441 | containerPID, err := queryContainerPID(s.containerUUID) 442 | if err != nil { 443 | log.Errorf("query container pid error: %v", err) 444 | return err 445 | } 446 | cgroupID := fmt.Sprintf("slurm/uid_%s/job_%s/", s.CurrentUID, s.slurmJobID) 447 | log.Debugf("target cgroup id is: %s", cgroupID) 448 | for { 449 | pids, err := QueryChildPIDs(containerPID) 450 | if err != nil { 451 | log.Errorf("query child process ids failed: %v", err) 452 | } 453 | err = s.setCgroupLimit(pids, cgroupID) 454 | if err != nil { 455 | return err 456 | } 457 | // TODO: find a better way to watch cgroup new tasks without polling. 458 | time.Sleep(time.Second * 1) 459 | } 460 | } 461 | 462 | func (s *Socker) setCgroupLimit(pids []string, cgroupID string) error { 463 | for _, pid := range pids { 464 | // frees process from the docker cgroups. 465 | output, err := exec.Command(cmdCgclassify, "-g", 466 | "cpu,cpuset,memory,devices:/", pid).CombinedOutput() 467 | log.Debugf("frees container cgroups limit") 468 | if err != nil { 469 | log.Errorf("frees container cgroups limit failed: %v:%s", err, output) 470 | return err 471 | } 472 | // add process into slurm job cgroups. 473 | output, err = exec.Command(cmdCgclassify, "-g", 474 | fmt.Sprintf("memory,cpu,cpuset,freezer,devices:/%s", 475 | cgroupID), 476 | pid).CombinedOutput() 477 | log.Debugf("enforcing slurm limit to pid: %s", pid) 478 | if err != nil { 479 | log.Errorf("enforces Slurm job limit failed: %v:%s", err, output) 480 | return err 481 | } 482 | } 483 | return nil 484 | } 485 | 486 | // QueryChildPIDs lookups child process ids of specified parent process. 487 | func QueryChildPIDs(parentID string) ([]string, error) { 488 | out, err := exec.Command(cmdPgrep, "-P", parentID).CombinedOutput() 489 | if err != nil { 490 | // if no processes were matched pgrep exit with 1 491 | if strings.Contains(err.Error(), "exit status 1") { 492 | return nil, nil 493 | } 494 | log.Errorf("query child pids failed: %v:%s", err, out) 495 | return nil, err 496 | } 497 | pids := strings.Split(strings.TrimSpace(string(out)), lineBrk) 498 | return pids, nil 499 | } 500 | 501 | func (s *Socker) isVolumePermit(vols []string) error { 502 | for _, vol := range vols { 503 | if !strings.HasSuffix(vol, ":ro") { 504 | return fmt.Errorf("volume %s must mounted as read-only", vol) 505 | } 506 | if strings.Contains(vol, sepColon) { 507 | vol = strings.Split(vol, sepColon)[0] 508 | } 509 | err := filepath.Walk(vol, walkfunc) 510 | if err != nil { 511 | return err 512 | } 513 | } 514 | return nil 515 | } 516 | 517 | func walkfunc(vol string, info os.FileInfo, err error) error { 518 | if err := unix.Access(vol, unix.R_OK); err != nil { 519 | log.Debugf("volume %s permissin denined: %v", vol, err) 520 | return fmt.Errorf("volume %s permissin denined: %v", vol, err) 521 | } 522 | return nil 523 | } 524 | 525 | func (s *Socker) runWithPty(cmd *exec.Cmd) error { 526 | tty, err := pty.Start(cmd) 527 | if err != nil { 528 | return fmt.Errorf("docker command exec failed: %v", err) 529 | } 530 | // Handle pty size. 531 | ch := make(chan os.Signal, 1) 532 | signal.Notify(ch, syscall.SIGWINCH) 533 | go func() { 534 | for range ch { 535 | if err := pty.InheritSize(os.Stdin, tty); err != nil { 536 | log.Printf("error resizing pty: %s", err) 537 | } 538 | } 539 | }() 540 | ch <- syscall.SIGWINCH // Initial resize. 541 | 542 | oldState, err := terminal.MakeRaw(int(os.Stdin.Fd())) 543 | if err != nil { 544 | return err 545 | } 546 | defer func() { _ = terminal.Restore(int(os.Stdin.Fd()), oldState) }() 547 | go func() { io.Copy(os.Stdout, tty) }() 548 | go func() { io.Copy(tty, os.Stdin) }() 549 | return cmd.Wait() 550 | } 551 | 552 | func (s *Socker) checkPrerequisite() error { 553 | if !isCommandAvailable(cmdDocker) { 554 | return cli.NewExitError("docker command not found, make sure Docker is installed...", 127) 555 | } 556 | u, err := user.Lookup("dockerroot") 557 | if err != nil { 558 | return cli.NewExitError("there must exist a user 'dockerroot' and a group 'docker'", 1) 559 | } 560 | s.dockerUID = u.Uid 561 | g, err := user.LookupGroup("docker") 562 | if err != nil { 563 | return cli.NewExitError("there must exist a user 'dockerroot' and a group 'docker'", 1) 564 | } 565 | s.dockerGID = g.Gid 566 | gids, err := u.GroupIds() 567 | if err != nil && isMemberOfGroup(gids, u.Gid) { 568 | return cli.NewExitError("the user 'dockerroot' must be a member of the 'docker' group", 2) 569 | } 570 | current, err := user.Current() 571 | if err != nil { 572 | return cli.NewExitError("can't get current user info", 2) 573 | } 574 | s.CurrentUID = current.Uid 575 | s.currentUser = current.Username 576 | s.currentGID = current.Gid 577 | currentGroup, err := user.LookupGroupId(s.currentGID) 578 | if err != nil { 579 | return cli.NewExitError("can't get current user's group info", 2) 580 | } 581 | s.currentGroup = currentGroup.Name 582 | s.homeDir = current.HomeDir 583 | if jobID := os.Getenv(envSlurmJobID); jobID != "" { 584 | log.Debugf("slurm job id: %s", jobID) 585 | s.isInsideJob = true 586 | s.slurmJobID = jobID 587 | } 588 | return os.MkdirAll(epilogDir, permRecordFile) 589 | } 590 | 591 | func isMemberOfGroup(gids []string, gid string) bool { 592 | for _, id := range gids { 593 | if id == gid { 594 | return true 595 | } 596 | } 597 | return false 598 | } 599 | 600 | func isCommandAvailable(name string) bool { 601 | _, err := exec.LookPath(name) 602 | if err != nil { 603 | return false 604 | } 605 | return true 606 | } 607 | --------------------------------------------------------------------------------