fix(splitargs): wrong code, check should be outside of loop

feat(config): added WMPidProxyCheckVersion and WMExecCheckVersion to the interface; mutex for accessing viper
fixed(docker/bookworm-newconfig): golang version and config path
feat(UtilDepCheck): added utility dependency check before running the task
This commit is contained in:
Suyono 2024-03-27 23:04:30 +11:00
parent a63646aab2
commit 7db6f6f8f3
13 changed files with 295 additions and 40 deletions

View File

@ -2,10 +2,9 @@ package cli
import ( import (
"errors" "errors"
"os"
) )
func SplitArgs() ([]string, []string, error) { func SplitArgs(args []string) ([]string, []string, error) {
var ( var (
i int i int
arg string arg string
@ -13,25 +12,25 @@ func SplitArgs() ([]string, []string, error) {
childArgs []string childArgs []string
) )
found := false found := false
for i, arg = range os.Args { for i, arg = range args {
if arg == "--" { if arg == "--" {
found = true found = true
if i+1 == len(os.Args) { if i+1 == len(args) {
return nil, nil, errors.New("invalid argument") return nil, nil, errors.New("invalid argument")
} }
if len(os.Args[i+1:]) == 0 { if len(args[i+1:]) == 0 {
return nil, nil, errors.New("invalid argument") return nil, nil, errors.New("invalid argument")
} }
selfArgs = os.Args[1:i] selfArgs = args[1:i]
childArgs = os.Args[i+1:] childArgs = args[i+1:]
break break
} }
}
if !found { if !found {
return nil, nil, errors.New("invalid argument") return nil, nil, errors.New("invalid argument")
}
} }
return selfArgs, childArgs, nil return selfArgs, childArgs, nil

View File

@ -0,0 +1,7 @@
package cli
import "testing"
func TestSplitArgs(t *testing.T) {
SplitArgs([]string{"wmexec", "--user", "1200", "--", "wmspawner"})
}

View File

@ -71,7 +71,7 @@ func main() {
app.version.Cmd(rootCmd) app.version.Cmd(rootCmd)
selfArgs, childArgs, err = cli.SplitArgs() selfArgs, childArgs, err = cli.SplitArgs(os.Args)
app.childArgs = childArgs app.childArgs = childArgs
app.err = err app.err = err

View File

@ -68,7 +68,7 @@ func main() {
app.version.Flag(rootCmd) app.version.Flag(rootCmd)
app.version.Cmd(rootCmd) app.version.Cmd(rootCmd)
selfArgs, childArgs, err = cli.SplitArgs() selfArgs, childArgs, err = cli.SplitArgs(os.Args)
app.childArgs = childArgs app.childArgs = childArgs
app.err = err app.err = err

View File

@ -53,7 +53,7 @@ func convert(cfg *config.Config) *wConfig {
ct := task.NewCronTask(c.Name).SetCommand(c.Command...).SetEnv(c.Environ...) ct := task.NewCronTask(c.Name).SetCommand(c.Command...).SetEnv(c.Environ...)
ct.SetFlagSetsid(c.Setsid).SetWorkingDir(c.WorkingDir).SetUser(c.User).SetGroup(c.Group) ct.SetFlagSetsid(c.Setsid).SetWorkingDir(c.WorkingDir).SetUser(c.User).SetGroup(c.Group)
ct.SetSchedule(schedule) ct.SetSchedule(c.Schedule, schedule)
ct.SetConfig(cfg) ct.SetConfig(cfg)
retval.tasks.AddCron(ct) retval.tasks.AddCron(ct)

View File

@ -2,6 +2,7 @@ package config
import ( import (
"errors" "errors"
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -108,6 +109,7 @@ func Read() (*Config, error) {
serviceAvailable = false serviceAvailable = false
cronAvailable = false cronAvailable = false
outConfig := &Config{ outConfig := &Config{
viperMtx: &sync.Mutex{},
ServiceV0: make([]string, 0), ServiceV0: make([]string, 0),
} }
configPath := viper.GetString(ConfigPath) configPath := viper.GetString(ConfigPath)
@ -160,6 +162,13 @@ func Read() (*Config, error) {
return outConfig, nil return outConfig, nil
} }
func (c *Config) GetAppVersion() string {
c.viperMtx.Lock()
defer c.viperMtx.Unlock()
return viper.GetString(WingmateVersion)
}
func (c *Config) WMPidProxyPath() string { func (c *Config) WMPidProxyPath() string {
c.viperMtx.Lock() c.viperMtx.Lock()
defer c.viperMtx.Unlock() defer c.viperMtx.Unlock()
@ -167,9 +176,46 @@ func (c *Config) WMPidProxyPath() string {
return viper.GetString(PidProxyPathConfig) return viper.GetString(PidProxyPathConfig)
} }
func (c *Config) WMPidProxyCheckVersion() error {
var (
binVersion string
appVersion string
err error
)
if binVersion, err = getVersion(c.WMPidProxyPath()); err != nil {
return fmt.Errorf("get wmpidproxy version: %w", err)
}
appVersion = c.GetAppVersion()
if appVersion != binVersion {
return fmt.Errorf("wmpidproxy version mismatch")
}
return nil
}
func (c *Config) WMExecPath() string { func (c *Config) WMExecPath() string {
c.viperMtx.Lock() c.viperMtx.Lock()
defer c.viperMtx.Unlock() defer c.viperMtx.Unlock()
return viper.GetString(ExecPathConfig) return viper.GetString(ExecPathConfig)
} }
func (c *Config) WMExecCheckVersion() error {
var (
binVersion string
appVersion string
err error
)
if binVersion, err = getVersion(c.WMExecPath()); err != nil {
return fmt.Errorf("get wmexec version: %w", err)
}
appVersion = c.GetAppVersion()
if appVersion != binVersion {
return fmt.Errorf("wmexec version mismatch")
}
return nil
}

View File

@ -1,4 +1,4 @@
FROM golang:1.21-bookworm as builder FROM golang:1.22-bookworm as builder
ADD . /root/wingmate ADD . /root/wingmate
WORKDIR /root/wingmate/ WORKDIR /root/wingmate/
@ -13,8 +13,8 @@ RUN ln -sf /usr/share/zoneinfo/Australia/Sydney /etc/localtime && \
apt update && apt install -y procps && \ apt update && apt install -y procps && \
useradd -m -s /bin/bash user1 useradd -m -s /bin/bash user1
COPY --from=builder /usr/local/bin/wingmate/ /usr/local/bin/ 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-newconfig/entry.sh /usr/local/bin/entry.sh
ADD --chmod=755 docker/bookworm/etc /etc ADD --chmod=755 docker/bookworm-newconfig/etc /etc
ENTRYPOINT [ "/usr/local/bin/entry.sh" ] ENTRYPOINT [ "/usr/local/bin/entry.sh" ]
CMD [ "/usr/local/bin/wingmate" ] CMD [ "/usr/local/bin/wingmate" ]

View File

@ -1,5 +1,8 @@
service: service:
one: # one:
command: [ "/workspace/wingmate/cmd/experiment/starter/starter" ] # command: [ "wmstarter" ]
environ: [ "DUMMY_PATH=/workspace/wingmate/cmd/experiment/dummy/dummy" ] # environ: [ "DUMMY_PATH=/workspace/wingmate/cmd/experiment/dummy/dummy" ]
spawner:
command: [ "wmspawner" ]
user: "1200"

View File

@ -29,6 +29,10 @@ cron:
for { for {
if cron.TimeToRun(time.Now()) { if cron.TimeToRun(time.Now()) {
wingmate.Log().Info().Str(cronTag, cron.Name()).Msg("executing") wingmate.Log().Info().Str(cronTag, cron.Name()).Msg("executing")
if err = cron.UtilDepCheck(); err != nil {
wingmate.Log().Error().Str(cronTag, cron.Name()).Msgf("%+v", err)
goto fail
}
cmd = exec.Command(cron.Command(), cron.Arguments()...) cmd = exec.Command(cron.Command(), cron.Arguments()...)
iwg = &sync.WaitGroup{} iwg = &sync.WaitGroup{}

View File

@ -30,6 +30,7 @@ type Task interface {
UserGroup() UserGroup UserGroup() UserGroup
WorkingDir() string WorkingDir() string
Status() TaskStatus Status() TaskStatus
UtilDepCheck() error
} }
type CronTask interface { type CronTask interface {

View File

@ -33,6 +33,11 @@ func (i *Init) service(wg *sync.WaitGroup, task ServiceTask, exitFlag <-chan any
service: service:
for { for {
failStatus = false failStatus = false
if err = task.UtilDepCheck(); err != nil {
wingmate.Log().Error().Str(serviceTag, task.Name()).Msgf("%+v", err)
failStatus = true
goto fail
}
cmd = exec.Command(task.Command(), task.Arguments()...) cmd = exec.Command(task.Command(), task.Arguments()...)
iwg = &sync.WaitGroup{} iwg = &sync.WaitGroup{}

View File

@ -1,6 +1,10 @@
package task package task
import ( import (
"crypto/sha256"
"encoding/json"
"fmt"
"gitea.suyono.dev/suyono/wingmate"
"time" "time"
wminit "gitea.suyono.dev/suyono/wingmate/init" wminit "gitea.suyono.dev/suyono/wingmate/init"
@ -70,14 +74,16 @@ func (cms *CronMultiOccurrenceSpec) Match(u uint8) bool {
type CronTask struct { type CronTask struct {
CronSchedule CronSchedule
userGroup userGroup
name string cronScheduleString string
command []string name string
environ []string command []string
setsid bool cmdLine []string
workingDir string environ []string
lastRun time.Time setsid bool
hasRun bool //NOTE: make sure initialised as false workingDir string
config config lastRun time.Time
hasRun bool //NOTE: make sure initialised as false
config config
} }
func NewCronTask(name string) *CronTask { func NewCronTask(name string) *CronTask {
@ -119,7 +125,8 @@ func (c *CronTask) SetGroup(group string) *CronTask {
return c return c
} }
func (c *CronTask) SetSchedule(schedule CronSchedule) *CronTask { func (c *CronTask) SetSchedule(scheduleStr string, schedule CronSchedule) *CronTask {
c.cronScheduleString = scheduleStr
c.CronSchedule = schedule c.CronSchedule = schedule
return c return c
} }
@ -129,21 +136,104 @@ func (c *CronTask) SetConfig(config config) *CronTask {
return c return c
} }
func (c *CronTask) Equals(another *CronTask) bool {
if another == nil {
return false
}
type toCompare struct {
Name string
Command string
Arguments []string
Environ []string
Setsid bool
UserGroup string
WorkingDir string
Schedule string
}
cmpStruct := func(p *CronTask) ([]byte, error) {
s := &toCompare{
Name: c.Name(),
Command: c.Command(),
Arguments: c.Arguments(),
Environ: c.Environ(),
Setsid: c.Setsid(),
UserGroup: c.UserGroup().String(),
WorkingDir: c.WorkingDir(),
Schedule: c.cronScheduleString,
}
return json.Marshal(s)
}
var (
err error
ours, theirs []byte
ourHash, theirHash [sha256.Size]byte
)
if ours, err = cmpStruct(c); err != nil {
wingmate.Log().Error().Msgf("cron task equals: %+v", err)
return false
}
ourHash = sha256.Sum256(ours)
if theirs, err = cmpStruct(another); err != nil {
wingmate.Log().Error().Msgf("cron task equals: %+v", err)
return false
}
theirHash = sha256.Sum256(theirs)
for i := 0; i < sha256.Size; i++ {
if ourHash[i] != theirHash[i] {
return false
}
}
return true
}
func (c *CronTask) Name() string { func (c *CronTask) Name() string {
return c.name return c.name
} }
func (c *CronTask) UtilDepCheck() error {
c.cmdLine = make([]string, 0)
if c.setsid || c.UserGroup().IsSet() {
if err := c.config.WMExecCheckVersion(); err != nil {
return fmt.Errorf("utility dependency check: %w", err)
}
c.cmdLine = append(c.cmdLine, c.config.WMExecPath())
if c.setsid {
c.cmdLine = append(c.cmdLine, "--setsid")
}
if c.UserGroup().IsSet() {
c.cmdLine = append(c.cmdLine, "--user", c.UserGroup().String())
}
c.cmdLine = append(c.cmdLine, "--")
}
c.cmdLine = append(c.cmdLine, c.command...)
return nil
}
func (c *CronTask) Command() string { func (c *CronTask) Command() string {
return c.command[0] return c.cmdLine[0]
} }
func (c *CronTask) Arguments() []string { func (c *CronTask) Arguments() []string {
if len(c.command) == 1 { if len(c.cmdLine) == 1 {
return nil return nil
} }
retval := make([]string, len(c.command)-1) retval := make([]string, len(c.cmdLine)-1)
copy(retval, c.command[1:]) copy(retval, c.cmdLine[1:])
return retval return retval
} }

View File

@ -1,14 +1,19 @@
package task package task
import ( import (
"crypto/sha256"
"encoding/json"
"fmt" "fmt"
"gitea.suyono.dev/suyono/wingmate"
wminit "gitea.suyono.dev/suyono/wingmate/init" wminit "gitea.suyono.dev/suyono/wingmate/init"
) )
type config interface { type config interface {
WMPidProxyPath() string WMPidProxyPath() string
WMPidProxyCheckVersion() error
WMExecPath() string WMExecPath() string
WMExecCheckVersion() error
} }
type Tasks struct { type Tasks struct {
@ -33,7 +38,7 @@ func (ts *Tasks) AddService(serviceTask *ServiceTask) *ServiceTask {
} }
func (ts *Tasks) AddV0Cron(schedule CronSchedule, path string) { func (ts *Tasks) AddV0Cron(schedule CronSchedule, path string) {
ts.AddCron(NewCronTask(path)).SetCommand(path).SetSchedule(schedule) ts.AddCron(NewCronTask(path)).SetCommand(path).SetSchedule("", schedule)
} }
func (ts *Tasks) AddCron(cronTask *CronTask) *CronTask { func (ts *Tasks) AddCron(cronTask *CronTask) *CronTask {
@ -138,6 +143,70 @@ func (t *ServiceTask) SetConfig(config config) *ServiceTask {
return t return t
} }
func (t *ServiceTask) Equals(another *ServiceTask) bool {
if another == nil {
return false
}
type toCompare struct {
Name string
Command string
Arguments []string
Environ []string
Setsid bool
UserGroup string
WorkingDir string
PidFile string
StartSecs uint
AutoStart bool
AutoRestart bool
}
cmpStruct := func(p *ServiceTask) ([]byte, error) {
s := &toCompare{
Name: p.Name(),
Command: p.Command(),
Arguments: p.Arguments(),
Environ: p.Environ(),
Setsid: p.Setsid(),
UserGroup: p.UserGroup().String(),
WorkingDir: p.WorkingDir(),
PidFile: p.PidFile(),
StartSecs: p.StartSecs(),
AutoStart: p.AutoStart(),
AutoRestart: p.AutoRestart(),
}
return json.Marshal(s)
}
var (
err error
ours, theirs []byte
ourHash, theirHash [sha256.Size]byte
)
if ours, err = cmpStruct(t); err != nil {
wingmate.Log().Error().Msgf("task equals: %+v", err)
return false
}
ourHash = sha256.Sum256(ours)
if theirs, err = cmpStruct(another); err != nil {
wingmate.Log().Error().Msgf("task equals: %+v", err)
return false
}
theirHash = sha256.Sum256(theirs)
for i := 0; i < sha256.Size; i++ {
if ourHash[i] != theirHash[i] {
return false
}
}
return true
}
func (t *ServiceTask) Validate() error { func (t *ServiceTask) Validate() error {
// call this function for validate the field // call this function for validate the field
return validate( /* input the validators here */ ) return validate( /* input the validators here */ )
@ -176,19 +245,50 @@ func (t *ServiceTask) prepareCommandLine() []string {
return t.cmdLine return t.cmdLine
} }
func (t *ServiceTask) UtilDepCheck() error {
t.cmdLine = make([]string, 0)
if t.background {
if err := t.config.WMPidProxyCheckVersion(); err != nil {
return fmt.Errorf("utility dependency check: %w", err)
}
t.cmdLine = append(t.cmdLine, t.config.WMPidProxyPath(), "--pid-file", t.pidFile, "--")
}
if t.setsid || t.UserGroup().IsSet() {
if err := t.config.WMExecCheckVersion(); err != nil {
return fmt.Errorf("utility dependency check: %w", err)
}
t.cmdLine = append(t.cmdLine, t.config.WMExecPath())
if t.setsid {
t.cmdLine = append(t.cmdLine, "--setsid")
}
if t.UserGroup().IsSet() {
t.cmdLine = append(t.cmdLine, "--user", t.UserGroup().String())
}
t.cmdLine = append(t.cmdLine, "--")
}
t.cmdLine = append(t.cmdLine, t.command...)
return nil
}
func (t *ServiceTask) Command() string { func (t *ServiceTask) Command() string {
cl := t.prepareCommandLine() return t.cmdLine[0]
return cl[0]
} }
func (t *ServiceTask) Arguments() []string { func (t *ServiceTask) Arguments() []string {
cl := t.prepareCommandLine() if len(t.cmdLine) == 1 {
if len(cl) == 1 {
return nil return nil
} }
retval := make([]string, len(cl)-1) retval := make([]string, len(t.cmdLine)-1)
copy(retval, cl[1:]) copy(retval, t.cmdLine[1:])
return retval return retval
} }