Compare commits

...

7 Commits

Author SHA1 Message Date
a87c568335 wip: config unit test 2023-12-25 13:13:03 +11:00
c043c91f0e chore(Makefiles): to simplify Dockerfile
fix(wmexec): setgid before setuid
test(wmexec): tested
2023-12-17 23:45:03 +00:00
15a804aa7d feat(exec): cgo call to getpwnam and getgrnam 2023-12-17 05:23:26 +00:00
653b4ff158 feat(exec): initial 2023-12-17 03:36:47 +00:00
8cf92167df chore: fix .gitignore
feat(waiter): waiting for signal
2023-12-14 00:11:39 +00:00
9c74296c27 chore(pidproxy): remove binary from git and update .gitignore 2023-12-12 01:25:31 +00:00
8a85ad5107 feat(pidproxy): added signal handler 2023-12-12 00:29:32 +00:00
27 changed files with 468 additions and 56 deletions

View File

@ -18,7 +18,8 @@
"extensions": [
"golang.go",
"ms-azuretools.vscode-docker",
"ms-vscode.makefile-tools"
"ms-vscode.makefile-tools",
"ms-vscode.cpptools-extension-pack"
]
}
}

2
.gitignore vendored
View File

@ -1 +1,3 @@
/cmd/wingmate/wingmate
/cmd/pidproxy/pidproxy
/cmd/exec/exec

View File

@ -1,6 +1,6 @@
DESTDIR = /usr/local/bin
all: wingmate dummy oneshot spawner starter pidproxy
all: wingmate dummy oneshot spawner starter pidproxy exec
wingmate:
$(MAKE) -C cmd/wingmate all
@ -8,6 +8,9 @@ wingmate:
pidproxy:
$(MAKE) -C cmd/pidproxy all
exec:
$(MAKE) -C cmd/exec all
dummy:
$(MAKE) -C cmd/experiment/dummy all
@ -23,7 +26,18 @@ starter:
clean:
$(MAKE) -C cmd/wingmate clean
$(MAKE) -C cmd/pidproxy clean
$(MAKE) -C cmd/exec clean
$(MAKE) -C cmd/experiment/dummy clean
$(MAKE) -C cmd/experiment/oneshot clean
$(MAKE) -C cmd/experiment/spawner clean
$(MAKE) -C cmd/experiment/starter clean
install:
install -d ${DESTDIR}
$(MAKE) -C cmd/wingmate DESTDIR=${DESTDIR} install
$(MAKE) -C cmd/pidproxy DESTDIR=${DESTDIR} install
$(MAKE) -C cmd/exec DESTDIR=${DESTDIR} install
$(MAKE) -C cmd/experiment/dummy DESTDIR=${DESTDIR} install
$(MAKE) -C cmd/experiment/oneshot DESTDIR=${DESTDIR} install
$(MAKE) -C cmd/experiment/spawner DESTDIR=${DESTDIR} install
$(MAKE) -C cmd/experiment/starter DESTDIR=${DESTDIR} install

8
cmd/exec/Makefile Normal file
View File

@ -0,0 +1,8 @@
all:
go build -v
clean:
go clean -i -cache -testcache
install:
install exec ${DESTDIR}/wmexec

11
cmd/exec/ent.go Normal file
View File

@ -0,0 +1,11 @@
//go:build !(cgo && linux)
package main
func getUid(user string) (uint64, error) {
panic("not implemented")
}
func getGid(group string) (uint64, error) {
panic("not implemented")
}

51
cmd/exec/ent_lin.go Normal file
View File

@ -0,0 +1,51 @@
//go:build cgo && linux
package main
/*
#include<errno.h>
#include<string.h>
#include<sys/types.h>
#include<pwd.h>
#include<grp.h>
static uid_t getuid(const char* username) {
struct passwd local, *rv;
errno = 0;
rv = getpwnam(username);
if (errno != 0) {
return 0;
}
memcpy(&local, rv, sizeof(struct passwd));
return local.pw_uid;
}
static gid_t getgid(const char* groupname) {
struct group local, *rv;
errno = 0;
rv = getgrnam(groupname);
if (errno != 0) {
return 0;
}
memcpy(&local, rv, sizeof(struct group));
return local.gr_gid;
}
*/
import "C"
func getUid(user string) (uint64, error) {
u, err := C.getuid(C.CString(user))
if err != nil {
return 0, err
}
return uint64(u), nil
}
func getGid(group string) (uint64, error) {
g, err := C.getgid(C.CString(group))
if err != nil {
return 0, err
}
return uint64(g), nil
}

