Compare commits
No commits in common. "70a4d132c32831cb28308a33795ec17bc5e57ba5" and "574d3deb56efbcac830884337e3f2c9a46863e8f" have entirely different histories.
70a4d132c3
...
574d3deb56
108
README.md
108
README.md
@ -5,108 +5,6 @@ It also has cron feature. It is designed to run in a container/docker.
|
|||||||
The Wingmate binary do not need any external dependency.
|
The Wingmate binary do not need any external dependency.
|
||||||
Just copy the binary, and exec from the entry point script.
|
Just copy the binary, and exec from the entry point script.
|
||||||
|
|
||||||
# Getting Started
|
## Getting Started
|
||||||
|
There are two docker examples [using alpine](docker/alpine/gettting-started.md)
|
||||||
## Binaries
|
and [using debian bookworm](docker/bookworm/gettting-started.md)
|
||||||
|
|
||||||
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 [here](#wingmate-core-binary) for further details about `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 this [directory](../alpine/).
|
|
||||||
|
|
||||||
## 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. The structure
|
|
||||||
inside the config path should look like this.
|
|
||||||
|
|
||||||
```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 (0–59)
|
|
||||||
│ ┌───────────── hour (0–23)
|
|
||||||
│ │ ┌───────────── day of the month (1–31)
|
|
||||||
│ │ │ ┌───────────── month (1–12)
|
|
||||||
│ │ │ │ ┌───────────── day of the week (0–6) (Sunday to Saturday)
|
|
||||||
│ │ │ │ │
|
|
||||||
│ │ │ │ │
|
|
||||||
│ │ │ │ │
|
|
||||||
* * * * * <commad or shell script or binary>
|
|
||||||
```
|
|
||||||
|
|
||||||
The command part only support simple command and arguments. Shell expression is not supported
|
|
||||||
yet. It is recommended to write a shell script and put the path to shell script in
|
|
||||||
the command part.
|
|
||||||
|
|
||||||
# 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>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Wingmate Exec binary
|
|
||||||
## Wingmate core binary
|
|
||||||
### Service
|
|
||||||
### Cron
|
|
||||||
@ -1,8 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"errors"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@ -132,6 +130,8 @@ func readPid(pidFile string) (int, error) {
|
|||||||
var (
|
var (
|
||||||
file *os.File
|
file *os.File
|
||||||
err error
|
err error
|
||||||
|
buf []byte
|
||||||
|
n int
|
||||||
pid64 int64
|
pid64 int64
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -142,15 +142,18 @@ func readPid(pidFile string) (int, error) {
|
|||||||
_ = file.Close()
|
_ = file.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
scanner := bufio.NewScanner(file)
|
buf = make([]byte, 1024)
|
||||||
if scanner.Scan() {
|
n, err = file.Read(buf)
|
||||||
if pid64, err = strconv.ParseInt(scanner.Text(), 10, 64); err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
|
||||||
return int(pid64), nil
|
|
||||||
} else {
|
|
||||||
return 0, errors.New("invalid scanner")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pid64, err = strconv.ParseInt(string(buf[:n]), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return int(pid64), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func startProcess(arg0 string, args ...string) {
|
func startProcess(arg0 string, args ...string) {
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import (
|
|||||||
|
|
||||||
"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 (
|
||||||
@ -49,11 +48,8 @@ func Read() (*Config, error) {
|
|||||||
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())
|
serviceAvailable = true
|
||||||
if err = unix.Access(svcPath, unix.X_OK); err == nil {
|
outConfig.ServicePaths = append(outConfig.ServicePaths, filepath.Join(svcdir, d.Name()))
|
||||||
serviceAvailable = true
|
|
||||||
outConfig.ServicePaths = append(outConfig.ServicePaths, svcPath)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,130 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"gitea.suyono.dev/suyono/wingmate"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestRead(t *testing.T) {
|
|
||||||
|
|
||||||
type testEntry struct {
|
|
||||||
name string
|
|
||||||
testFunc func(t *testing.T)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
configDir string
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
const serviceDir = "service"
|
|
||||||
|
|
||||||
setup := func(t *testing.T) {
|
|
||||||
if configDir, err = os.MkdirTemp("", "wingmate-*-test"); err != nil {
|
|
||||||
t.Fatal("setup", err)
|
|
||||||
}
|
|
||||||
viper.Set(EnvConfigPath, configDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
tear := func(t *testing.T) {
|
|
||||||
if err = os.RemoveAll(configDir); err != nil {
|
|
||||||
t.Fatal("tear", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mkSvcDir := func(t *testing.T) {
|
|
||||||
if err := os.MkdirAll(path.Join(configDir, serviceDir), 0755); err != nil {
|
|
||||||
t.Fatal("create dir", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
touchFile := func(t *testing.T, name string, perm os.FileMode) {
|
|
||||||
f, err := os.OpenFile(name, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, perm)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("create file", err)
|
|
||||||
}
|
|
||||||
_ = f.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = wingmate.NewLog(os.Stderr)
|
|
||||||
tests := []testEntry{
|
|
||||||
{
|
|
||||||
name: "positive",
|
|
||||||
testFunc: func(t *testing.T) {
|
|
||||||
mkSvcDir(t)
|
|
||||||
touchFile(t, path.Join(configDir, serviceDir, "one.sh"), 0755)
|
|
||||||
touchFile(t, path.Join(configDir, serviceDir, "two.sh"), 0755)
|
|
||||||
|
|
||||||
cfg, err := Read()
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.ElementsMatch(
|
|
||||||
t,
|
|
||||||
cfg.ServicePaths,
|
|
||||||
[]string{
|
|
||||||
path.Join(configDir, serviceDir, "one.sh"),
|
|
||||||
path.Join(configDir, serviceDir, "two.sh"),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "with directory",
|
|
||||||
testFunc: func(t *testing.T) {
|
|
||||||
const subdir1 = "subdir1"
|
|
||||||
mkSvcDir(t)
|
|
||||||
assert.Nil(t, os.Mkdir(path.Join(configDir, serviceDir, subdir1), 0755))
|
|
||||||
touchFile(t, path.Join(configDir, serviceDir, subdir1, "one.sh"), 0755)
|
|
||||||
touchFile(t, path.Join(configDir, serviceDir, "two.sh"), 0755)
|
|
||||||
cfg, err := Read()
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.ElementsMatch(
|
|
||||||
t,
|
|
||||||
cfg.ServicePaths,
|
|
||||||
[]string{
|
|
||||||
path.Join(configDir, serviceDir, "two.sh"),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "wrong mode",
|
|
||||||
testFunc: func(t *testing.T) {
|
|
||||||
mkSvcDir(t)
|
|
||||||
touchFile(t, path.Join(configDir, serviceDir, "one.sh"), 0755)
|
|
||||||
touchFile(t, path.Join(configDir, serviceDir, "two.sh"), 0644)
|
|
||||||
|
|
||||||
cfg, err := Read()
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.ElementsMatch(
|
|
||||||
t,
|
|
||||||
cfg.ServicePaths,
|
|
||||||
[]string{
|
|
||||||
path.Join(configDir, serviceDir, "one.sh"),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty",
|
|
||||||
testFunc: func(t *testing.T) {
|
|
||||||
mkSvcDir(t)
|
|
||||||
|
|
||||||
_, err := Read()
|
|
||||||
assert.NotNil(t, err)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
setup(t)
|
|
||||||
tt.testFunc(t)
|
|
||||||
tear(t)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -17,15 +17,9 @@ for further details about `wmpidproxy`.
|
|||||||
It also useful for setting the process as process group leader.
|
It also useful for setting the process as process group leader.
|
||||||
Read [here](#wingmate-exec-binary) for further details about `wmexec`.
|
Read [here](#wingmate-exec-binary) for further details about `wmexec`.
|
||||||
|
|
||||||
## Building a container image based on wingmate image in Docker Hub
|
## Building your 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
|
||||||
# Dockerfile
|
|
||||||
FROM suyono/wingmate:alpine as source
|
FROM suyono/wingmate:alpine as source
|
||||||
|
|
||||||
FROM alpine:latest
|
FROM alpine:latest
|
||||||
@ -37,15 +31,10 @@ COPY --from=source /usr/local/bin/wmexec /usr/local/bin/wmexec
|
|||||||
ENTRYPOINT [ "/usr/local/bin/entry.sh" ]
|
ENTRYPOINT [ "/usr/local/bin/entry.sh" ]
|
||||||
CMD [ "/usr/local/bin/wingmate" ]
|
CMD [ "/usr/local/bin/wingmate" ]
|
||||||
```
|
```
|
||||||
You can find some examples for shell script in this [directory](../alpine/).
|
|
||||||
|
|
||||||
## Configuration
|
## 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. The structure
|
|
||||||
inside the config path should look like this.
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
/etc
|
/etc
|
||||||
└── wingmate
|
└── wingmate
|
||||||
@ -59,32 +48,7 @@ inside the config path should look like this.
|
|||||||
└── spawner.sh
|
└── spawner.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
First, `wingmate` will try to read the content of `service` directory. The content of
|
## Appendix
|
||||||
this directory should be executables (either shell scripts or binaries). The wingmate
|
### Wingmate core binary
|
||||||
will run every executable in `service` directory without going into any subdirectory.
|
### Wingmate PID Proxy binary
|
||||||
|
### Wingmate Exec binary
|
||||||
Next, `wingmate` will read the `crontab` file. `wingmate` expects the `crontab` file using
|
|
||||||
common UNIX crontab file format. Something like this
|
|
||||||
|
|
||||||
```shell
|
|
||||||
┌───────────── minute (0–59)
|
|
||||||
│ ┌───────────── hour (0–23)
|
|
||||||
│ │ ┌───────────── day of the month (1–31)
|
|
||||||
│ │ │ ┌───────────── month (1–12)
|
|
||||||
│ │ │ │ ┌───────────── day of the week (0–6) (Sunday to Saturday)
|
|
||||||
│ │ │ │ │
|
|
||||||
│ │ │ │ │
|
|
||||||
│ │ │ │ │
|
|
||||||
* * * * * <commad or shell script or binary>
|
|
||||||
```
|
|
||||||
|
|
||||||
The command part only support simple command and arguments. Shell expression is not supported
|
|
||||||
yet. It is recommended to write a shell script and put the path to shell script in
|
|
||||||
the command part.
|
|
||||||
|
|
||||||
# Appendix
|
|
||||||
## Wingmate core binary
|
|
||||||
### Service
|
|
||||||
### Cron
|
|
||||||
## Wingmate PID Proxy binary
|
|
||||||
## Wingmate Exec binary
|
|
||||||
@ -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" ]
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
if [ $# -gt 0 ]; then
|
|
||||||
exec "$@"
|
|
||||||
else
|
|
||||||
exec /usr/local/bin/wingmate
|
|
||||||
fi
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
exec /usr/local/bin/wmpidproxy --pid-file /var/run/sshd.pid -- /usr/sbin/sshd
|
|
||||||
1
go.mod
1
go.mod
@ -28,7 +28,6 @@ require (
|
|||||||
github.com/spf13/cast v1.6.0 // indirect
|
github.com/spf13/cast v1.6.0 // indirect
|
||||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/stretchr/objx v0.5.1 // indirect
|
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
go.uber.org/atomic v1.11.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
|
||||||
|
|||||||
3
go.sum
3
go.sum
@ -201,15 +201,12 @@ github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0
|
|||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0=
|
|
||||||
github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0=
|
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
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.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.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.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.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
|
||||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
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=
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user