21 Commits
main ... rework

Author SHA1 Message Date
439baa60fb wip: ready for initial test 2023-12-07 04:47:00 +00:00
ca6c2de4dc wip: config 2023-12-07 03:16:37 +00:00
c5e20f7910 wip 2023-12-06 23:04:19 +11:00
dbb703db61 revamp: start simple 2023-12-06 17:01:43 +11:00
4f54db3cbd chore: update golang version and tools-versions 2023-12-06 11:27:51 +11:00
f482eb8631 wip: default writer tested 2023-09-11 13:56:38 +10:00
d406fe698f wip: log 2023-09-11 12:28:53 +10:00
277e8feba1 wip: log 2023-09-11 10:53:21 +10:00
5050a4fdba wip: log 2023-09-09 17:42:39 +10:00
5f7befe02c wip: log 2023-09-09 14:07:49 +10:00
8a480acde7 wip: log 2023-09-06 03:37:43 +10:00
1f25e5249c wip: log 2023-09-05 14:22:36 +10:00
a5a019b28b wip: log 2023-09-05 13:09:53 +10:00
9f7c004105 wip: log 2023-09-05 10:20:49 +10:00
6eada2d348 wip: log 2023-09-04 15:58:41 +10:00
5621312be1 wip: log 2023-09-04 12:49:48 +10:00
badd2e3543 wip: log 2023-09-03 12:11:35 +10:00
91147f073f wip: daemon 2023-09-01 12:29:31 +10:00
eac78ec322 wip: test signal 2023-08-31 08:55:48 +10:00
92f7873f98 wip: process management 2023-08-31 08:05:20 +10:00
9cbdbebee5 wip: initial work 2023-08-28 18:12:31 +10:00
71 changed files with 783 additions and 4182 deletions

View File