133
cmd/exec/exec.go Normal file
View File

@ -0,0 +1,133 @@
package main
import (
"errors"
"fmt"
"log"
"os"
"os/exec"
"strconv"
"strings"
"gitea.suyono.dev/suyono/wingmate"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"golang.org/x/sys/unix"
)
const (
setsidFlag = "setsid"
EnvSetsid = "SETSID"
userFlag = "user"
EnvUser = "USER"
)
var (
rootCmd = &cobra.Command{
Use: "wmexec",
RunE: execCmd,
}
childArgs []string
)
func main() {
var (
found bool
i int
arg string
selfArgs []string
)
rootCmd.PersistentFlags().BoolP(setsidFlag, "s", false, "set to true to run setsid() before exec")
viper.BindPFlag(EnvSetsid, rootCmd.PersistentFlags().Lookup(setsidFlag))
rootCmd.PersistentFlags().StringP(userFlag, "u", "", "\"user:[group]\"")
viper.BindPFlag(EnvUser, rootCmd.PersistentFlags().Lookup(userFlag))
viper.SetEnvPrefix(wingmate.EnvPrefix)
viper.BindEnv(EnvUser)
viper.BindEnv(EnvSetsid)
viper.SetDefault(EnvSetsid, false)
viper.SetDefault(EnvUser, "")
found = false
for i, arg = range os.Args {
if arg == "--" {
found = true
if len(os.Args) <= i+1 {
log.Println("invalid argument")
os.Exit(1)
}
selfArgs = os.Args[1:i]
childArgs = os.Args[i+1:]
break
}
}
if !found {
log.Println("invalid argument")
os.Exit(1)
}
if len(childArgs) == 0 {
log.Println("invalid argument")
os.Exit(1)
}
rootCmd.SetArgs(selfArgs)
if err := rootCmd.Execute(); err != nil {
log.Println(err)
os.Exit(1)
}
}
func execCmd(cmd *cobra.Command, args []string) error {
if viper.GetBool(EnvSetsid) {
_, _ = unix.Setsid()
}
var (
uid uint64
gid uint64
err error
path string
)
ug := viper.GetString(EnvUser)
if len(ug) > 0 {
user, group, ok := strings.Cut(ug, ":")
if ok {
if gid, err = strconv.ParseUint(group, 10, 32); err != nil {
if gid, err = getGid(group); err != nil {
return fmt.Errorf("cgo getgid: %w", err)
}
}
if err = unix.Setgid(int(gid)); err != nil {
return fmt.Errorf("setgid: %w", err)
}
}
uid, err = strconv.ParseUint(user, 10, 32)
if err != nil {
if uid, err = getUid(user); err != nil {
return fmt.Errorf("cgo getuid: %w", err)
}
}
if err = unix.Setuid(int(uid)); err != nil {
return fmt.Errorf("setuid: %w", err)
}
}
if path, err = exec.LookPath(childArgs[0]); err != nil {
if !errors.Is(err, exec.ErrDot) {
return fmt.Errorf("lookpath: %w", err)
}
}
if err = unix.Exec(path, childArgs, os.Environ()); err != nil {
return fmt.Errorf("exec: %w", err)
}
return nil
}

View File

@ -3,3 +3,6 @@ all:
clean:
go clean -i -cache -testcache
install:
install dummy ${DESTDIR}/wmdummy

View File

@ -3,3 +3,6 @@ all:
clean:
go clean -i -cache -testcache
install:
install oneshot ${DESTDIR}/wmoneshot

View File

@ -3,3 +3,6 @@ all:
clean:
go clean -i -cache -testcache
install:
install spawner ${DESTDIR}/wmspawner

View File

@ -3,3 +3,6 @@ all:
clean:
go clean -i -cache -testcache
install:
install starter ${DESTDIR}/wmstarter

View File

@ -3,3 +3,6 @@ all:
clean:
go clean -i -cache -testcache
install:
install pidproxy ${DESTDIR}/wmpidproxy

View File

@ -4,6 +4,7 @@ import (
"log"
"os"
"os/exec"
"os/signal"
"strconv"
"syscall"
"time"
@ -41,11 +42,6 @@ func main() {
viper.BindEnv(EnvStartSecs)
viper.SetDefault(EnvStartSecs, EnvDefaultStartSecs)
if len(os.Args) <= 2 {
log.Println("invalid argument")
os.Exit(1)
}
rootCmd.PersistentFlags().StringP(pidFileFlag, "p", "", "location of pid file")
rootCmd.MarkFlagRequired(pidFileFlag)
viper.BindPFlag(pidFileFlag, rootCmd.PersistentFlags().Lookup(pidFileFlag))
@ -54,11 +50,11 @@ func main() {
for i, arg = range os.Args {
if arg == "--" {
found = true
selfArgs = os.Args[1:i]
if len(os.Args) <= i+1 {
log.Println("invalid argument")
os.Exit(1)
}
selfArgs = os.Args[1:i]
childArgs = os.Args[i+1:]
break
}
@ -68,6 +64,11 @@ func main() {
os.Exit(1)
}
if len(childArgs) == 0 {
log.Println("invalid argument")
os.Exit(1)
}
rootCmd.SetArgs(selfArgs)
if err := rootCmd.Execute(); err != nil {
@ -90,7 +91,16 @@ func pidProxy(cmd *cobra.Command, args []string) error {
var (
err error
pid int
sc chan os.Signal
t *time.Timer
)
sc = make(chan os.Signal, 1)
signal.Notify(sc, unix.SIGTERM)
t = time.NewTimer(time.Second)
check:
for {
if pid, err = readPid(pidfile); err != nil {
return err
@ -100,8 +110,20 @@ func pidProxy(cmd *cobra.Command, args []string) error {
return err
}
time.Sleep(time.Second)
select {
case <-t.C:
case <-sc:
if pid, err = readPid(pidfile); err != nil {
return err
}
if err = unix.Kill(pid, unix.SIGTERM); err != nil {
return err
}
break check
}
}
return nil
}
func readPid(pidFile string) (int, error) {

View File

@ -3,3 +3,7 @@ all:
clean:
go clean -i -cache -testcache
install:
install wingmate ${DESTDIR}/wingmate

View File

@ -7,6 +7,7 @@ import (
"gitea.suyono.dev/suyono/wingmate"
"github.com/spf13/viper"
"golang.org/x/sys/unix"
)
const (
@ -48,8 +49,11 @@ func Read() (*Config, error) {
if len(dirent) > 0 {
for _, d := range dirent {
if d.Type().IsRegular() {
svcPath := filepath.Join(svcdir, d.Name())
if err = unix.Access(svcPath, unix.X_OK); err == nil {
serviceAvailable = true
outConfig.ServicePaths = append(outConfig.ServicePaths, filepath.Join(svcdir, d.Name()))
outConfig.ServicePaths = append(outConfig.ServicePaths, svcPath)
}
}
}
}

130
config/config_test.go Normal file
View File

@ -0,0 +1,130 @@
package config
import (
"os"
"path"
"testing"
"gitea.suyono.dev/suyono/wingmate"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
)
func TestRead(t *testing.T) {
type testEntry struct {
name string
testFunc func(t *testing.T)
}
var (
configDir string
err error
)
const serviceDir = "service"
setup := func(t *testing.T) {
if configDir, err = os.MkdirTemp("", "wingmate-*-test"); err != nil {
t.Fatal("setup", err)
}
viper.Set(EnvConfigPath, configDir)
}
tear := func(t *testing.T) {
if err = os.RemoveAll(configDir); err != nil {
t.Fatal("tear", err)
}
}
mkSvcDir := func(t *testing.T) {
if err := os.MkdirAll(path.Join(configDir, serviceDir), 0755); err != nil {
t.Fatal("create dir", err)
}
}
touchFile := func(t *testing.T, name string, perm os.FileMode) {
f, err := os.OpenFile(name, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, perm)
if err != nil {
t.Fatal("create file", err)
}
_ = f.Close()
}
_ = wingmate.NewLog(os.Stderr)
tests := []testEntry{
{
name: "positive",
testFunc: func(t *testing.T) {
mkSvcDir(t)
touchFile(t, path.Join(configDir, serviceDir, "one.sh"), 0755)
touchFile(t, path.Join(configDir, serviceDir, "two.sh"), 0755)
cfg, err := Read()
assert.Nil(t, err)
assert.ElementsMatch(
t,
cfg.ServicePaths,
[]string{
path.Join(configDir, serviceDir, "one.sh"),
path.Join(configDir, serviceDir, "two.sh"),
},
)
},
},
{
name: "with directory",
testFunc: func(t *testing.T) {
const subdir1 = "subdir1"
mkSvcDir(t)
assert.Nil(t, os.Mkdir(path.Join(configDir, serviceDir, subdir1), 0755))
touchFile(t, path.Join(configDir, serviceDir, subdir1, "one.sh"), 0755)
touchFile(t, path.Join(configDir, serviceDir, "two.sh"), 0755)
cfg, err := Read()
assert.Nil(t, err)
assert.ElementsMatch(
t,
cfg.ServicePaths,
[]string{
path.Join(configDir, serviceDir, "two.sh"),
},
)
},
},
{
name: "wrong mode",
testFunc: func(t *testing.T) {
mkSvcDir(t)
touchFile(t, path.Join(configDir, serviceDir, "one.sh"), 0755)
touchFile(t, path.Join(configDir, serviceDir, "two.sh"), 0644)
cfg, err := Read()
assert.Nil(t, err)
assert.ElementsMatch(
t,
cfg.ServicePaths,
[]string{
path.Join(configDir, serviceDir, "one.sh"),
},
)
},
},
{
name: "empty",
testFunc: func(t *testing.T) {
mkSvcDir(t)
_, err := Read()
assert.NotNil(t, err)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
setup(t)
tt.testFunc(t)
tear(t)
})
}
}

View File

@ -2,19 +2,16 @@ FROM golang:1.21-alpine as builder
ADD . /root/wingmate
WORKDIR /root/wingmate/
RUN apk add make && make all
RUN apk add make build-base && CGO_ENABLED=1 make all && make DESTDIR=/usr/local/bin/wingmate install
FROM alpine:3.18
RUN apk add tzdata && ln -s /usr/share/zoneinfo/Australia/Sydney /etc/localtime
COPY --from=builder /root/wingmate/cmd/wingmate/wingmate /usr/local/bin/wingmate
COPY --from=builder /root/wingmate/cmd/experiment/dummy/dummy /usr/local/bin/wmdummy
COPY --from=builder /root/wingmate/cmd/experiment/starter/starter /usr/local/bin/wmstarter
COPY --from=builder /root/wingmate/cmd/experiment/oneshot/oneshot /usr/local/bin/wmoneshot
COPY --from=builder /root/wingmate/cmd/experiment/spawner/spawner /usr/local/bin/wmspawner
COPY --from=builder /root/wingmate/cmd/pidproxy/pidproxy /usr/local/bin/wmpidproxy
RUN apk add tzdata && ln -s /usr/share/zoneinfo/Australia/Sydney /etc/localtime && \
adduser -h /home/user1 -D -s /bin/sh user1 && \
adduser -h /home/user2 -D -s /bin/sh user2
COPY --from=builder /usr/local/bin/wingmate/ /usr/local/bin/
ADD --chmod=755 docker/alpine/entry.sh /usr/local/bin/entry.sh
ADD --chmod=755 docker/alpine/etc /etc

View File

@ -1,4 +1,4 @@
#!/bin/sh
export DUMMY_PATH=/usr/local/bin/wmdummy
exec /usr/local/bin/wmstarter
exec /usr/local/bin/wmexec --setsid --user user1:user1 -- /usr/local/bin/wmstarter

View File

@ -2,4 +2,4 @@
export WINGMATE_ONESHOT_PATH=/usr/local/bin/wmoneshot
export WINGMATE_DUMMY_PATH=/usr/local/bin/wmdummy
exec /usr/local/bin/wmspawner
exec /usr/local/bin/wmexec --user 1001 -- /usr/local/bin/wmspawner

View File

@ -2,19 +2,16 @@ FROM golang:1.21-bookworm as builder
ADD . /root/wingmate
WORKDIR /root/wingmate/
RUN make all
RUN make all && make DESTDIR=/usr/local/bin/wingmate install
FROM debian:bookworm
RUN ln -sf /usr/share/zoneinfo/Australia/Sydney /etc/localtime
COPY --from=builder /root/wingmate/cmd/wingmate/wingmate /usr/local/bin/wingmate
COPY --from=builder /root/wingmate/cmd/experiment/dummy/dummy /usr/local/bin/wmdummy
COPY --from=builder /root/wingmate/cmd/experiment/starter/starter /usr/local/bin/wmstarter
COPY --from=builder /root/wingmate/cmd/experiment/oneshot/oneshot /usr/local/bin/wmoneshot
COPY --from=builder /root/wingmate/cmd/experiment/spawner/spawner /usr/local/bin/wmspawner
COPY --from=builder /root/wingmate/cmd/pidproxy/pidproxy /usr/local/bin/wmpidproxy
RUN ln -sf /usr/share/zoneinfo/Australia/Sydney /etc/localtime && \
apt update && apt install -y procps && \
useradd -m -s /bin/bash user1
COPY --from=builder /usr/local/bin/wingmate/ /usr/local/bin/
ADD --chmod=755 docker/bookworm/entry.sh /usr/local/bin/entry.sh
ADD --chmod=755 docker/bookworm/etc /etc

View File

@ -1,4 +1,4 @@
#!/usr/bin/bash
export DUMMY_PATH=/usr/local/bin/wmdummy
exec /usr/local/bin/wmstarter
exec /usr/local/bin/wmexec --setsid --user user1:user1 -- /usr/local/bin/wmstarter

View File

@ -2,4 +2,4 @@
export WINGMATE_ONESHOT_PATH=/usr/local/bin/wmoneshot
export WINGMATE_DUMMY_PATH=/usr/local/bin/wmdummy
exec /usr/local/bin/wmspawner
exec /usr/local/bin/wmexec --user 1200 -- /usr/local/bin/wmspawner

1
go.mod
View File

@ -28,6 +28,7 @@ require (
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.1 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect

3
go.sum
View File

@ -201,12 +201,15 @@ github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0=
github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=

View File

@ -1,6 +1,7 @@
package init
import (
"os"
"sync"
"time"
)
@ -34,17 +35,19 @@ func (i *Init) Start() {
wg *sync.WaitGroup
signalTrigger chan any
sighandlerExit chan any
sigchld chan os.Signal
)
signalTrigger = make(chan any)
sighandlerExit = make(chan any)
sigchld = make(chan os.Signal, 1)
wg = &sync.WaitGroup{}
wg.Add(1)
go i.waiter(wg, signalTrigger, sighandlerExit)
go i.waiter(wg, signalTrigger, sighandlerExit, sigchld)
wg.Add(1)
go i.sighandler(wg, signalTrigger, sighandlerExit)
go i.sighandler(wg, signalTrigger, sighandlerExit, sigchld)
for _, s := range i.config.Services() {
wg.Add(1)

View File

@ -9,7 +9,7 @@ import (
"golang.org/x/sys/unix"
)
func (i *Init) sighandler(wg *sync.WaitGroup, trigger chan<- any, selfExit <-chan any) {
func (i *Init) sighandler(wg *sync.WaitGroup, trigger chan<- any, selfExit <-chan any, sigchld chan<- os.Signal) {
defer wg.Done()
defer func() {
@ -35,7 +35,10 @@ signal:
isOpen = false
}
case unix.SIGCHLD:
// do nothing
select {
case sigchld <- s:
default:
}
}
case <-selfExit:

View File

@ -2,20 +2,20 @@ package init
import (
"errors"
"os"
"sync"
"time"
"gitea.suyono.dev/suyono/wingmate"
"golang.org/x/sys/unix"
)
func (i *Init) waiter(wg *sync.WaitGroup, runningFlag <-chan any, sigHandlerFlag chan<- any) {
func (i *Init) waiter(wg *sync.WaitGroup, runningFlag <-chan any, sigHandlerFlag chan<- any, sigchld <-chan os.Signal) {
var (
ws unix.WaitStatus
// pid int
err error
running bool
flagged bool
waitingForSignal bool
)
defer wg.Done()
@ -25,15 +25,28 @@ func (i *Init) waiter(wg *sync.WaitGroup, runningFlag <-chan any, sigHandlerFlag
running = true
flagged = true
waitingForSignal = true
wait:
for {
if running {
if waitingForSignal {
select {
case <-runningFlag:
wingmate.Log().Info().Msg("waiter received shutdown signal...")
running = false
case <-sigchld:
waitingForSignal = false
}
} else {
select {
case <-runningFlag:
wingmate.Log().Info().Msg("waiter received shutdown signal...")
running = false
default:
}
}
}
if _, err = unix.Wait4(-1, &ws, 0, nil); err != nil {
@ -50,7 +63,7 @@ wait:
}
wingmate.Log().Warn().Msgf("Wait4 returns error: %+v", err)
time.Sleep(time.Millisecond * 100)
waitingForSignal = true
}
}
}