@@ -1,25 +1,10 @@
{ {
"name": "Golang Dev", "name": "Golang Dev",
"image": "golang-dev:1.22-bookworm-user", "image": "golang-dev:1.21-bookworm-user",
"mounts": [
{
"source": "WingmateGoPath",
"target": "/go",
"type": "volume"
},
{
"source": "WingmateGolangDevHome",
"target": "/home/golang",
"type": "volume"
}
],
"customizations": { "customizations": {
"vscode": { "vscode": {
"extensions": [ "extensions": [
"golang.go", "golang.go"
"ms-azuretools.vscode-docker",
"ms-vscode.makefile-tools",
"ms-vscode.cpptools-extension-pack"
] ]
} }
} }

8
.gitignore vendored
View File

@@ -1,6 +1,2 @@
/cmd/wingmate/wingmate /.idea
/cmd/wingmate/version.txt /wingmate
/cmd/pidproxy/pidproxy
/cmd/pidproxy/version.txt
/cmd/exec/exec
/cmd/exec/version.txt

8
.idea/.gitignore generated vendored
View File

@@ -1,8 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

8
.idea/modules.xml generated
View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/wingmate.iml" filepath="$PROJECT_DIR$/.idea/wingmate.iml" />
</modules>
</component>
</project>

View File

@@ -1,55 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteTargetsManager">
<targets>
<target name="golang-dev:1.21-bookworm-user" type="docker" uuid="5b79636b-3db9-4e2a-9fd3-03277653ae58">
<config>
<option name="targetPlatform">
<TargetPlatform />
</option>
<option name="buildImageConfig">
<BuildImageConfig>
<option name="dockerFile" value="docker/bookworm/Dockerfile" />
</BuildImageConfig>
</option>
<option name="buildNotPull" value="false" />
<option name="containerConfig">
<ContainerConfig>
<option name="runCliOptions" value="-u &quot;1000:1000&quot; --rm" />
</ContainerConfig>
</option>
<option name="pullImageConfig">
<PullImageConfig>
<option name="tagToPull" value="golang-dev:1.21-bookworm-user" />
</PullImageConfig>
</option>
</config>
<ContributedStateBase type="GoLanguageRuntime">
<config>
<option name="compiledExecutablesVolume">
<VolumeState>
<option name="targetSpecificBits">
<map>
<entry key="mountAsVolume" value="false" />
</map>
</option>
</VolumeState>
</option>
<option name="goPath" value="/go" />
<option name="goRoot" value="/usr/local/go/bin/go" />
<option name="goVersion" value="go1.21.5 linux/amd64" />
<option name="projectSourcesVolume">
<VolumeState>
<option name="targetSpecificBits">
<map>
<entry key="mountAsVolume" value="false" />
</map>
</option>
</VolumeState>
</option>
</config>
</ContributedStateBase>
</target>
</targets>
</component>
</project>

6
.idea/vcs.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

9
.idea/wingmate.iml generated
View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

1
.tool-versions Normal file
View File

@@ -0,0 +1 @@
golang 1.21.4

View File

@@ -1,59 +0,0 @@
DESTDIR = /usr/local/bin
installs = install-dir
programs = wingmate pidproxy exec
ifdef TEST_BUILD
programs += oneshot spawner starter dummy bg
installs += install-test
endif
all: ${programs}
wingmate:
$(MAKE) -C cmd/wingmate all
pidproxy:
$(MAKE) -C cmd/pidproxy all
exec:
$(MAKE) -C cmd/exec all
dummy:
$(MAKE) -C cmd/experiment/dummy all
oneshot:
$(MAKE) -C cmd/experiment/oneshot all
spawner:
$(MAKE) -C cmd/experiment/spawner all
starter:
$(MAKE) -C cmd/experiment/starter all
bg:
$(MAKE) -C cmd/experiment/bg all
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
$(MAKE) -C cmd/experiment/bg clean
install: ${installs}
$(MAKE) -C cmd/wingmate DESTDIR=${DESTDIR} install
$(MAKE) -C cmd/pidproxy DESTDIR=${DESTDIR} install
$(MAKE) -C cmd/exec DESTDIR=${DESTDIR} install
install-test:
$(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
$(MAKE) -C cmd/experiment/bg DESTDIR=${DESTDIR} install
install-dir:
install -d ${DESTDIR}

155
README.md
View File

@@ -1,157 +1,2 @@
# wingmate # wingmate
Wingmate is a process manager for services. It works like init. It starts and restarts services.
It also has cron feature. It is designed to run in a container/docker.
The Wingmate binary do not need any external dependency.
Just copy the binary, and exec from the entry point script.
# Getting Started
## Binaries
There are three binaries in this project: `wingmate`, `wmpidproxy`, and `wmexec`.
`wingmate` is the core binary. It reads config, starts, restarts services. It also
runs cron. Read the [configuration](#configuration) section for files needed to run
`wingmate`.
`wmpidproxy` is a helper binary for monitoring _legacy style_ service (fork, exit
initial proces, and continue in background). Read [here](#wingmate-pid-proxy-binary)
for further details about `wmpidproxy`.
`wmexec` is a helper binary for running process in different `user` or `group`.
It also useful for setting the process as process group leader.
Read [here](#wingmate-exec-binary) for further details about `wmexec`.
## Building a container image based on wingmate image in Docker Hub
Wingmate has no dependency other than `alpine` base image, so you just need to copy
the binaries directly. If you have built your application into an `alpine` based image,
all you need to do is copy whichever binary you need, crontab file (if you use cron)
and add some shell script to glue them together. Here is a Dockerfile example.
```Dockerfile
# Dockerfile
FROM suyono/wingmate:alpine as source
FROM alpine:latest
ADD --chmod=755 wingmate/ /etc/wingmate/
ADD --chmod=755 entry.sh /usr/local/bin/entry.sh
COPY --from=source /usr/local/bin/wingmate /usr/local/bin/wingmate
COPY --from=source /usr/local/bin/wmpidproxy /usr/local/bin/wmpidproxy
COPY --from=source /usr/local/bin/wmexec /usr/local/bin/wmexec
ENTRYPOINT [ "/usr/local/bin/entry.sh" ]
CMD [ "/usr/local/bin/wingmate" ]
```
You can find some examples for shell script in [alpine docker](docker/alpine) and
[bookworm docker](docker/bookworm).
## Configuration
When `wingmate` binary starts, it will look for some files. By default, it will
try to read the content of `/etc/wingmate` directory. You can change the directory
where it reads by setting `WINGMATE_CONFIG_PATH` environment variable. Wingmate supports
two format of configurations: yaml and shell script.
### YAML configuration
File structure:
```shell
/etc
└── wingmate
└── wingmate.yaml
```
Wingmate will parse the `wingmate.yaml` file and start services and crones based on the content
of the yaml file. Please read [wingmate.yaml.md](wingmate.yaml.md) for details on
the structure of yaml configuration file and some examples.
### Shell script configuration
Files and directories structure:
```shell
/etc
└── wingmate
├── crontab
├── crontab.d
│ ├── cron1.sh
│ ├── cron2.sh
│ └── cron3.sh
└── service
├── one.sh
└── spawner.sh
```
First, `wingmate` will try to read the content of `service` directory. The content of
this directory should be executables (either shell scripts or binaries). The wingmate
will run every executable in `service` directory without going into any subdirectory.
Next, `wingmate` will read the `crontab` file. `wingmate` expects the `crontab` file using
common UNIX crontab file format. Something like this
```shell
┌───────────── minute (059)
│ ┌───────────── hour (023)
│ │ ┌───────────── day of the month (131)
│ │ │ ┌───────────── month (112)
│ │ │ │ ┌───────────── day of the week (06) (Sunday to Saturday)
│ │ │ │ │
│ │ │ │ │
│ │ │ │ │
* * * * * <commad or shell script or binary>
```
The command part only support simple command and arguments. Shell expression is not supported.
It is recommended to write a shell script and put the path to shell script in
the command part.
**Note: It is recommended to use the yaml format instead of shell script. In order to avoid less
obvious mistake when writing shell script.**
# Appendix
## Wingmate PID Proxy binary
`wingmate` works by monitoring its direct children process. When it sees one of its children
exited, it will start the child process again.
Sometimes you find some services work by running in the background. It means it forks a new
process, disconnect the new child from terminal, exit the parent process, and continue
running in the child process. This kind of service usually write its background process
PID in a pid file.
To monitor the background services, `wingmate` utilizes `wmpidproxy`. `wmpidproxy` runs
in foreground in-place of the background service. It also periodically check whether the
background service is still running, in current implementation it checks every second.
```shell
wmpidproxy --pid-file <path to pid file> -- <background service binary/start script>
```
#### Example
Running sshd background with `wingmate` and `wmpidproxy`: [here](example/ssh-docker)
#### Note
It is recommended to configure services to run in foreground if they support it. When services
running in foreground, they are running as direct children process of `wingmate`.
`wingmate` monitors children process effectively. Whenever a child process exited/terminated,
`wingmate` will start it again quickly. Running in foreground also removes the overhead of running
`wmpidproxy` together with the service.
## Wingmate Exec binary
`wingmate` runs all the services as its children using the same `uid`, `gid`, and in the
same process group. You can use `wmexec` to run service in different `uid`, `gid`, or make
the service process as its own process group leader.
#### Syntax
```shell
wmexec [--user <uid>[:<gid>]] [--setsid] -- <target executable>
```
| Option | Parameter | Description |
|----------|-----------|----------------------------------------------------------------------------------------------------------|
| --user | uid[:gid] | Set the real user ID and the real group id. Uid and Gid can be either in numeric form or in name form |
| --setsid | | Set the process become the leader of its own process group, effectively detaching from parent's terminal |
#### Example
You can find example for `wmexec` in [here](docker/alpine/etc/wingmate) and [here](docker/bookworm/etc/wingmate)

View File

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

View File

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

View File

@@ -1,45 +0,0 @@
package cli
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
type Version string
const versionFlag = "version"
func (v Version) Print() {
fmt.Print(v)
os.Exit(0)
}
func (v Version) Cmd(cmd *cobra.Command) {
cmd.AddCommand(&cobra.Command{
Use: "version",
RunE: func(cmd *cobra.Command, args []string) error {
v.Print()
return nil
},
})
}
func (v Version) Flag(cmd *cobra.Command) {
v.FlagSet(cmd.PersistentFlags())
}
func (v Version) FlagSet(fs *pflag.FlagSet) {
viper.SetDefault(versionFlag, false)
fs.Bool(versionFlag, false, "print version")
_ = viper.BindPFlag(versionFlag, fs.Lookup(versionFlag))
}
func (v Version) FlagHook() {
if viper.GetBool(versionFlag) {
v.Print()
}
}

View File

@@ -1,10 +0,0 @@
all:
git describe > version.txt
go build -v
clean:
echo "dev" > version.txt
go clean -i -cache -testcache
install:
install exec ${DESTDIR}/wmexec

View File

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

View File

@@ -1,51 +0,0 @@
//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
}

View File

@@ -1,143 +0,0 @@
package main
import (
_ "embed"
"errors"
"fmt"
"log"
"os"
"os/exec"
"strconv"
"strings"
"gitea.suyono.dev/suyono/wingmate"
"gitea.suyono.dev/suyono/wingmate/cmd/cli"
wmenv "gitea.suyono.dev/suyono/wingmate/task/env"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"golang.org/x/sys/unix"
)
type execApp struct {
childArgs []string
err error
version cli.Version
}
const (
setsidFlag = "setsid"
EnvSetsid = "SETSID"
userFlag = "user"
EnvUser = "USER"
)
var (
//go:embed version.txt
version string
)
func main() {
var (
selfArgs []string
childArgs []string
app *execApp
rootCmd *cobra.Command
err error
)
app = &execApp{
version: cli.Version(version),
}
rootCmd = &cobra.Command{
Use: "wmexec",
SilenceUsage: true,
RunE: app.execCmd,
}
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))
app.version.Flag(rootCmd)
viper.SetEnvPrefix(wingmate.EnvPrefix)
_ = viper.BindEnv(EnvUser)
_ = viper.BindEnv(EnvSetsid)
viper.SetDefault(EnvSetsid, false)
viper.SetDefault(EnvUser, "")
app.version.Cmd(rootCmd)
if selfArgs, childArgs, err = cli.SplitArgs(os.Args); err != nil {
selfArgs = os.Args
}
app.childArgs = wmenv.ExpandEnv(os.Environ(), childArgs)
app.err = err
rootCmd.SetArgs(selfArgs[1:])
if err := rootCmd.Execute(); err != nil {
log.Println(err)
os.Exit(1)
}
}
func (e *execApp) execCmd(_ *cobra.Command, _ []string) error {
e.version.FlagHook()
if e.err != nil {
return e.err
}
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(e.childArgs[0]); err != nil {
if !errors.Is(err, exec.ErrDot) {
return fmt.Errorf("lookpath: %w", err)
}
}
if err = unix.Exec(path, e.childArgs, os.Environ()); err != nil {
return fmt.Errorf("exec: %w", err)
}
return nil
}

View File

@@ -1,10 +0,0 @@
/dummy/dummy
/dummy/version.txt
/starter/starter
/starter/version.txt
/oneshot/oneshot
/oneshot/version.txt
/spawner/spawner
/spawner/version.txt
/bg/bg
/bg/version.txt

View File

@@ -1,10 +0,0 @@
all:
git describe > version.txt
go build -v
clean:
echo "dev" > version.txt
go clean -i -cache -testcache
install:
install bg ${DESTDIR}/wmbg

View File

@@ -1,67 +0,0 @@
package main
import (
"fmt"
"log"
"os"
"time"
"github.com/spf13/pflag"
"golang.org/x/sys/unix"
)
const (
logPathFlag = "log-path"
pidFileFlag = "pid-file"
)
func main() {
var (
logPath string
pidFilePath string
name string
pause uint
lf *os.File
err error
)
pflag.StringVarP(&logPath, logPathFlag, "l", "/var/log/wmbg.log", "log file path")
pflag.StringVarP(&pidFilePath, pidFileFlag, "p", "/var/run/wmbg.pid", "pid file path")
pflag.StringVar(&name, "name", "no-name", "process name")
pflag.UintVar(&pause, "pause", 5, "pause interval")
pflag.Parse()
if lf, err = os.OpenFile(logPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644); err != nil {
os.Exit(2)
}
defer func() {
_ = lf.Close()
}()
log.SetOutput(lf)
log.Printf("starting process %s with pause interval %d", name, pause)
if err = writePid(pidFilePath); err != nil {
log.Printf("failed to write pid file: %+v", err)
}
time.Sleep(time.Duration(pause) * time.Second)
log.Printf("process %s finished", name)
}
func writePid(path string) error {
var (
err error
pf *os.File
)
if pf, err = os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644); err != nil {
return fmt.Errorf("opening pid file for write: %w", err)
}
defer func() {
_ = pf.Close()
}()
if _, err = fmt.Fprintf(pf, "%d", unix.Getpid()); err != nil {
return fmt.Errorf("writing pid to the pid file: %w", err)
}
return nil
}

View File

@@ -1,10 +0,0 @@
all:
git describe > version.txt
go build -v
clean:
echo "dev" > version.txt
go clean -i -cache -testcache
install:
install dummy ${DESTDIR}/wmdummy

View File

@@ -1,17 +0,0 @@
package main
import (
"fmt"
"log"
"time"
)
func main() {
log.Println("using log.Println")
fmt.Println("using fmt.Println")
time.Sleep(time.Second * 5)
log.Println("log: finishing up")
fmt.Println("fmt: finishing up")
}

View File

@@ -1,10 +0,0 @@
all:
git describe > version.txt
go build -v
clean:
echo "dev" > version.txt
go clean -i -cache -testcache
install:
install oneshot ${DESTDIR}/wmoneshot

View File

@@ -1,79 +0,0 @@
package main
import (
"log"
"math/rand"
"os"
"os/exec"
"gitea.suyono.dev/suyono/wingmate"
"gitea.suyono.dev/suyono/wingmate/cmd/cli"
"github.com/spf13/viper"
)
const (
EnvLog = "LOG"
EnvLogMessage = "LOG_MESSAGE"
EnvDefaultLogMessage = "oneshot executed"
EnvInstanceNum = "INSTANCE_NUM"
EnvDefaultInstances = 0
)
func main() {
viper.SetEnvPrefix(wingmate.EnvPrefix)
viper.BindEnv(EnvLog)
viper.BindEnv(EnvLogMessage)
viper.BindEnv(EnvInstanceNum)
viper.SetDefault(EnvLogMessage, EnvDefaultLogMessage)
viper.SetDefault(EnvInstanceNum, EnvDefaultInstances)
_, childArgs, err := cli.SplitArgs(os.Args)
if err != nil {
log.Printf("splitargs: %+v", err)
os.Exit(2)
}
logPath := viper.GetString(EnvLog)
logMessage := viper.GetString(EnvLogMessage)
log.Println("log path:", logPath)
if logPath != "" {
var (
file *os.File
)
if file, err = os.OpenFile(logPath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o666); err == nil {
defer func() {
_ = file.Close()
}()
if err = wingmate.NewLog(file, wingmate.Time|wingmate.Caller); err == nil {
wingmate.Log().Info().Msg(logMessage)
}
}
}
if len(childArgs) > 0 {
StartInstances(childArgs[0], childArgs[1:]...)
}
}
func StartInstances(exePath string, args ...string) {
num := (rand.Uint32() % 16) + 16
iNum := viper.GetInt(EnvInstanceNum)
if iNum > 0 {
num = uint32(iNum)
}
var (
ctr uint32
cmd *exec.Cmd
err error
)
for ctr = 0; ctr < num; ctr++ {
cmd = exec.Command(exePath, args...)
if err = cmd.Start(); err != nil {
log.Printf("failed to run %s: %+v\n", exePath, err)
}
}
}

View File

@@ -1,10 +0,0 @@
all:
git describe > version.txt
go build -v
clean:
echo "dev" > version.txt
go clean -i -cache -testcache
install:
install spawner ${DESTDIR}/wmspawner

View File

@@ -1,40 +0,0 @@
package main
import (
"log"
"os/exec"
"time"
"gitea.suyono.dev/suyono/wingmate"
"github.com/spf13/viper"
)
const (
EnvOneShotPath = "ONESHOT_PATH"
OneShotPath = "/usr/local/bin/wmoneshot"
)
func main() {
var (
cmd *exec.Cmd
err error
t *time.Ticker
)
viper.SetEnvPrefix(wingmate.EnvPrefix)
viper.BindEnv(EnvOneShotPath)
viper.SetDefault(EnvOneShotPath, OneShotPath)
exePath := viper.GetString(EnvOneShotPath)
t = time.NewTicker(time.Second * 5)
for {
cmd = exec.Command(exePath, "--", "wmdummy")
if err = cmd.Run(); err != nil {
log.Printf("failed to run %s: %+v\n", exePath, err)
} else {
log.Printf("%s executed\n", exePath)
}
<-t.C
}
}

View File

@@ -1,10 +0,0 @@
all:
git describe > version.txt
go build -v
clean:
echo "dev" > version.txt
go clean -i -cache -testcache
install:
install starter ${DESTDIR}/wmstarter

View File

@@ -1,110 +0,0 @@
package main
import (
"bufio"
"io"
"log"
"os"
"os/exec"
"sync"
"gitea.suyono.dev/suyono/wingmate"
"gitea.suyono.dev/suyono/wingmate/cmd/cli"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
const (
DummyPath = "/usr/local/bin/wmdummy"
EnvDummyPath = "DUMMY_PATH"
NoWaitFlag = "no-wait"
)
func main() {
var (
stdout io.ReadCloser
stderr io.ReadCloser
wg *sync.WaitGroup
err error
exePath string
selfArgs []string
childArgs []string
flagSet *pflag.FlagSet
noWait bool
cmd *exec.Cmd
)
if selfArgs, childArgs, err = cli.SplitArgs(os.Args); err == nil {
flagSet = pflag.NewFlagSet(selfArgs[0], pflag.ExitOnError)
flagSet.Count(NoWaitFlag, "do not wait for the child process")
if err = flagSet.Parse(selfArgs[1:]); err != nil {
log.Printf("invalid argument: %+v", err)
return
}
} else {
flagSet = pflag.NewFlagSet(os.Args[0], pflag.ExitOnError)
flagSet.Count(NoWaitFlag, "do not wait for the child process")
if err = flagSet.Parse(os.Args[1:]); err != nil {
log.Printf("invalid argument: %+v", err)
return
}
}
_ = viper.BindPFlag(NoWaitFlag, flagSet.Lookup(NoWaitFlag))
if viper.GetInt(NoWaitFlag) > 0 {
noWait = true
}
viper.SetEnvPrefix(wingmate.EnvPrefix)
_ = viper.BindEnv(EnvDummyPath)
viper.SetDefault(EnvDummyPath, DummyPath)
exePath = viper.GetString(EnvDummyPath)
if len(childArgs) > 0 {
cmd = exec.Command(childArgs[0], childArgs[1:]...)
} else {
cmd = exec.Command(exePath)
}
if !noWait {
if stdout, err = cmd.StdoutPipe(); err != nil {
log.Panic(err)
}
if stderr, err = cmd.StderrPipe(); err != nil {
log.Panic(err)
}
wg = &sync.WaitGroup{}
wg.Add(2)
go pulley(wg, stdout, "stdout")
go pulley(wg, stderr, "stderr")
}
if err = cmd.Start(); err != nil {
log.Panic(err)
}
if !noWait {
wg.Wait()
if err = cmd.Wait(); err != nil {
log.Printf("got error when Waiting for child process: %#v\n", err)
}
}
}
func pulley(wg *sync.WaitGroup, src io.ReadCloser, srcName string) {
defer wg.Done()
scanner := bufio.NewScanner(src)
for scanner.Scan() {
log.Printf("coming out from %s: %s\n", srcName, scanner.Text())
}
if err := scanner.Err(); err != nil {
log.Printf("got error whean reading from %s: %#v\n", srcName, err)
}
log.Printf("closing %s...\n", srcName)
_ = src.Close()
}

View File

@@ -1,10 +0,0 @@
all:
git describe > version.txt
go build -v
clean:
echo "dev" > version.txt
go clean -i -cache -testcache
install:
install pidproxy ${DESTDIR}/wmpidproxy

View File

@@ -1,168 +0,0 @@
package main
import (
"bufio"
"errors"
"gitea.suyono.dev/suyono/wingmate/cmd/cli"
"log"
"os"
"os/exec"
"os/signal"
"strconv"
"syscall"
"time"
"gitea.suyono.dev/suyono/wingmate"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"golang.org/x/sys/unix"
_ "embed"
)
type pidProxyApp struct {
childArgs []string
err error
version cli.Version
}
const (
pidFileFlag = "pid-file"
EnvStartSecs = "STARTSECS"
EnvDefaultStartSecs = 1
)
var (
//go:embed version.txt
version string
)
func main() {
var (
selfArgs []string
childArgs []string
err error
app *pidProxyApp
rootCmd *cobra.Command
)
app = &pidProxyApp{
version: cli.Version(version),
}
rootCmd = &cobra.Command{
Use: "wmpidproxy",
SilenceUsage: true,
RunE: app.pidProxy,
}
viper.SetEnvPrefix(wingmate.EnvPrefix)
_ = viper.BindEnv(EnvStartSecs)
viper.SetDefault(EnvStartSecs, EnvDefaultStartSecs)
rootCmd.PersistentFlags().StringP(pidFileFlag, "p", "", "location of pid file")
_ = rootCmd.MarkFlagRequired(pidFileFlag)
_ = viper.BindPFlag(pidFileFlag, rootCmd.PersistentFlags().Lookup(pidFileFlag))
app.version.Flag(rootCmd)
app.version.Cmd(rootCmd)
if selfArgs, childArgs, err = cli.SplitArgs(os.Args); err != nil {
selfArgs = os.Args
}
app.childArgs = childArgs
app.err = err
rootCmd.SetArgs(selfArgs[1:])
if err := rootCmd.Execute(); err != nil {
log.Println(err)
os.Exit(1)
}
}
func (p *pidProxyApp) pidProxy(cmd *cobra.Command, args []string) error {
p.version.FlagHook()
pidfile := viper.GetString(pidFileFlag)
log.Printf("%s %v", pidfile, p.childArgs)
if len(p.childArgs) > 1 {
go p.startProcess(p.childArgs[0], p.childArgs[1:]...)
} else {
go p.startProcess(p.childArgs[0])
}
initialWait := viper.GetInt(EnvStartSecs)
time.Sleep(time.Second * time.Duration(initialWait))
var (
err error
pid int
sc chan os.Signal
t *time.Ticker
)
sc = make(chan os.Signal, 1)
signal.Notify(sc, unix.SIGTERM)
t = time.NewTicker(time.Second)
check:
for {
if pid, err = p.readPid(pidfile); err != nil {
return err
}
if err = unix.Kill(pid, syscall.Signal(0)); err != nil {
if !errors.Is(err, unix.ESRCH) {
return err
}
break check
}
select {
case <-t.C:
case <-sc:
if pid, err = p.readPid(pidfile); err != nil {
return err
}
if err = unix.Kill(pid, unix.SIGTERM); err != nil {
return err
}
break check
}
}
return nil
}
func (p *pidProxyApp) readPid(pidFile string) (int, error) {
var (
file *os.File
err error
pid64 int64
)
if file, err = os.Open(pidFile); err != nil {
return 0, err
}
defer func() {
_ = file.Close()
}()
scanner := bufio.NewScanner(file)
if scanner.Scan() {
if pid64, err = strconv.ParseInt(scanner.Text(), 10, 64); err != nil {
return 0, err
}
return int(pid64), nil
} else {
return 0, errors.New("invalid scanner")
}
}
func (p *pidProxyApp) startProcess(arg0 string, args ...string) {
if err := exec.Command(arg0, args...).Run(); err != nil {
log.Println("exec:", err)
return
}
}

View File

@@ -1,11 +0,0 @@
all:
git describe > version.txt
go build -v
clean:
echo "dev" > version.txt
go clean -i -cache -testcache
install:
install wingmate ${DESTDIR}/wingmate

View File

@@ -1,87 +1,60 @@
package main package main
import ( import (
"sync" "time"
"gitea.suyono.dev/suyono/wingmate/config" "gitea.suyono.dev/suyono/wingmate/config"
wminit "gitea.suyono.dev/suyono/wingmate/init" wminit "gitea.suyono.dev/suyono/wingmate/init"
"gitea.suyono.dev/suyono/wingmate/task"
) )
type wPath struct {
path string
}
func (p wPath) Path() string {
return p.path
}
type wConfig struct { type wConfig struct {
tasks *task.Tasks services []wminit.Path
config *config.Config cron []wminit.Cron
viperMtx *sync.Mutex
} }
func (c *wConfig) Tasks() wminit.Tasks { func (c wConfig) Services() []wminit.Path {
return c.tasks return c.services
} }
func (c *wConfig) Reload() error { func (c wConfig) Cron() []wminit.Cron {
//NOTE: for future use when reloading is possible return c.cron
return nil
} }
func convert(cfg *config.Config) *wConfig { type wCron struct {
retval := &wConfig{ iCron *config.Cron
tasks: task.NewTasks(),
config: cfg,
viperMtx: &sync.Mutex{},
} }
for _, s := range cfg.Service { func (c wCron) TimeToRun(now time.Time) bool {
st := task.NewServiceTask(s.Name).SetCommand(s.Command...).SetEnv(s.Environ...) return c.iCron.TimeToRun(now)
st.SetFallbackEnv(s.FallbackEnv...).SetFlagSetsid(s.Setsid).SetWorkingDir(s.WorkingDir)
st.SetUser(s.User).SetGroup(s.Group).SetStartSecs(s.StartSecs).SetPidFile(s.PidFile)
st.SetConfig(cfg)
retval.tasks.AddService(st)
} }
for _, s := range cfg.ServiceV0 { func (c wCron) Command() wminit.Path {
retval.tasks.AddV0Service(s) return wPath{
path: c.iCron.Command(),
}
} }
var schedule task.CronSchedule func convert(cfg *config.Config) wConfig {
for _, c := range cfg.CronV0 { retval := wConfig{
schedule = configToTaskCronSchedule(c.CronSchedule) services: make([]wminit.Path, 0, len(cfg.ServicePaths)),
retval.tasks.AddV0Cron(schedule, c.Command) cron: make([]wminit.Cron, 0, len(cfg.Cron)),
}
for _, s := range cfg.ServicePaths {
retval.services = append(retval.services, wPath{path: s})
} }
for _, c := range cfg.Cron { for _, c := range cfg.Cron {
schedule = configToTaskCronSchedule(c.CronSchedule) retval.cron = append(retval.cron, wCron{iCron: c})
ct := task.NewCronTask(c.Name).SetCommand(c.Command...).SetEnv(c.Environ...)
ct.SetFallbackEnv(c.FallbackEnv...).SetFlagSetsid(c.Setsid).SetWorkingDir(c.WorkingDir)
ct.SetUser(c.User).SetGroup(c.Group)
ct.SetSchedule(c.Schedule, schedule)
ct.SetConfig(cfg)
retval.tasks.AddCron(ct)
} }
return retval return retval
} }
func configToTaskCronSchedule(cfgSchedule config.CronSchedule) (taskSchedule task.CronSchedule) {
taskSchedule.Minute = configToTaskCronTimeSpec(cfgSchedule.Minute)
taskSchedule.Hour = configToTaskCronTimeSpec(cfgSchedule.Hour)
taskSchedule.DoM = configToTaskCronTimeSpec(cfgSchedule.DoM)
taskSchedule.Month = configToTaskCronTimeSpec(cfgSchedule.Month)
taskSchedule.DoW = configToTaskCronTimeSpec(cfgSchedule.DoW)
return
}
func configToTaskCronTimeSpec(cfg config.CronTimeSpec) task.CronTimeSpec {
switch v := cfg.(type) {
case *config.SpecAny:
return task.NewCronAnySpec()
case *config.SpecExact:
return task.NewCronExactSpec(v.Value())
case *config.SpecMultiOccurrence:
return task.NewCronMultiOccurrenceSpec(v.Values()...)
}
panic("invalid conversion")
}

View File

@@ -1,18 +1,11 @@
package main package main
import ( import (
_ "embed"
"os" "os"
"gitea.suyono.dev/suyono/wingmate" "gitea.suyono.dev/suyono/wingmate"
"gitea.suyono.dev/suyono/wingmate/config" "gitea.suyono.dev/suyono/wingmate/config"
wminit "gitea.suyono.dev/suyono/wingmate/init" wminit "gitea.suyono.dev/suyono/wingmate/init"
"github.com/spf13/viper"
)
var (
//go:embed version.txt
version string
) )
func main() { func main() {
@@ -21,15 +14,9 @@ func main() {
cfg *config.Config cfg *config.Config
) )
_ = wingmate.NewLog(os.Stderr, wingmate.Time|wingmate.Caller) _ = wingmate.NewLog(os.Stderr)
wingmate.SetGlobalLevel(wingmate.InfoLevel)
config.SetVersion(version)
config.ParseFlags()
wingmate.Log().Info().Msgf("starting wingmate version %s", viper.GetString(config.WingmateVersion))
if cfg, err = config.Read(); err != nil { if cfg, err = config.Read(); err != nil {
wingmate.Log().Fatal().Err(err).Msg("failed to read config") wingmate.Log().Error().Msgf("failed to read config %#v", err)
} }
initCfg := convert(cfg) initCfg := convert(cfg)

View File

@@ -1,23 +0,0 @@
package main
import (
"os"
"testing"
)
func TestEntry_configPathEnv(t *testing.T) {
_ = os.Setenv("WINGMATE_CONFIG_PATH", "/Volumes/Source/go/src/gitea.suyono.dev/suyono/wingmate/docker/bookworm/etc/wingmate")
defer func() {
_ = os.Unsetenv("WINGMATE_CONFIG_PATH")
}()
main()
}
func TestEntry_configPathPFlag(t *testing.T) {
os.Args = []string{"wingmate", "--config", "/workspaces/wingmate/docker/bookworm-newconfig/etc/wingmate"}
main()
}
func TestEntry(t *testing.T) {
main()
}

View File

@@ -2,95 +2,30 @@ package config
import ( import (
"errors" "errors"
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"sync"
"gitea.suyono.dev/suyono/wingmate" "gitea.suyono.dev/suyono/wingmate"
"github.com/spf13/viper" "github.com/spf13/viper"
"golang.org/x/sys/unix"
) )
const ( const (
EnvPrefix = "WINGMATE" EnvPrefix = "WINGMATE"
PathConfig = "config_path" EnvConfigPath = "CONFIG_PATH"
DefaultConfigPath = "/etc/wingmate" DefaultConfigPath = "/etc/wingmate"
ServiceDirName = "service" ServiceDirName = "service"
CrontabFileName = "crontab" CrontabFileName = "crontab"
WingmateConfigFileName = "wingmate"
WingmateConfigFileFormat = "yaml"
WingmateVersion = "APP_VERSION"
PidProxyPathConfig = "pidproxy_path"
PidProxyPathDefault = "wmpidproxy"
ExecPathConfig = "exec_path"
ExecPathDefault = "wmexec"
versionTrimRightCutSet = "\r\n "
WMPidProxyPathFlag = "pid-proxy"
WMExecPathFlag = "exec"
PathConfigFlag = "config"
GlobalLogConfig = "log_level"
) )
type Config struct { type Config struct {
ServiceV0 []string ServicePaths []string
CronV0 []*Cron Cron []*Cron
Service []ServiceTask
Cron []CronTask
viperMtx *sync.Mutex
}
type Task struct {
Command []string `mapstructure:"command"`
Environ []string `mapstructure:"environ"`
FallbackEnv []string `mapstructure:"fallback_env"`
Setsid bool `mapstructure:"setsid"`
User string `mapstructure:"user"`
Group string `mapstructure:"group"`
WorkingDir string `mapstructure:"working_dir"`
}
type ServiceTask struct {
Task `mapstructure:",squash"`
Name string `mapstructure:"-"`
Background bool `mapstructure:"background"`
PidFile string `mapstructure:"pidfile"`
StartSecs uint `mapstructure:"startsecs"`
AutoStart bool `mapstructure:"autostart"`
AutoRestart bool `mapstructure:"autorestart"`
}
type CronTask struct {
CronSchedule `mapstructure:"-"`
Task `mapstructure:",squash"`
Name string `mapstructure:"-"`
Schedule string `mapstructure:"schedule"`
}
type CronSchedule struct {
Minute CronTimeSpec
Hour CronTimeSpec
DoM CronTimeSpec
Month CronTimeSpec
DoW CronTimeSpec
}
func SetVersion(version string) {
version = strings.TrimRight(version, versionTrimRightCutSet)
viper.Set(WingmateVersion, version)
} }
func Read() (*Config, error) { func Read() (*Config, error) {
viper.SetEnvPrefix(EnvPrefix) viper.SetEnvPrefix(EnvPrefix)
_ = viper.BindEnv(PathConfig) viper.BindEnv(EnvConfigPath)
_ = viper.BindEnv(PidProxyPathConfig) viper.SetDefault(EnvConfigPath, DefaultConfigPath)
_ = viper.BindEnv(ExecPathConfig)
_ = viper.BindEnv(GlobalLogConfig)
viper.SetDefault(PathConfig, DefaultConfigPath)
viper.SetDefault(PidProxyPathConfig, PidProxyPathDefault)
viper.SetDefault(ExecPathConfig, ExecPathDefault)
viper.SetDefault(GlobalLogConfig, wingmate.InfoLevelStr)
var ( var (
dirent []os.DirEntry dirent []os.DirEntry
@@ -98,124 +33,43 @@ func Read() (*Config, error) {
svcdir string svcdir string
serviceAvailable bool serviceAvailable bool
cronAvailable bool cronAvailable bool
wingmateConfigAvailable bool
cron []*Cron cron []*Cron
crontabfile string crontabfile string
services []ServiceTask
crones []CronTask
) )
wingmate.SetGlobalLevelStr(viper.GetString(GlobalLogConfig))
serviceAvailable = false serviceAvailable = false
cronAvailable = false cronAvailable = false
outConfig := &Config{ outConfig := &Config{
viperMtx: &sync.Mutex{}, ServicePaths: make([]string, 0),
ServiceV0: make([]string, 0),
} }
configPath := viper.GetString(PathConfig) configPath := viper.GetString(EnvConfigPath)
svcdir = filepath.Join(configPath, ServiceDirName) svcdir = filepath.Join(configPath, ServiceDirName)
dirent, err = os.ReadDir(svcdir) dirent, err = os.ReadDir(svcdir)
if err != nil {
wingmate.Log().Warn().Msgf("encounter error when reading service directory %s: %+v", svcdir, err)
}
if len(dirent) > 0 { if len(dirent) > 0 {
for _, d := range dirent { for _, d := range dirent {
if d.Type().IsRegular() { if d.Type().IsRegular() {
svcPath := filepath.Join(svcdir, d.Name())
if err = unix.Access(svcPath, unix.X_OK); err == nil {
serviceAvailable = true serviceAvailable = true
outConfig.ServiceV0 = append(outConfig.ServiceV0, svcPath) outConfig.ServicePaths = append(outConfig.ServicePaths, filepath.Join(svcdir, d.Name()))
} else {
wingmate.Log().Error().Msgf("checking executable access for %s: %+v", svcPath, err)
} }
} }
} }
if err != nil {
wingmate.Log().Error().Msgf("encounter error when reading service directory %s: %#v", svcdir, err)
} }
crontabfile = filepath.Join(configPath, CrontabFileName) crontabfile = filepath.Join(configPath, CrontabFileName)
cron, err = readCrontab(crontabfile) cron, err = readCrontab(crontabfile)
if len(cron) > 0 { if len(cron) > 0 {
outConfig.CronV0 = cron outConfig.Cron = cron
cronAvailable = true cronAvailable = true
} }
if err != nil { if err != nil {
wingmate.Log().Warn().Msgf("encounter error when reading crontab %s: %+v", crontabfile, err) wingmate.Log().Error().Msgf("encounter error when reading crontab %s: %#v", crontabfile, err)
} }
wingmateConfigAvailable = false if !serviceAvailable && !cronAvailable {
if services, crones, err = readConfigYaml(configPath, WingmateConfigFileName, WingmateConfigFileFormat); err != nil {
wingmate.Log().Error().Msgf("encounter error when reading wingmate config file in %s/%s: %+v", configPath, WingmateConfigFileName, err)
}
if len(services) > 0 {
outConfig.Service = services
wingmateConfigAvailable = true
}
if len(crones) > 0 {
outConfig.Cron = crones
wingmateConfigAvailable = true
}
if !serviceAvailable && !cronAvailable && !wingmateConfigAvailable {
return nil, errors.New("no config found") return nil, errors.New("no config found")
} }
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 {
c.viperMtx.Lock()
defer c.viperMtx.Unlock()
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 {
c.viperMtx.Lock()
defer c.viperMtx.Unlock()
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,132 +0,0 @@
package config
import (
"os"
"path"
"testing"
"gitea.suyono.dev/suyono/wingmate"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
)
const (
serviceDir = "service"
)
var (
configDir string
)
func setup(t *testing.T) {
var err error
if configDir, err = os.MkdirTemp("", "wingmate-*-test"); err != nil {
t.Fatal("setup", err)
}
viper.Set(PathConfig, configDir)
}
func tear(t *testing.T) {
if err := os.RemoveAll(configDir); err != nil {
t.Fatal("tear", err)
}
}
func TestRead(t *testing.T) {
type testEntry struct {
name string
testFunc func(t *testing.T)
}
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, wingmate.Caller|wingmate.Time)
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.ServiceV0,
[]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.ServiceV0,
[]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.ServiceV0,
[]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

@@ -4,30 +4,54 @@ import (
"bufio" "bufio"
"errors" "errors"
"fmt" "fmt"
"gitea.suyono.dev/suyono/wingmate"
"os" "os"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"time"
"gitea.suyono.dev/suyono/wingmate"
) )
type CronTimeSpec interface { type CronExactSpec interface {
//Type() wingmate.CronTimeType CronTimeSpec
//Match(uint8) bool Value() uint8
} }
type CronMultipleOccurrenceSpec interface {
CronTimeSpec
Values() []uint8
}
type CronTimeSpec interface {
Type() wingmate.CronTimeType
Match(uint8) bool
}
// type Cron interface {
// Minute() CronTimeSpec
// Hour() CronTimeSpec
// DayOfMonth() CronTimeSpec
// Month() CronTimeSpec
// DayOfWeek() CronTimeSpec
// }
type Cron struct { type Cron struct {
CronSchedule minute CronTimeSpec
Command string hour CronTimeSpec
dom CronTimeSpec
month CronTimeSpec
dow CronTimeSpec
command string
lastRun time.Time
hasRun bool
} }
type cronField int type cronField int
const ( const (
CrontabEntryRegexPattern = `^\s*(?P<minute>\S+)\s+(?P<hour>\S+)\s+(?P<dom>\S+)\s+(?P<month>\S+)\s+(?P<dow>\S+)\s+(?P<command>\S.*\S)\s*$` CrontabEntryRegex = `^\s*(?P<minute>\S+)\s+(?P<hour>\S+)\s+(?P<dom>\S+)\s+(?P<month>\S+)\s+(?P<dow>\S+)\s+(?P<command>\S.*\S)\s*$`
CrontabCommentLineRegexPattern = `^\s*#.*$` CrontabSubmatchLen = 7
CrontabCommentSuffixRegexPattern = `^\s*([^#]+)#.*$`
CrontabSubMatchLen = 7
minute cronField = iota minute cronField = iota
hour hour
@@ -36,22 +60,21 @@ const (
dow dow
) )
var (
crontabEntryRegex = regexp.MustCompile(CrontabEntryRegexPattern)
crontabCommentLineRegex = regexp.MustCompile(CrontabCommentLineRegexPattern)
crontabCommentSuffixRegex = regexp.MustCompile(CrontabCommentSuffixRegexPattern)
)
func readCrontab(path string) ([]*Cron, error) { func readCrontab(path string) ([]*Cron, error) {
var ( var (
file *os.File file *os.File
err error err error
scanner *bufio.Scanner scanner *bufio.Scanner
line string line string
re *regexp.Regexp
parts []string parts []string
retval []*Cron retval []*Cron
) )
if re, err = regexp.Compile(CrontabEntryRegex); err != nil {
return nil, err
}
if file, err = os.Open(path); err != nil { if file, err = os.Open(path); err != nil {
return nil, err return nil, err
} }
@@ -64,48 +87,41 @@ func readCrontab(path string) ([]*Cron, error) {
for scanner.Scan() { for scanner.Scan() {
line = scanner.Text() line = scanner.Text()
if crontabCommentLineRegex.MatchString(line) { parts = re.FindStringSubmatch(line)
continue if len(parts) != CrontabSubmatchLen {
}
parts = crontabCommentSuffixRegex.FindStringSubmatch(line)
if len(parts) == 2 {
line = parts[1]
}
parts = crontabEntryRegex.FindStringSubmatch(line)
if len(parts) != CrontabSubMatchLen {
wingmate.Log().Error().Msgf("invalid entry %s", line) wingmate.Log().Error().Msgf("invalid entry %s", line)
continue continue
} }
c := &Cron{} c := &Cron{
hasRun: false,
}
if err = c.setField(minute, parts[1]); err != nil { if err = c.setField(minute, parts[1]); err != nil {
wingmate.Log().Error().Msgf("error parsing Minute field %+v", err) wingmate.Log().Error().Msgf("error parsing minute field %#v", err)
continue continue
} }
if err = c.setField(hour, parts[2]); err != nil { if err = c.setField(hour, parts[2]); err != nil {
wingmate.Log().Error().Msgf("error parsing Hour field %+v", err) wingmate.Log().Error().Msgf("error parsing hour field %#v", err)
continue continue
} }
if err = c.setField(dom, parts[3]); err != nil { if err = c.setField(dom, parts[3]); err != nil {
wingmate.Log().Error().Msgf("error parsing Day of Month field %+v", err) wingmate.Log().Error().Msgf("error parsing day of month field %#v", err)
continue continue
} }
if err = c.setField(month, parts[4]); err != nil { if err = c.setField(month, parts[4]); err != nil {
wingmate.Log().Error().Msgf("error parsing Month field %+v", err) wingmate.Log().Error().Msgf("error parsing month field %#v", err)
continue continue
} }
if err = c.setField(dow, parts[5]); err != nil { if err = c.setField(dow, parts[5]); err != nil {
wingmate.Log().Error().Msgf("error parsing Day of Week field %+v", err) wingmate.Log().Error().Msgf("error parsing day of week field %#v", err)
continue continue
} }
c.Command = parts[6] c.command = parts[6]
retval = append(retval, c) retval = append(retval, c)
} }
@@ -113,6 +129,32 @@ func readCrontab(path string) ([]*Cron, error) {
return retval, nil return retval, nil
} }
func (c *Cron) Command() string {
return c.command
}
func (c *Cron) TimeToRun(now time.Time) bool {
if !c.hasRun {
c.lastRun = now
c.hasRun = true
return true
}
if now.Sub(c.lastRun) <= time.Minute && now.Minute() == c.lastRun.Minute() {
return false
}
if c.minute.Match(uint8(now.Minute())) &&
c.hour.Match(uint8(now.Hour())) &&
c.dom.Match(uint8(now.Day())) &&
c.month.Match(uint8(now.Month())) &&
c.dow.Match(uint8(now.Weekday())) {
return true
}
return false
}
type fieldRange struct { type fieldRange struct {
min int min int
max int max int
@@ -145,33 +187,33 @@ func (c *Cron) setField(field cronField, input string) error {
switch field { switch field {
case minute: case minute:
fr = newRange(0, 59) fr = newRange(0, 59)
cField = &c.Minute cField = &c.minute
case hour: case hour:
fr = newRange(0, 23) fr = newRange(0, 23)
cField = &c.Hour cField = &c.hour
case dom: case dom:
fr = newRange(1, 31) fr = newRange(1, 31)
cField = &c.DoM cField = &c.dom
case month: case month:
fr = newRange(1, 12) fr = newRange(1, 12)
cField = &c.Month cField = &c.month
case dow: case dow:
fr = newRange(0, 6) fr = newRange(0, 6)
cField = &c.DoW cField = &c.dow
default: default:
return errors.New("invalid cron field descriptor") return errors.New("invalid cron field descriptor")
} }
if input == "*" { if input == "*" {
*cField = &SpecAny{} *cField = &specAny{}
} else if strings.HasPrefix(input, "*/") { } else if strings.HasPrefix(input, "*/") {
if parsed64, err = strconv.ParseUint(input[2:], 10, 8); err != nil { if parsed64, err = strconv.ParseUint(input[2:], 10, 8); err != nil {
return fmt.Errorf("error parse field %+v with input %s: %w", field, input, err) return fmt.Errorf("error parse field %#v with input %s: %w", field, input, err)
} }
parsed = uint8(parsed64) parsed = uint8(parsed64)
if !fr.valid(parsed) { if fr.valid(parsed) {
return fmt.Errorf("error parse field %+v with input %s parsed to %d: invalid value", field, input, parsed) return fmt.Errorf("error parse field %#v with input %s: invalid value", field, input)
} }
multi = make([]uint8, 0) multi = make([]uint8, 0)
current = parsed current = parsed
@@ -180,7 +222,7 @@ func (c *Cron) setField(field cronField, input string) error {
current += parsed current += parsed
} }
*cField = &SpecMultiOccurrence{ *cField = &specMultiOccurrence{
values: multi, values: multi,
} }
} else { } else {
@@ -189,31 +231,31 @@ func (c *Cron) setField(field cronField, input string) error {
multi = make([]uint8, 0) multi = make([]uint8, 0)
for _, s := range multiStr { for _, s := range multiStr {
if parsed64, err = strconv.ParseUint(s, 10, 8); err != nil { if parsed64, err = strconv.ParseUint(s, 10, 8); err != nil {
return fmt.Errorf("error parse field %+v with input %s: %w", field, input, err) return fmt.Errorf("error parse field %#v with input %s: %w", field, input, err)
} }
parsed = uint8(parsed64) parsed = uint8(parsed64)
if !fr.valid(parsed) { if fr.valid(parsed) {
return fmt.Errorf("error parse field %+v with input %s: invalid value", field, input) return fmt.Errorf("error parse field %#v with input %s: invalid value", field, input)
} }
multi = append(multi, parsed) multi = append(multi, parsed)
} }
*cField = &SpecMultiOccurrence{ *cField = &specMultiOccurrence{
values: multi, values: multi,
} }
} else { } else {
if parsed64, err = strconv.ParseUint(input, 10, 8); err != nil { if parsed64, err = strconv.ParseUint(input, 10, 8); err != nil {
return fmt.Errorf("error parse field %+v with input %s: %w", field, input, err) return fmt.Errorf("error parse field %#v with input %s: %w", field, input, err)
} }
parsed = uint8(parsed64) parsed = uint8(parsed64)
if !fr.valid(parsed) { if fr.valid(parsed) {
return fmt.Errorf("error parse field %+v with input %s: invalid value", field, input) return fmt.Errorf("error parse field %#v with input %s: invalid value", field, input)
} }
*cField = &SpecExact{ *cField = &specExact{
value: parsed, value: parsed,
} }
} }
@@ -222,21 +264,51 @@ func (c *Cron) setField(field cronField, input string) error {
return nil return nil
} }
type SpecAny struct{} type specAny struct{}
type SpecExact struct { func (a *specAny) Type() wingmate.CronTimeType {
return wingmate.Any
}
func (a *specAny) Match(u uint8) bool {
return true
}
type specExact struct {
value uint8 value uint8
} }
func (e *SpecExact) Value() uint8 { func (e *specExact) Type() wingmate.CronTimeType {
return wingmate.Exact
}
func (e *specExact) Match(u uint8) bool {
return u == e.value
}
func (e *specExact) Value() uint8 {
return e.value return e.value
} }
type SpecMultiOccurrence struct { type specMultiOccurrence struct {
values []uint8 values []uint8
} }
func (m *SpecMultiOccurrence) Values() []uint8 { func (m *specMultiOccurrence) Type() wingmate.CronTimeType {
return wingmate.MultipleOccurrence
}
func (m *specMultiOccurrence) Match(u uint8) bool {
for _, v := range m.values {
if v == u {
return true
}
}
return false
}
func (m *specMultiOccurrence) Values() []uint8 {
out := make([]uint8, len(m.values)) out := make([]uint8, len(m.values))
copy(out, m.values) copy(out, m.values)
return out return out

View File

@@ -1,129 +0,0 @@
package config
import (
"os"
"path/filepath"
"testing"
"gitea.suyono.dev/suyono/wingmate"
"github.com/stretchr/testify/assert"
)
const (
crontabFileName = "crontab"
)
func TestCrontab(t *testing.T) {
type testEntry struct {
name string
crontab string
wantErr bool
}
_ = wingmate.NewLog(os.Stderr, wingmate.Caller|wingmate.Time)
tests := []testEntry{
{
name: "positive",
crontab: crontabTestCase0,
wantErr: false,
},
{
name: "with comment",
crontab: crontabTestCase1,
wantErr: false,
},
{
name: "various values",
crontab: crontabTestCase2,
wantErr: false,
},
{
name: "failed to parse",
crontab: crontabTestCase3,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
setup(t)
defer tear(t)
writeCrontab(t, tt.crontab)
cfg, err := Read()
if tt.wantErr != (err != nil) {
t.Fatalf("wantErr is %v but err is %+v", tt.wantErr, err)
}
t.Logf("cfg: %+v", cfg)
for _, c := range cfg.CronV0 {
t.Logf("%+v", c)
}
})
}
}
func writeCrontab(t *testing.T, content string) {
var (
f *os.File
err error
)
if f, err = os.Create(filepath.Join(configDir, crontabFileName)); err != nil {
t.Fatal("create crontab file", err)
}
defer func() {
_ = f.Close()
}()
if _, err = f.Write([]byte(content)); err != nil {
t.Fatal("writing crontab file", err)
}
}
const crontabTestCase0 = `* * * * * /path/to/executable`
const crontabTestCase1 = `# this is a comment
## comment with space
* * * * * /path/to/executable
* * * * * /path/to/executable # comment as a suffix
`
const crontabTestCase2 = `# first comment
*/5 13 3,5,7 * * /path/to/executable`
const crontabTestCase3 = `a 13 3,5,7 * * /path/to/executable
*/5 a 3,5,7 * * /path/to/executable
*/5 13 a * * /path/to/executable
*/5 13 3,5,7 a * /path/to/executable
*/5 13 3,5,7 * a /path/to/executable
*/x 13 3,5,7 * * /path/to/executable
76 13 3,5,7 * * /path/to/executable
*/75 13 3,5,7 * * /path/to/executable
*/5 13 3,x,7 * * /path/to/executable
*/5 13 3,5,67 * * /path/to/executable
*/5 13 * * /path/to/executable
*/5 13 3,5,7 * * /path/to/executable`
func TestSpecExact(t *testing.T) {
var val uint8 = 45
s := SpecExact{
value: val,
}
assert.Equal(t, val, s.Value())
}
func TestSpecMulti(t *testing.T) {
val := []uint8{3, 5, 7, 15}
s := SpecMultiOccurrence{
values: val,
}
assert.ElementsMatch(t, val, s.Values())
}
func TestInvalidField(t *testing.T) {
c := &Cron{}
assert.NotNil(t, c.setField(cronField(99), "x"))
}

View File

@@ -1,26 +0,0 @@
package config
import (
"fmt"
"gitea.suyono.dev/suyono/wingmate/cmd/cli"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
func ParseFlags() {
version := cli.Version(fmt.Sprintln(viper.GetString(WingmateVersion)))
version.FlagSet(pflag.CommandLine)
pflag.String(WMPidProxyPathFlag, "", "wmpidproxy path")
pflag.String(WMExecPathFlag, "", "wmexec path")
pflag.StringP(PathConfigFlag, "c", "", "config path")
pflag.Parse()
_ = viper.BindPFlag(PathConfig, pflag.CommandLine.Lookup(PathConfigFlag))
_ = viper.BindPFlag(PidProxyPathConfig, pflag.CommandLine.Lookup(WMPidProxyPathFlag))
_ = viper.BindPFlag(ExecPathConfig, pflag.CommandLine.Lookup(WMExecPathFlag))
version.FlagHook()
}

View File

@@ -1,37 +0,0 @@
package config
import (
"fmt"
"io"
"os/exec"
"strings"
)
func getVersion(binPath string) (string, error) {
var (
outBytes []byte
err error
output string
stdout io.ReadCloser
n int
)
cmd := exec.Command(binPath, "version")
if stdout, err = cmd.StdoutPipe(); err != nil {
return "", fmt.Errorf("setting up stdout reader: %w", err)
}
if err = cmd.Start(); err != nil {
return "", fmt.Errorf("starting process: %w", err)
}
outBytes = make([]byte, 1024)
if n, err = stdout.Read(outBytes); err != nil {
return "", fmt.Errorf("reading stdout: %w", err)
}
_ = cmd.Wait()
output = string(outBytes[:n])
output = strings.TrimRight(output, versionTrimRightCutSet)
return output, nil
}

View File

@@ -1,203 +0,0 @@
package config
import (
"errors"
"fmt"
"regexp"
"strconv"
"strings"
"gitea.suyono.dev/suyono/wingmate"
"github.com/spf13/viper"
)
const (
CrontabScheduleRegexPattern = `^\s*(?P<minute>\S+)\s+(?P<hour>\S+)\s+(?P<dom>\S+)\s+(?P<month>\S+)\s+(?P<dow>\S+)\s*$`
CrontabScheduleSubMatchLen = 6
ServiceConfigGroup = "service"
CronConfigGroup = "cron"
ServiceKeyFormat = "service.%s"
CronKeyFormat = "cron.%s"
)
var (
crontabScheduleRegex = regexp.MustCompile(CrontabScheduleRegexPattern)
)
func readConfigYaml(path, name, format string) ([]ServiceTask, []CronTask, error) {
var (
err error
nameMap map[string]any
itemName string
serviceTask ServiceTask
cronTask CronTask
item any
services []ServiceTask
crones []CronTask
//findUtils *FindUtils
)
viper.AddConfigPath(path)
viper.SetConfigType(format)
viper.SetConfigName(name)
if err = viper.ReadInConfig(); err != nil {
return nil, nil, fmt.Errorf("reading config in dir %s, file %s, format %s: %w", path, name, format, err)
}
services = make([]ServiceTask, 0)
nameMap = viper.GetStringMap(ServiceConfigGroup)
for itemName, item = range nameMap {
serviceTask = ServiceTask{}
if err = viper.UnmarshalKey(fmt.Sprintf(ServiceKeyFormat, itemName), &serviceTask); err != nil {
wingmate.Log().Error().Msgf("failed to parse service %s: %+v | %+v", itemName, err, item)
continue
}
serviceTask.Name = itemName
services = append(services, serviceTask)
}
crones = make([]CronTask, 0)
nameMap = viper.GetStringMap(CronConfigGroup)
for itemName, item = range nameMap {
cronTask = CronTask{}
if err = viper.UnmarshalKey(fmt.Sprintf(CronKeyFormat, itemName), &cronTask); err != nil {
wingmate.Log().Error().Msgf("failed to parse cron %s: %v | %v", itemName, err, item)
continue
}
cronTask.Name = itemName
if cronTask.CronSchedule, err = parseYamlSchedule(cronTask.Schedule); err != nil {
wingmate.Log().Error().Msgf("parsing cron schedule: %+v", err)
continue
}
crones = append(crones, cronTask)
}
return services, crones, nil
}
func parseYamlSchedule(input string) (schedule CronSchedule, err error) {
var (
parts []string
pSched *CronSchedule
)
parts = crontabScheduleRegex.FindStringSubmatch(input)
if len(parts) != CrontabScheduleSubMatchLen {
return schedule, fmt.Errorf("invalid schedule: %s", input)
}
pSched = &schedule
if err = pSched.setField(minute, parts[1]); err != nil {
return schedule, fmt.Errorf("error parsing Minute field: %w", err)
}
if err = pSched.setField(hour, parts[2]); err != nil {
return schedule, fmt.Errorf("error parsing Hour field: %w", err)
}
if err = pSched.setField(dom, parts[3]); err != nil {
return schedule, fmt.Errorf("error parsing Day of Month field: %w", err)
}
if err = pSched.setField(month, parts[4]); err != nil {
return schedule, fmt.Errorf("error parsing Month field: %w", err)
}
if err = pSched.setField(dow, parts[5]); err != nil {
return schedule, fmt.Errorf("error parsing Day of Week field: %w", err)
}
return
}
func (c *CronSchedule) setField(field cronField, input string) error {
var (
fr *fieldRange
cField *CronTimeSpec
err error
parsed64 uint64
parsed uint8
multi []uint8
current uint8
multiStr []string
)
switch field {
case minute:
fr = newRange(0, 59)
cField = &c.Minute
case hour:
fr = newRange(0, 23)
cField = &c.Hour
case dom:
fr = newRange(1, 31)
cField = &c.DoM
case month:
fr = newRange(1, 12)
cField = &c.Month
case dow:
fr = newRange(0, 6)
cField = &c.DoW
default:
return errors.New("invalid cron field descriptor")
}
if input == "*" {
*cField = &SpecAny{}
} else if strings.HasPrefix(input, "*/") {
if parsed64, err = strconv.ParseUint(input[2:], 10, 8); err != nil {
return fmt.Errorf("error parse field %+v with input %s: %w", field, input, err)
}
parsed = uint8(parsed64)
if !fr.valid(parsed) {
return fmt.Errorf("error parse field %+v with input %s parsed to %d: invalid value", field, input, parsed)
}
multi = make([]uint8, 0)
current = parsed
for fr.valid(current) {
multi = append(multi, current)
current += parsed
}
*cField = &SpecMultiOccurrence{
values: multi,
}
} else {
multiStr = strings.Split(input, ",")
if len(multiStr) > 1 {
multi = make([]uint8, 0)
for _, s := range multiStr {
if parsed64, err = strconv.ParseUint(s, 10, 8); err != nil {
return fmt.Errorf("error parse field %+v with input %s: %w", field, input, err)
}
parsed = uint8(parsed64)
if !fr.valid(parsed) {
return fmt.Errorf("error parse field %+v with input %s: invalid value", field, input)
}
multi = append(multi, parsed)
}
*cField = &SpecMultiOccurrence{
values: multi,
}
} else {
if parsed64, err = strconv.ParseUint(input, 10, 8); err != nil {
return fmt.Errorf("error parse field %+v with input %s: %w", field, input, err)
}
parsed = uint8(parsed64)
if !fr.valid(parsed) {
return fmt.Errorf("error parse field %+v with input %s: invalid value", field, input)
}
*cField = &SpecExact{
value: parsed,
}
}
}
return nil
}

View File

@@ -1,258 +0,0 @@
package config
import (
"fmt"
"os"
"path"
"testing"
"gitea.suyono.dev/suyono/wingmate"
)
const configName = "wingmate.yaml"
func TestYaml(t *testing.T) {
type testEntry struct {
name string
config string
wantErr bool
}
_ = wingmate.NewLog(os.Stderr, wingmate.Caller|wingmate.Time)
tests := []testEntry{
{
name: "positive",
config: yamlTestCase0,
wantErr: false,
},
{
name: "service only",
config: yamlTestCase1,
wantErr: false,
},
{
name: "cron only",
config: yamlTestCase2,
wantErr: false,
},
{
name: "invalid content - service",
config: yamlTestCase3,
wantErr: false,
},
}
for i, tc := range yamlBlobs {
tests = append(tests, testEntry{
name: fmt.Sprintf("negative - %d", i),
config: tc,
wantErr: true,
})
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
setup(t)
defer tear(t)
writeYaml(t, path.Join(configDir, configName), tt.config)
cfg, err := Read()
if tt.wantErr != (err != nil) {
t.Fatalf("wantErr is %v but err is %+v", tt.wantErr, err)
}
t.Logf("cfg: %+v", cfg)
})
}
}
func writeYaml(t *testing.T, path, content string) {
var (
f *os.File
err error
)
if f, err = os.Create(path); err != nil {
t.Fatal("create yaml file", err)
}
defer func() {
_ = f.Close()
}()
if _, err = f.Write([]byte(content)); err != nil {
t.Fatal("write yaml file", err)
}
}
const yamlTestCase0 = `version: "1"
service:
one:
command: ["command", "arg0", "arg1"]
environ: ["ENV1=value1", "ENV2=valueX"]
user: "user1"
group: "999"
working_dir: "/path/to/working"
cron:
cron-one:
command:
- command-cron
- arg0
- arg1
environ: ["ENV1=v1", "ENV2=var2"]
user: "1001"
group: "978"
schedule: "*/5 * * * 2,3"`
const yamlTestCase1 = `version: "1"
service:
one:
command: ["command", "arg0", "arg1"]
environ: ["ENV1=value1", "ENV2=valueX"]
user: "user1"
group: "999"
working_dir: "/path/to/working"`
const yamlTestCase2 = `version: "1"
cron:
cron-one:
command:
- command-cron
- arg0
- arg1
environ: ["ENV1=v1", "ENV2=var2"]
user: "1001"
group: "978"
schedule: "*/5 * * * 2,3"`
const yamlTestCase3 = `version: "1"
service:
one:
command: 12345
environ: ["ENV1=value1", "ENV2=valueX"]
user: "user1"
group: "999"
working_dir: "/path/to/working"`
var yamlBlobs = []string{
`version: "1"
cron:
cron-one:
command:
- command-cron
- arg0
- arg1
environ: ["ENV1=v1", "ENV2=var2"]
user: "1001"
group: "978"
schedule: "a 13 3,5,7 * *"`,
`version: "1"
cron:
cron-one:
command:
- command-cron
- arg0
- arg1
environ: ["ENV1=v1", "ENV2=var2"]
user: "1001"
group: "978"
schedule: "*/5 a 3,5,7 * *"`,
`version: "1"
cron:
cron-one:
command:
- command-cron
- arg0
- arg1
environ: ["ENV1=v1", "ENV2=var2"]
user: "1001"
group: "978"
schedule: "*/5 13 a * *"`,
`version: "1"
cron:
cron-one:
command:
- command-cron
- arg0
- arg1
environ: ["ENV1=v1", "ENV2=var2"]
user: "1001"
group: "978"
schedule: "*/5 13 3,5,7 a *"`,
`version: "1"
cron:
cron-one:
command:
- command-cron
- arg0
- arg1
environ: ["ENV1=v1", "ENV2=var2"]
user: "1001"
group: "978"
schedule: "*/5 13 3,5,7 * a"`,
`version: "1"
cron:
cron-one:
command:
- command-cron
- arg0
- arg1
environ: ["ENV1=v1", "ENV2=var2"]
user: "1001"
group: "978"
schedule: "*/x 13 3,5,7 * *"`,
`version: "1"
cron:
cron-one:
command:
- command-cron
- arg0
- arg1
environ: ["ENV1=v1", "ENV2=var2"]
user: "1001"
group: "978"
schedule: "76 13 3,5,7 * *"`,
`version: "1"
cron:
cron-one:
command:
- command-cron
- arg0
- arg1
environ: ["ENV1=v1", "ENV2=var2"]
user: "1001"
group: "978"
schedule: "*/75 13 3,5,7 * *"`,
`version: "1"
cron:
cron-one:
command:
- command-cron
- arg0
- arg1
environ: ["ENV1=v1", "ENV2=var2"]
user: "1001"
group: "978"
schedule: "*/5 13 3,x,7 * *"`,
`version: "1"
cron:
cron-one:
command:
- command-cron
- arg0
- arg1
environ: ["ENV1=v1", "ENV2=var2"]
user: "1001"
group: "978"
schedule: "*/5 13 3,5,67 * *"`,
`version: "1"
cron:
cron-one:
command:
- command-cron
- arg0
- arg1
environ: ["ENV1=v1", "ENV2=var2"]
user: "1001"
group: "978"
schedule: "*/5 13 * *"`,
}

View File

@@ -1,24 +0,0 @@
FROM golang:1.24-alpine3.22 AS builder
ADD . /root/wingmate
WORKDIR /root/wingmate/
ARG TEST_BUILD
#RUN apk update && apk add git make build-base && \
# CGO_ENABLED=1 make all && \
# make DESTDIR=/usr/local/bin/wingmate install
RUN apk update && apk add git make && \
make all && make DESTDIR=/usr/local/bin/wingmate install
FROM alpine:3.22
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
ENTRYPOINT [ "/usr/local/bin/entry.sh" ]
CMD [ "/usr/local/bin/wingmate" ]

View File

@@ -1,7 +0,0 @@
#!/bin/sh
if [ $# -gt 0 ]; then
exec "$@"
else
exec /usr/local/bin/wingmate
fi

View File

@@ -1,59 +0,0 @@
service:
# one:
# command: [ "wmstarter" ]
# environ: [ "DUMMY_PATH=/workspace/wingmate/cmd/experiment/dummy/dummy" ]
spawner:
command: [ "wmspawner" ]
user: "1200"
bgtest:
command:
- "wmstarter"
- "--no-wait"
- "--"
- "wmexec"
- "--setsid"
- "--"
- "wmbg"
- "--name"
- "test-run"
- "--pause"
- "10"
- "--log-path"
- "/var/log/wmbg.log"
- "--pid-file"
- "/var/run/wmbg.pid"
pidfile: "/var/run/wmbg.pid"
cron:
cron1:
command:
- "wmoneshot"
- "--"
- "sleep"
- "5"
schedule: "*/5 * * * *"
environ:
- "WINGMATE_LOG=/var/log/cron1.log"
- "WINGMATE_LOG_MESSAGE=cron executed in minute 5,10,15,20,25,30,35,40,45,50,55"
cron2:
command:
- "wmoneshot"
- "--"
- "sleep"
- "5"
schedule: "17,42 */2 * * *"
environ:
- "WINGMATE_LOG=/var/log/cron2.log"
- "WINGMATE_LOG_MESSAGE=cron scheduled using 17,42 */2 * * *"
cron3:
command:
- "wmoneshot"
- "--"
- "sleep"
- "5"
schedule: "7,19,23,47 22 * * *"
environ:
- "WINGMATE_LOG=/var/log/cron3.log"
- "WINGMATE_LOG_MESSAGE=cron scheduled using 7,19,23,47 22 * * *"

View File

@@ -1,20 +0,0 @@
FROM golang:1.24-bookworm AS builder
ADD . /root/wingmate
WORKDIR /root/wingmate/
ARG TEST_BUILD
RUN make all && make DESTDIR=/usr/local/bin/wingmate install
FROM debian:bookworm
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
ENTRYPOINT [ "/usr/local/bin/entry.sh" ]
CMD [ "/usr/local/bin/wingmate" ]

View File

@@ -1,7 +0,0 @@
#!/usr/bin/bash
if [ $# -gt 0 ]; then
exec "$@"
else
exec /usr/local/bin/wingmate
fi

View File

@@ -1,59 +0,0 @@
service:
# one:
# command: [ "wmstarter" ]
# environ: [ "DUMMY_PATH=/workspace/wingmate/cmd/experiment/dummy/dummy" ]
spawner:
command: [ "wmspawner" ]
user: "1200"
bgtest:
command:
- "wmstarter"
- "--no-wait"
- "--"
- "wmexec"
- "--setsid"
- "--"
- "wmbg"
- "--name"
- "test-run"
- "--pause"
- "10"
- "--log-path"
- "/var/log/wmbg.log"
- "--pid-file"
- "/var/run/wmbg.pid"
pidfile: "/var/run/wmbg.pid"
cron:
cron1:
command:
- "wmoneshot"
- "--"
- "sleep"
- "5"
schedule: "*/5 * * * *"
environ:
- "WINGMATE_LOG=/var/log/cron1.log"
- "WINGMATE_LOG_MESSAGE=cron executed in minute 5,10,15,20,25,30,35,40,45,50,55"
cron2:
command:
- "wmoneshot"
- "--"
- "sleep"
- "5"
schedule: "17,42 */2 * * *"
environ:
- "WINGMATE_LOG=/var/log/cron2.log"
- "WINGMATE_LOG_MESSAGE=cron scheduled using 17,42 */2 * * *"
cron3:
command:
- "wmoneshot"
- "--"
- "sleep"
- "5"
schedule: "7,19,23,47 22 * * *"
environ:
- "WINGMATE_LOG=/var/log/cron3.log"
- "WINGMATE_LOG_MESSAGE=cron scheduled using 7,19,23,47 22 * * *"

View File

@@ -1,8 +0,0 @@
FROM suyono/wingmate:test AS source
FROM alpine:3.20
COPY --from=source /usr/local/bin/ /usr/local/bin/
ENTRYPOINT [ "/usr/local/bin/entry.sh" ]
CMD [ "/usr/local/bin/wingmate" ]

View File

@@ -1,8 +0,0 @@
FROM suyono/wingmate:test AS source
FROM debian:bookworm
COPY --from=source /usr/local/bin/ /usr/local/bin/
ENTRYPOINT [ "/usr/local/bin/entry.sh" ]
CMD [ "/usr/local/bin/wingmate" ]

View File

@@ -1,14 +0,0 @@
FROM suyono/wingmate:alpine as source
FROM alpine:3.19
RUN apk update && apk add tzdata openssh-server && \
ln -s /usr/share/zoneinfo/Australia/Sydney /etc/localtime && ssh-keygen -A
COPY --from=source /usr/local/bin/wingmate /usr/local/bin/
COPY --from=source /usr/local/bin/wmpidproxy /usr/local/bin/
ADD --chmod=755 example/ssh-docker/entry.sh /usr/local/bin/entry.sh
ADD --chmod=755 example/ssh-docker/etc /etc
ENTRYPOINT [ "/usr/local/bin/entry.sh" ]
CMD [ "/usr/local/bin/wingmate" ]

View File

@@ -1,7 +0,0 @@
#!/bin/sh
if [ $# -gt 0 ]; then
exec "$@"
else
exec /usr/local/bin/wingmate
fi

View File

@@ -1,3 +0,0 @@
#!/bin/sh
exec /usr/local/bin/wmpidproxy --pid-file /var/run/sshd.pid -- /usr/sbin/sshd

9
files/testconfig/one.go Normal file
View File

@@ -0,0 +1,9 @@
package testconfig
var One = `service:
one:
command: "mycommand -o output"
two:
command: ["cmd", "now"]
workdir: /
`

6
files/testconfig/one.yml Normal file
View File

@@ -0,0 +1,6 @@
service:
one:
command: "mycommand -o output"
two:
command: ["cmd", "now"]
workdir: /

39
go.mod
View File

@@ -1,31 +1,38 @@
module gitea.suyono.dev/suyono/wingmate module gitea.suyono.dev/suyono/wingmate
go 1.24 go 1.21
require ( require (
github.com/rs/zerolog v1.34.0 github.com/rs/zerolog v1.30.0
github.com/spf13/cobra v1.9.1 github.com/spf13/cobra v1.7.0
github.com/spf13/pflag v1.0.6 github.com/spf13/viper v1.17.0
github.com/spf13/viper v1.20.1 github.com/stretchr/testify v1.8.4
github.com/stretchr/testify v1.10.0 golang.org/x/sys v0.15.0
golang.org/x/sys v0.33.0
) )
require ( require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/sagikazarmark/locafero v0.9.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.14.0 // indirect github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.8.0 // indirect 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/subosito/gotenv v1.6.0 // 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 go.uber.org/multierr v1.11.0 // indirect
golang.org/x/text v0.25.0 // indirect golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

542
go.sum
View File

@@ -1,69 +1,533 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk= github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc=
github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg=
github.com/spf13/viper v1.17.0 h1:I5txKw7MJasPL/BrfkbA0Jyo/oELqVmux4pR/UxOMfI=
github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0+yVI=
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/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.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= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No=
golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View File

@@ -1,8 +1,6 @@
package init package init
import ( import (
"io"
"os"
"os/exec" "os/exec"
"sync" "sync"
"time" "time"
@@ -10,72 +8,19 @@ import (
"gitea.suyono.dev/suyono/wingmate" "gitea.suyono.dev/suyono/wingmate"
) )
const ( func (i *Init) cron(wg *sync.WaitGroup, cron Cron, exitFlag <-chan any) {
cronTag = "cron"
)
func (i *Init) cron(wg *sync.WaitGroup, cron CronTask, exitFlag <-chan any) {
defer wg.Done() defer wg.Done()
var ( ticker := time.NewTicker(time.Second * 20)
iwg *sync.WaitGroup
err error
stdout io.ReadCloser
stderr io.ReadCloser
cmd *exec.Cmd
patchedEnv []string
)
ticker := time.NewTicker(time.Second * 30)
cron: cron:
for { for {
if cron.TimeToRun(time.Now()) { if cron.TimeToRun(time.Now()) {
wingmate.Log().Info().Str(cronTag, cron.Name()).Msg("executing") cmd := exec.Command(cron.Command().Path())
if err = cron.UtilDepCheck(); err != nil { if err := cmd.Run(); err != nil {
wingmate.Log().Error().Str(cronTag, cron.Name()).Msgf("%+v", err) wingmate.Log().Error().Msgf("running cron %s error %#v", cron.Command().Path(), err)
goto fail
} }
patchedEnv = cron.PatchEnv(os.Environ())
cmd = exec.Command(cron.Command(patchedEnv...), cron.Arguments(patchedEnv...)...)
cmd.Env = patchedEnv
if len(cron.WorkingDir()) > 0 {
cmd.Dir = cron.WorkingDir()
} }
iwg = &sync.WaitGroup{}
if stdout, err = cmd.StdoutPipe(); err != nil {
wingmate.Log().Error().Str(cronTag, cron.Name()).Msgf("stdout pipe: %+v", err)
goto fail
}
if stderr, err = cmd.StderrPipe(); err != nil {
wingmate.Log().Error().Str(cronTag, cron.Name()).Msgf("stderr pipe: %+v", err)
_ = stdout.Close()
goto fail
}
iwg.Add(1)
go i.pipeReader(iwg, stdout, cronTag, cron.Name())
iwg.Add(1)
go i.pipeReader(iwg, stderr, cronTag, cron.Name())
if err := cmd.Start(); err != nil {
wingmate.Log().Error().Msgf("starting cron %s error %+v", cron.Name(), err)
_ = stdout.Close()
_ = stderr.Close()
iwg.Wait()
goto fail
}
iwg.Wait()
_ = cmd.Wait()
}
fail:
select { select {
case <-exitFlag: case <-exitFlag:
ticker.Stop() ticker.Stop()

View File

@@ -1,56 +1,22 @@
package init package init
import ( import (
"os"
"sync" "sync"
"time" "time"
) )
type Tasks interface { type Path interface {
List() []Task Path() string
Services() []ServiceTask
Crones() []CronTask
Get(string) (Task, error)
} }
type UserGroup interface { type Cron interface {
String() string Command() Path
IsSet() bool
}
type TaskStatus interface {
}
type Task interface {
Name() string
Command(...string) string
Arguments(...string) []string
EnvLen() int
Environ() []string
PatchEnv([]string) []string
Setsid() bool
UserGroup() UserGroup
WorkingDir() string
Status() TaskStatus
UtilDepCheck() error
}
type CronTask interface {
Task
TimeToRun(time.Time) bool TimeToRun(time.Time) bool
} }
type ServiceTask interface {
Task
Background() bool //NOTE: implies using wmpidproxy
PidFile() string //NOTE: implies using wmpidproxy
StartSecs() uint
AutoStart() bool
AutoRestart() bool
}
type Config interface { type Config interface {
Tasks() Tasks Services() []Path
Cron() []Cron
} }
type Init struct { type Init struct {
@@ -68,26 +34,24 @@ func (i *Init) Start() {
wg *sync.WaitGroup wg *sync.WaitGroup
signalTrigger chan any signalTrigger chan any
sighandlerExit chan any sighandlerExit chan any
sigchld chan os.Signal
) )
signalTrigger = make(chan any) signalTrigger = make(chan any)
sighandlerExit = make(chan any) sighandlerExit = make(chan any)
sigchld = make(chan os.Signal, 1)
wg = &sync.WaitGroup{} wg = &sync.WaitGroup{}
wg.Add(1) wg.Add(1)
go i.waiter(wg, signalTrigger, sighandlerExit, sigchld) go i.waiter(wg, signalTrigger, sighandlerExit)
wg.Add(1) wg.Add(1)
go i.sighandler(wg, signalTrigger, sighandlerExit, sigchld) go i.sighandler(wg, signalTrigger, sighandlerExit)
for _, s := range i.config.Tasks().Services() { for _, s := range i.config.Services() {
wg.Add(1) wg.Add(1)
go i.service(wg, s, signalTrigger) go i.service(wg, s, signalTrigger)
} }
for _, c := range i.config.Tasks().Crones() { for _, c := range i.config.Cron() {
wg.Add(1) wg.Add(1)
go i.cron(wg, c, signalTrigger) go i.cron(wg, c, signalTrigger)
} }

View File

@@ -1,91 +1,26 @@
package init package init
import ( import (
"bufio"
"io"
"os"
"os/exec" "os/exec"
"sync" "sync"
"time"
"gitea.suyono.dev/suyono/wingmate" "gitea.suyono.dev/suyono/wingmate"
) )
const ( func (i *Init) service(wg *sync.WaitGroup, path Path, exitFlag <-chan any) {
serviceTag = "service"
)
func (i *Init) service(wg *sync.WaitGroup, task ServiceTask, exitFlag <-chan any) {
defer wg.Done() defer wg.Done()
var ( var (
err error err error
iwg *sync.WaitGroup
stderr io.ReadCloser
stdout io.ReadCloser
failStatus bool
cmd *exec.Cmd
patchedEnv []string
) )
defer func() {
wingmate.Log().Info().Str(serviceTag, task.Name()).Msg("stopped")
}()
service: service:
for { for {
failStatus = false cmd := exec.Command(path.Path())
if err = task.UtilDepCheck(); err != nil { if err = cmd.Run(); err != nil {
wingmate.Log().Error().Str(serviceTag, task.Name()).Msgf("%+v", err) wingmate.Log().Error().Msgf("starting service %s error %#v", path.Path(), err)
failStatus = true
goto fail
} }
patchedEnv = task.PatchEnv(os.Environ())
cmd = exec.Command(task.Command(patchedEnv...), task.Arguments(patchedEnv...)...)
cmd.Env = patchedEnv
if len(task.WorkingDir()) > 0 {
cmd.Dir = task.WorkingDir()
}
iwg = &sync.WaitGroup{}
if stdout, err = cmd.StdoutPipe(); err != nil {
wingmate.Log().Error().Str(serviceTag, task.Name()).Msgf("stdout pipe: %#v", err)
failStatus = true
goto fail
}
iwg.Add(1)
go i.pipeReader(iwg, stdout, serviceTag, task.Name())
if stderr, err = cmd.StderrPipe(); err != nil {
wingmate.Log().Error().Str(serviceTag, task.Name()).Msgf("stderr pipe: %#v", err)
_ = stdout.Close()
failStatus = true
goto fail
}
iwg.Add(1)
go i.pipeReader(iwg, stderr, serviceTag, task.Name())
if err = cmd.Start(); err != nil {
wingmate.Log().Error().Msgf("starting service %s error %#v", task.Name(), err)
failStatus = true
_ = stdout.Close()
_ = stderr.Close()
iwg.Wait()
goto fail
}
iwg.Wait()
_ = cmd.Wait()
fail:
if failStatus {
time.Sleep(time.Second)
failStatus = false
}
select { select {
case <-exitFlag: case <-exitFlag:
break service break service
@@ -94,18 +29,3 @@ service:
} }
} }
func (i *Init) pipeReader(wg *sync.WaitGroup, pipe io.ReadCloser, tag, serviceName string) {
defer wg.Done()
scanner := bufio.NewScanner(pipe)
for scanner.Scan() {
wingmate.Log().Info().Str(tag, serviceName).Msg(scanner.Text())
}
if err := scanner.Err(); err != nil {
wingmate.Log().Error().Str(tag, serviceName).Msgf("got error when reading pipe: %#v", err)
}
wingmate.Log().Debug().Str(tag, serviceName).Msg("closing pipe")
}

View File

@@ -5,44 +5,32 @@ import (
"os/signal" "os/signal"
"sync" "sync"
"gitea.suyono.dev/suyono/wingmate"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
func (i *Init) sighandler(wg *sync.WaitGroup, trigger chan<- any, selfExit <-chan any, sigchld chan<- os.Signal) { func (i *Init) sighandler(wg *sync.WaitGroup, trigger chan<- any, selfExit <-chan any) {
defer wg.Done() defer wg.Wait()
defer func() {
wingmate.Log().Warn().Msg("signal handler: exiting")
}()
isOpen := true isOpen := true
c := make(chan os.Signal, 1) c := make(chan os.Signal, 1)
signal.Notify(c, unix.SIGINT, unix.SIGTERM, unix.SIGQUIT, unix.SIGCHLD) signal.Notify(c, unix.SIGINT, unix.SIGTERM, unix.SIGCHLD)
signal: signal:
for { for {
select { select {
case s := <-c: case s := <-c:
switch s { switch s {
case unix.SIGTERM, unix.SIGINT, unix.SIGQUIT: case unix.SIGTERM, unix.SIGINT:
if isOpen { if isOpen {
wingmate.Log().Info().Msg("initiating shutdown...")
close(trigger) close(trigger)
wg.Add(1)
go i.signalPump(wg, selfExit)
isOpen = false isOpen = false
} }
case unix.SIGCHLD: case unix.SIGCHLD:
select { // do nothing
case sigchld <- s:
default:
}
} }
case <-selfExit: case <-selfExit:
wingmate.Log().Warn().Msg("signal handler received completion flag")
break signal break signal
} }
} }

View File

@@ -1,71 +0,0 @@
package init
import (
"sync"
"time"
"gitea.suyono.dev/suyono/wingmate"
"golang.org/x/sys/unix"
)
type status int
const (
triggered status = iota
expired
)
func (i *Init) signalPump(wg *sync.WaitGroup, selfExit <-chan any) {
defer wg.Done()
defer func() {
wingmate.Log().Info().Msg("signal pump completed")
}()
if seStatus := i.sigTermPump(time.Now(), selfExit); seStatus == triggered {
return
}
i.sigKillPump(time.Now(), selfExit)
}
func (i *Init) sigKillPump(startTime time.Time, selfExit <-chan any) {
t := time.NewTicker(time.Millisecond * 200)
defer t.Stop()
wingmate.Log().Info().Msg("start pumping SIGKILL signal")
defer func() {
wingmate.Log().Info().Msg("stop pumping SIGKILL signal")
}()
for time.Since(startTime) < time.Second {
_ = unix.Kill(-1, unix.SIGKILL)
select {
case <-t.C:
case <-selfExit:
return
}
}
}
func (i *Init) sigTermPump(startTime time.Time, selfExit <-chan any) status {
t := time.NewTicker(time.Millisecond * 100)
defer t.Stop()
wingmate.Log().Debug().Msg("start pumping SIGTERM signal")
defer func() {
wingmate.Log().Debug().Msg("stop pumping SIGTERM signal")
}()
for time.Since(startTime) < time.Duration(time.Second*4) {
_ = unix.Kill(-1, unix.SIGTERM)
select {
case <-t.C:
case <-selfExit:
return triggered
}
}
return expired
}

View File

@@ -2,68 +2,35 @@ package init
import ( import (
"errors" "errors"
"os"
"sync" "sync"
"gitea.suyono.dev/suyono/wingmate"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
func (i *Init) waiter(wg *sync.WaitGroup, runningFlag <-chan any, sigHandlerFlag chan<- any, sigchld <-chan os.Signal) { func (i *Init) waiter(wg *sync.WaitGroup, runningFlag <-chan any, sigHandlerFlag chan<- any) {
var ( var (
ws unix.WaitStatus ws unix.WaitStatus
// pid int
err error err error
running bool running bool
flagged bool
waitingForSignal bool
) )
defer wg.Done()
defer func() {
wingmate.Log().Debug().Msg("waiter exiting...")
}()
running = true running = true
flagged = true
waitingForSignal = true
wait: wait:
for { for {
if running {
if waitingForSignal {
select { select {
case <-runningFlag: case <-runningFlag:
wingmate.Log().Info().Msg("waiter received shutdown signal...")
running = false
case <-sigchld:
waitingForSignal = false
}
} else {
select {
case <-runningFlag:
wingmate.Log().Debug().Msg("waiter received shutdown signal...")
running = false running = false
default: default:
} }
}
}
if _, err = unix.Wait4(-1, &ws, 0, nil); err != nil { if _, err = unix.Wait4(-1, &ws, 0, nil); err != nil {
if errors.Is(err, unix.ECHILD) { if errors.Is(err, unix.ECHILD) {
if !running { if !running {
if flagged {
close(sigHandlerFlag) close(sigHandlerFlag)
flagged = false
wingmate.Log().Debug().Msg("waiter: inner flag")
}
wingmate.Log().Debug().Msg("waiter: no child left")
break wait break wait
} }
} }
wingmate.Log().Debug().Msgf("Wait4 returns error: %+v", err)
waitingForSignal = true
} }
} }
} }

135
logger.go
View File

@@ -1,40 +1,9 @@
package wingmate package wingmate
import ( import (
"io"
"time"
"gitea.suyono.dev/suyono/wingmate/logger" "gitea.suyono.dev/suyono/wingmate/logger"
"github.com/rs/zerolog" "github.com/rs/zerolog"
) "io"
const (
timeTag = "time"
)
const (
Time = 1 << iota
Caller
)
const (
PanicLevel = 5 - iota
FatalLevel
ErrorLevel
WarnLevel
InfoLevel
DebugLevel
TraceLevel
)
const (
PanicLevelStr = "panic"
FatalLevelStr = "fatal"
ErrorLevelStr = "error"
WarnLevelStr = "warn"
InfoLevelStr = "info"
DebugLevelStr = "debug"
TraceLevelStr = "trace"
) )
var ( var (
@@ -43,13 +12,11 @@ var (
type wrapper struct { type wrapper struct {
log zerolog.Logger log zerolog.Logger
flag int
} }
func NewLog(wc io.WriteCloser, flag int) error { func NewLog(wc io.WriteCloser) error {
w = &wrapper{ w = &wrapper{
log: zerolog.New(wc), log: zerolog.New(wc),
flag: flag,
} }
return nil return nil
} }
@@ -61,106 +28,14 @@ func Log() logger.Log {
return w return w
} }
func SetGlobalLevel(level int) {
switch level {
case PanicLevel:
zerolog.SetGlobalLevel(zerolog.PanicLevel)
case FatalLevel:
zerolog.SetGlobalLevel(zerolog.FatalLevel)
case ErrorLevel:
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
case WarnLevel:
zerolog.SetGlobalLevel(zerolog.WarnLevel)
case InfoLevel:
zerolog.SetGlobalLevel(zerolog.InfoLevel)
case DebugLevel:
zerolog.SetGlobalLevel(zerolog.DebugLevel)
case TraceLevel:
zerolog.SetGlobalLevel(zerolog.TraceLevel)
}
}
func SetGlobalLevelStr(level string) {
switch level {
case PanicLevelStr:
zerolog.SetGlobalLevel(zerolog.PanicLevel)
case FatalLevelStr:
zerolog.SetGlobalLevel(zerolog.FatalLevel)
case ErrorLevelStr:
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
case WarnLevelStr:
zerolog.SetGlobalLevel(zerolog.WarnLevel)
case InfoLevelStr:
zerolog.SetGlobalLevel(zerolog.InfoLevel)
case DebugLevelStr:
zerolog.SetGlobalLevel(zerolog.DebugLevel)
case TraceLevelStr:
zerolog.SetGlobalLevel(zerolog.TraceLevel)
}
}
func (w *wrapper) flagPass(e *zerolog.Event) *zerolog.Event {
if w.flag&Time != 0 {
e = e.Time(timeTag, time.Now())
}
if w.flag&Caller != 0 {
e = e.Caller(2)
}
return e
}
func (w *wrapper) Info() logger.Content { func (w *wrapper) Info() logger.Content {
return (*eventWrapper)(w.flagPass(w.log.Info())) return w.log.Info()
}
func (w *wrapper) Debug() logger.Content {
return (*eventWrapper)(w.flagPass(w.log.Debug()))
}
func (w *wrapper) Trace() logger.Content {
return (*eventWrapper)(w.flagPass(w.log.Trace()))
} }
func (w *wrapper) Warn() logger.Content { func (w *wrapper) Warn() logger.Content {
return (*eventWrapper)(w.flagPass(w.log.Warn())) return w.log.Warn()
} }
func (w *wrapper) Error() logger.Content { func (w *wrapper) Error() logger.Content {
return (*eventWrapper)(w.flagPass(w.log.Error())) return w.log.Error()
}
func (w *wrapper) Fatal() logger.Content {
return (*eventWrapper)(w.flagPass(w.log.Fatal()))
}
type eventWrapper zerolog.Event
func (w *eventWrapper) Msg(msg string) {
(*zerolog.Event)(w).Msg(msg)
}
func (w *eventWrapper) Msgf(format string, data ...any) {
(*zerolog.Event)(w).Msgf(format, data...)
}
func (w *eventWrapper) Str(key, value string) logger.Content {
rv := (*zerolog.Event)(w).Str(key, value)
return (*eventWrapper)(rv)
}
func (w *eventWrapper) Err(err error) logger.Content {
rv := (*zerolog.Event)(w).Err(err)
return (*eventWrapper)(rv)
}
func (w *eventWrapper) Caller() logger.Content {
rv := (*zerolog.Event)(w).Caller()
return (*eventWrapper)(rv)
}
func (w *eventWrapper) Time(tag string, now time.Time) logger.Content {
rv := (*zerolog.Event)(w).Time(tag, now)
return (*eventWrapper)(rv)
} }

View File

@@ -3,19 +3,14 @@ package logger
type Content interface { type Content interface {
Msg(string) Msg(string)
Msgf(string, ...any) Msgf(string, ...any)
Str(string, string) Content
Err(error) Content
} }
type LogLevel interface { type Level interface {
Info() Content Info() Content
Warn() Content Warn() Content
Error() Content Error() Content
Fatal() Content
Debug() Content
Trace() Content
} }
type Log interface { type Log interface {
LogLevel Level
} }

View File

@@ -1,311 +0,0 @@
package task
import (
"crypto/sha256"
"encoding/json"
"fmt"
wmenv "gitea.suyono.dev/suyono/wingmate/task/env"
"time"
"gitea.suyono.dev/suyono/wingmate"
wminit "gitea.suyono.dev/suyono/wingmate/init"
)
type CronSchedule struct {
Minute CronTimeSpec
Hour CronTimeSpec
DoM CronTimeSpec
Month CronTimeSpec
DoW CronTimeSpec
}
type CronTimeSpec interface {
Match(uint8) bool
}
type CronAnySpec struct {
}
func NewCronAnySpec() *CronAnySpec {
return &CronAnySpec{}
}
func (cas *CronAnySpec) Match(u uint8) bool {
return true
}
type CronExactSpec struct {
value uint8
}
func NewCronExactSpec(v uint8) *CronExactSpec {
return &CronExactSpec{
value: v,
}
}
func (ces *CronExactSpec) Match(u uint8) bool {
return u == ces.value
}
type CronMultiOccurrenceSpec struct {
values []uint8
}
func NewCronMultiOccurrenceSpec(v ...uint8) *CronMultiOccurrenceSpec {
retval := &CronMultiOccurrenceSpec{}
if len(v) > 0 {
retval.values = make([]uint8, len(v))
copy(retval.values, v)
}
return retval
}
func (cms *CronMultiOccurrenceSpec) Match(u uint8) bool {
for _, v := range cms.values {
if v == u {
return true
}
}
return false
}
type CronTask struct {
CronSchedule
userGroup
cronScheduleString string
name string
command []string
cmdLine []string
environ []string
fallbackEnv []string
setsid bool
workingDir string
lastRun time.Time
hasRun bool //NOTE: make sure initialised as false
config config
}
func NewCronTask(name string) *CronTask {
return &CronTask{
name: name,
hasRun: false,
}
}
func (c *CronTask) SetCommand(cmds ...string) *CronTask {
c.command = make([]string, len(cmds))
copy(c.command, cmds)
return c
}
func (c *CronTask) SetEnv(envs ...string) *CronTask {
c.environ = make([]string, len(envs))
copy(c.environ, envs)
return c
}
func (c *CronTask) SetFallbackEnv(envs ...string) *CronTask {
c.fallbackEnv = make([]string, len(envs))
copy(c.fallbackEnv, envs)
return c
}
func (c *CronTask) SetFlagSetsid(flag bool) *CronTask {
c.setsid = flag
return c
}
func (c *CronTask) SetWorkingDir(path string) *CronTask {
c.workingDir = path
return c
}
func (c *CronTask) SetUser(user string) *CronTask {
c.user = user
return c
}
func (c *CronTask) SetGroup(group string) *CronTask {
c.group = group
return c
}
func (c *CronTask) SetSchedule(scheduleStr string, schedule CronSchedule) *CronTask {
c.cronScheduleString = scheduleStr
c.CronSchedule = schedule
return c
}
func (c *CronTask) SetConfig(config config) *CronTask {
c.config = config
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: p.Name(),
Command: p.Command(),
Arguments: p.Arguments(),
Environ: p.Environ(),
Setsid: p.Setsid(),
UserGroup: p.UserGroup().String(),
WorkingDir: p.WorkingDir(),
Schedule: p.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 {
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(env ...string) string {
if len(env) > 0 {
return wmenv.ExpandEnv(env, []string{c.cmdLine[0]})[0]
}
return c.cmdLine[0]
}
func (c *CronTask) Arguments(env ...string) []string {
if len(c.cmdLine) == 1 {
return nil
}
argCopy := make([]string, len(c.cmdLine)-1)
copy(argCopy, c.cmdLine[1:])
if len(env) > 0 {
argCopy = wmenv.ExpandEnv(env, argCopy)
}
return argCopy
}
func (c *CronTask) EnvLen() int {
return len(c.environ)
}
func (c *CronTask) Environ() []string {
retval := make([]string, len(c.environ))
copy(retval, c.environ)
return retval
}
func (c *CronTask) PatchEnv(env []string) []string {
env = wmenv.PatchEnv(env, c.environ)
return wmenv.FallbackEnv(env, c.fallbackEnv)
}
func (c *CronTask) Setsid() bool {
return c.setsid
}
func (c *CronTask) UserGroup() wminit.UserGroup {
return &(c.userGroup)
}
func (c *CronTask) WorkingDir() string {
return c.workingDir
}
func (c *CronTask) Status() wminit.TaskStatus {
//TODO: implement me!
panic("not implemented")
// return nil
}
func (c *CronTask) TimeToRun(now time.Time) bool {
if c.Minute.Match(uint8(now.Minute())) &&
c.Hour.Match(uint8(now.Hour())) &&
c.DoM.Match(uint8(now.Day())) &&
c.Month.Match(uint8(now.Month())) &&
c.DoW.Match(uint8(now.Weekday())) {
if c.hasRun {
if now.Sub(c.lastRun) <= time.Minute && now.Minute() == c.lastRun.Minute() {
return false
} else {
c.lastRun = now
return true
}
} else {
c.lastRun = now
c.hasRun = true
return true
}
}
return false
}

127
task/env/env.go vendored
View File

@@ -1,127 +0,0 @@
package env
import (
"fmt"
"gitea.suyono.dev/suyono/wingmate"
"regexp"
"strings"
)
var (
envCapture = regexp.MustCompile(`\$+[a-zA-Z_][a-zA-Z0-9_]*|\$+{[a-zA-Z_][a-zA-Z0-9_]*}`)
envEsc = regexp.MustCompile(`^\$\$+[^$]+$`) // escaped, starts with two or more $ character
envRef = regexp.MustCompile(`^\$([^$]+)$`) // capture the variable name
envRefExplicit = regexp.MustCompile(`^\${([^$]+)}$`) // capture the variable name - explicit
)
func expandEnv(envMap map[string]string, input string) string {
if envEsc.MatchString(input) {
return input
}
if envName := envRefExplicit.FindStringSubmatch(input); envName != nil && envName[1] != "" {
exVal, ok := envMap[envName[1]]
if !ok {
return ""
}
return exVal
}
if envName := envRef.FindStringSubmatch(input); envName != nil && envName[1] != "" {
exVal, ok := envMap[envName[1]]
if !ok {
return ""
}
return exVal
}
return input
}
func PatchEnv(existing, new []string) []string {
tMap := make(map[string]string)
for _, e := range existing {
key, value, ok := strings.Cut(e, "=")
if !ok {
wingmate.Log().Warn().Msgf("removing invalid environment:", e)
continue
}
tMap[key] = value
}
for _, e := range new {
key, value, ok := strings.Cut(e, "=")
if !ok {
wingmate.Log().Warn().Msgf("removing invalid environment:", e)
continue
}
if strings.ContainsAny(key, "$") {
wingmate.Log().Error().Err(fmt.Errorf("variable name contains $")).Msgf("removing invalid environment:", e)
continue
}
value = envCapture.ReplaceAllStringFunc(value, func(rep string) string {
return expandEnv(tMap, rep)
})
tMap[key] = value
}
outEnv := make([]string, 0, len(existing))
for key, val := range tMap {
outEnv = append(outEnv, fmt.Sprintf("%s=%s", key, val))
}
return outEnv
}
func ExpandEnv(env []string, input []string) []string {
envMap := make(map[string]string)
for _, e := range env {
key, value, ok := strings.Cut(e, "=")
if !ok {
wingmate.Log().Warn().Msgf("removing bad environment:", e)
continue
}
envMap[key] = value
}
for i, s := range input {
s = envCapture.ReplaceAllStringFunc(s, func(rep string) string {
return expandEnv(envMap, rep)
})
input[i] = s
}
return input
}
func FallbackEnv(env []string, fallbacks []string) []string {
envMap := make(map[string]string)
for _, e := range env {
key, value, ok := strings.Cut(e, "=")
if !ok {
wingmate.Log().Warn().Msgf("removing bad environment:", e)
continue
}
envMap[key] = value
}
for _, e := range fallbacks {
key, value, ok := strings.Cut(e, "=")
if !ok {
wingmate.Log().Warn().Msgf("removing bad environment:", e)
continue
}
value = envCapture.ReplaceAllStringFunc(value, func(rep string) string {
return expandEnv(envMap, rep)
})
if _, ok = envMap[key]; !ok {
env = append(env, fmt.Sprintf("%s=%s", key, value))
}
}
return env
}

View File

@@ -1,392 +0,0 @@
package task
import (
"crypto/sha256"
"encoding/json"
"fmt"
"gitea.suyono.dev/suyono/wingmate"
wminit "gitea.suyono.dev/suyono/wingmate/init"
wmenv "gitea.suyono.dev/suyono/wingmate/task/env"
)
type config interface {
WMPidProxyPath() string
WMPidProxyCheckVersion() error
WMExecPath() string
WMExecCheckVersion() error
}
type Tasks struct {
services []wminit.ServiceTask
crones []wminit.CronTask
}
func NewTasks() *Tasks {
return &Tasks{
services: make([]wminit.ServiceTask, 0),
crones: make([]wminit.CronTask, 0),
}
}
func (ts *Tasks) AddV0Service(path string) {
ts.AddService(NewServiceTask(path)).SetCommand(path)
}
func (ts *Tasks) AddService(serviceTask *ServiceTask) *ServiceTask {
ts.services = append(ts.services, serviceTask)
return serviceTask
}
func (ts *Tasks) AddV0Cron(schedule CronSchedule, path string) {
ts.AddCron(NewCronTask(path)).SetCommand(path).SetSchedule("", schedule)
}
func (ts *Tasks) AddCron(cronTask *CronTask) *CronTask {
ts.crones = append(ts.crones, cronTask)
return cronTask
}
func (ts *Tasks) List() []wminit.Task {
retval := make([]wminit.Task, 0, len(ts.services)+len(ts.crones))
for _, s := range ts.services {
retval = append(retval, s.(wminit.Task))
}
for _, c := range ts.crones {
retval = append(retval, c.(wminit.Task))
}
return retval
}
func (ts *Tasks) Services() []wminit.ServiceTask {
return ts.services
}
func (ts *Tasks) Crones() []wminit.CronTask {
return ts.crones
}
func (ts *Tasks) Get(name string) (wminit.Task, error) {
//TODO: implement me!
panic("not implemented")
// return nil, nil
}
type ServiceTask struct {
name string
command []string
cmdLine []string
environ []string
fallbackEnv []string
setsid bool
background bool
workingDir string
startSecs uint
pidFile string
config config
userGroup
}
func NewServiceTask(name string) *ServiceTask {
return &ServiceTask{
name: name,
}
}
func (t *ServiceTask) SetCommand(cmds ...string) *ServiceTask {
t.command = make([]string, len(cmds))
copy(t.command, cmds)
return t
}
func (t *ServiceTask) SetEnv(envs ...string) *ServiceTask {
t.environ = make([]string, len(envs))
copy(t.environ, envs)
return t
}
func (t *ServiceTask) SetFallbackEnv(envs ...string) *ServiceTask {
t.fallbackEnv = make([]string, len(envs))
copy(t.fallbackEnv, envs)
return t
}
func (t *ServiceTask) SetFlagSetsid(flag bool) *ServiceTask {
t.setsid = flag
return t
}
func (t *ServiceTask) SetWorkingDir(path string) *ServiceTask {
t.workingDir = path
return t
}
func (t *ServiceTask) SetUser(user string) *ServiceTask {
t.user = user
return t
}
func (t *ServiceTask) SetGroup(group string) *ServiceTask {
t.group = group
return t
}
func (t *ServiceTask) SetStartSecs(secs uint) *ServiceTask {
t.startSecs = secs
return t
}
func (t *ServiceTask) SetPidFile(path string) *ServiceTask {
t.pidFile = path
if len(path) > 0 {
t.background = true
} else {
t.background = false
}
return t
}
func (t *ServiceTask) SetConfig(config config) *ServiceTask {
t.config = config
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 {
// call this function for validate the field
return validate( /* input the validators here */ )
}
func (t *ServiceTask) Name() string {
return t.name
}
func (t *ServiceTask) prepareCommandLine() []string {
//TODO: is this method used somewhere? if not, can I remove this?
if len(t.cmdLine) > 0 {
return t.cmdLine
}
t.cmdLine = make([]string, 0)
if t.background {
t.cmdLine = append(t.cmdLine, t.config.WMPidProxyPath(), "--pid-file", t.pidFile, "--")
}
if t.setsid || t.UserGroup().IsSet() {
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 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(env ...string) string {
if len(env) > 0 {
return wmenv.ExpandEnv(env, []string{t.cmdLine[0]})[0]
}
return t.cmdLine[0]
}
func (t *ServiceTask) Arguments(env ...string) []string {
if len(t.cmdLine) == 1 {
return nil
}
argCopy := make([]string, len(t.cmdLine)-1)
copy(argCopy, t.cmdLine[1:])
if len(env) > 0 {
argCopy = wmenv.ExpandEnv(env, argCopy)
}
return argCopy
}
func (t *ServiceTask) EnvLen() int {
return len(t.environ)
}
func (t *ServiceTask) Environ() []string {
retval := make([]string, len(t.environ))
copy(retval, t.environ)
return retval
}
func (t *ServiceTask) PatchEnv(env []string) []string {
env = wmenv.PatchEnv(env, t.environ)
return wmenv.FallbackEnv(env, t.fallbackEnv)
}
func (t *ServiceTask) Setsid() bool {
return t.setsid
}
func (t *ServiceTask) UserGroup() wminit.UserGroup {
return &(t.userGroup)
}
func (t *ServiceTask) Background() bool {
return t.background
}
func (t *ServiceTask) WorkingDir() string {
return t.workingDir
}
func (t *ServiceTask) Status() wminit.TaskStatus {
//TODO: implement me!
panic("not implemented")
// return nil
}
func (t *ServiceTask) AutoStart() bool {
//TODO: implement me!
panic("not implemented")
// return false
}
func (t *ServiceTask) AutoRestart() bool {
//TODO: implement me!
panic("not implemented")
// return false
}
func (t *ServiceTask) StartSecs() uint {
return t.startSecs
}
func (t *ServiceTask) PidFile() string {
return t.pidFile
}
type userGroup struct {
user string
group string
}
func (ug *userGroup) IsSet() bool {
return len(ug.user) > 0 || len(ug.group) > 0
}
func (ug *userGroup) String() string {
if len(ug.group) > 0 {
return fmt.Sprintf("%s:%s", ug.user, ug.group)
}
return ug.user
}
func validate(validators ...func() error) error {
var err error
for _, v := range validators {
if err = v(); err != nil {
return err
}
}
return nil
}

View File

@@ -1,131 +0,0 @@
package task
import (
"os"
"testing"
"gitea.suyono.dev/suyono/wingmate"
wminit "gitea.suyono.dev/suyono/wingmate/init"
"github.com/stretchr/testify/assert"
)
func TestServicesV0(t *testing.T) {
service := "/path/to/executable"
tasks := NewTasks()
tasks.AddV0Service(service)
assert.Equal(t, tasks.Services()[0].Name(), service)
assert.ElementsMatch(t, tasks.Services()[0].Command(), []string{service})
}
func TestCronV0(t *testing.T) {
cron := "/path/to/executable"
tasks := NewTasks()
tasks.AddV0Cron(CronSchedule{
Minute: NewCronAnySpec(),
Hour: NewCronAnySpec(),
DoM: NewCronAnySpec(),
Month: NewCronAnySpec(),
DoW: NewCronAnySpec(),
}, cron)
assert.Equal(t, tasks.Crones()[0].Name(), cron)
assert.ElementsMatch(t, tasks.Crones()[0].Command(), []string{cron})
}
func TestTasks_List(t *testing.T) {
tasks := NewTasks()
tasks.services = []wminit.ServiceTask{
&ServiceTask{
name: "one",
command: []string{"/path/to/executable"},
},
&ServiceTask{
name: "two",
command: []string{"/path/to/executable"},
},
}
tasks.crones = []wminit.CronTask{
&CronTask{
CronSchedule: CronSchedule{
Minute: NewCronAnySpec(),
Hour: NewCronAnySpec(),
DoM: NewCronAnySpec(),
Month: NewCronAnySpec(),
DoW: NewCronAnySpec(),
},
name: "cron-one",
command: []string{"/path/to/executable"},
},
&CronTask{
CronSchedule: CronSchedule{
Minute: NewCronAnySpec(),
Hour: NewCronAnySpec(),
DoM: NewCronAnySpec(),
Month: NewCronAnySpec(),
DoW: NewCronAnySpec(),
},
name: "cron-two",
command: []string{"/path/to/executable"},
},
}
tl := tasks.List()
tnames := make([]string, 0)
testNames := []string{"one", "two", "cron-one", "cron-two"}
for _, ti := range tl {
tnames = append(tnames, ti.Name())
}
assert.ElementsMatch(t, testNames, tnames)
}
func TestServiceTaskPatchEnv(t *testing.T) {
type testEntry struct {
name string
systemEnv []string
serviceEnv []string
expected []string
}
_ = wingmate.NewLog(os.Stderr, wingmate.Caller|wingmate.Time)
tests := []testEntry{
{
name: "normal",
systemEnv: []string{"PATH=/bin:/usr/bin:/usr/local/bin"},
serviceEnv: []string{
"SPARK_HOME=/opt/spark",
"PATH=$SPARK_HOME/bin:$PATH",
},
expected: []string{
"SPARK_HOME=/opt/spark",
"PATH=/opt/spark/bin:/bin:/usr/bin:/usr/local/bin",
},
},
{
name: "explicit",
systemEnv: []string{"PART=hello "},
serviceEnv: []string{"GREET=${PART}world"},
expected: []string{"PART=hello ", "GREET=hello world"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Run("service", func(t *testing.T) {
st := NewServiceTask(tt.name)
st.SetEnv(tt.serviceEnv...)
result := st.PatchEnv(tt.systemEnv)
assert.ElementsMatch(t, result, tt.expected)
})
t.Run("cron", func(t *testing.T) {
st := NewCronTask(tt.name)
st.SetEnv(tt.serviceEnv...)
result := st.PatchEnv(tt.systemEnv)
assert.ElementsMatch(t, result, tt.expected)
})
})
}
}

View File

@@ -6,6 +6,4 @@ const (
Any CronTimeType = iota Any CronTimeType = iota
Exact Exact
MultipleOccurrence MultipleOccurrence
EnvPrefix = "WINGMATE"
) )

View File

@@ -1,183 +0,0 @@
YAML Configuration
---
Table of content
- [Service](#service)
- [Command](#command)
- [Environ](#environ)
- [User and Group](#user-and-group)
- [Working Directory](#working-directory)
- [setsid](#setsid)
- [PID File](#pid-file)
- [Cron](#cron)
- [Schedule](#schedule)
Example
```yaml
service:
spawner:
command: [ wmspawner ]
user: "1200"
working_dir: "/var/run/test"
bgtest:
command:
- "wmstarter"
- "--no-wait"
- "--"
- "wmexec"
- "--setsid"
- "--"
- "wmbg"
- "--name"
- "test-run"
- "--pause"
- "10"
- "--log-path"
- "/var/log/wmbg.log"
- "--pid-file"
- "/var/run/wmbg.pid"
pidfile: "/var/run/wmbg.pid"
cron:
cron1:
command: ["wmoneshot", "--", "sleep", "5"]
schedule: "*/5 * * * *"
working_dir: "/var/run/cron"
environ:
- "WINGMATE_LOG=/var/log/cron1.log"
- "WINGMATE_LOG_MESSAGE=cron executed in minute 5,10,15,20,25,30,35,40,45,50,55"
cron2:
command: ["wmoneshot", "--", "sleep", "5"]
schedule: "17,42 */2 * * *"
environ:
- "WINGMATE_LOG=/var/log/cron2.log"
- "WINGMATE_LOG_MESSAGE=cron scheduled using 17,42 */2 * * *"
cron3:
command:
- "wmoneshot"
- "--"
- "sleep"
- "5"
schedule: "7,19,23,47 22 * * *"
environ:
- "WINGMATE_LOG=/var/log/cron3.log"
- "WINGMATE_LOG_MESSAGE=cron scheduled using 7,19,23,47 22 * * *"
```
At the top-level, there are two possible entries: Service and Cron.
## Service
`service` is a top-level element that hosts the definition of services to be started by `wingmate`.
Example
```yaml
service:
svc1:
command: [ some_executable ]
user: "1200"
working_dir: "/var/run/test"
```
In the example above, we declare a service called `svc1`. `wingmate` will start a process based on all
elements defined under `svc1`. To learn more about elements for a service, read below.
### Command
`command` element is an array of strings consists of an executable name (optionally with path) and
its arguments (if any). `wingmate` will start the service as its child process by executing
the executable with its arguments.
Example
```yaml
command: [ executable1, argument1, argument2 ]
```
Based on YAML standard, the above example can also be written like
```yaml
command:
- executable1
- argument1
- argument2
```
### Environ
`environ` element is an array of strings. It is a list of environment variables `wingmate` will pass to
the child process or service. The format of each environment variable is a pair of key and value separated
by `=` sign. By default, the child process or service will inherit all environment variables of its parent.
Example
```yaml
environ:
- "S3_BUCKET=YOURS3BUCKET"
- "SECRET_KEY=YOUR_SECRET_KEY"
```
Note: don't worry if an environment variable value has one or more `=` character(s) in it. `wingmate` will
separate key and value using the first `=` character only.
### Working Directory
`working_dir` is a string contains the path where the child process will be running in. By default, the child
process will run in the `wingmate` current directory.
### User and Group
Both `user` and `group` take string value. `user` and `group` refer to the operating system's user and group.
They can be in the form of name, like username or groupname, or in the form of id, like uid or gid.
If they are set, the child process will run as the specified user and group. By default, the child process
will run as the same user and group as the `wingmate` process. The `user` and `group` are only effective
when the `wingmate` running as privileged user, such as `root`. The `user` and `group` configuration depends
on the [wmexec](README.md#wingmate-exec-binary).
### setsid
`setsid` takes a boolean value, `true` or `false`. This feature is operating system dependant. If set to `true`,
the child process will run in a new session. Read `man setsid` on Linux/UNIX. The `setsid` configuration depends
on the [wmexec](README.md#wingmate-exec-binary).
### PID File
This feature is designated to handle service that run in the background. This kind of service usually forks a
new process, terminate the parent process, and continue running in the background child process. It writes its
background process PID in a file. This file is referred as PID file. Put the path of the PID file to this
`pidfile` element. It will help `wingmate` to restart the service if its process exited / terminated. The `pidfile`
configuration depends on the [wmpidproxy](README.md#wingmate-pid-proxy-binary).
## Cron
`cron` is a top-level element that hosts the definition of crones to run by `wingmate` on the specified schedule.
Cron shares almost all configuration elements with Service, except `schedule` and `pidfile`. For the following
elements, please refer to the [Service](#service) section
- [Command](#command)
- [Environ](#environ)
- [Working Directory](#working-directory)
- [setsid](#setsid)
- [User and Group](#user-and-group)
`pidfile` is an invalid config parameter for cron because `wingmate` cannot start cron in background mode. This
limitation is intentionally built into `wingmate` because it doesn't make any sense to run a periodic cron process
in background.
### Schedule
The schedule configuration field uses a format similar to the one described in the [README.md](README.md).
```shell
┌───────────── minute (059)
│ ┌───────────── hour (023)
│ │ ┌───────────── day of the month (131)
│ │ │ ┌───────────── month (112)
│ │ │ │ ┌───────────── day of the week (06) (Sunday to Saturday)
│ │ │ │ │
│ │ │ │ │
│ │ │ │ │
* * * * *
```