Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ sudo wget -O /usr/local/bin/yetis https://github.com/glossd/yetis/raw/refs/heads
sudo yetis start
```
*Must be `root` to configure `proxy`
Yetis will start in the background. The logs are available at `/tmp/yetis.log`. You can specify your own log directory with `-d` flag.
Yetis will start in the background. You can pass [yetis configuration](#yetis-server-configuration) with `-f` flag.
### Available commands
#### Deploy your process:
```shell
Expand Down Expand Up @@ -114,3 +114,18 @@ Zero downtime is achieved with `RollingUpdate` strategy and `restart` command. Y
then direct traffic to the new instance, and only then will terminate the old instance. The new deployment will have the name with an index i.e. frontend-1, frontend-2 and so on.
If deployment has `Recreate` strategy, Yetis will wait for the termination of the old instance before starting a new one with the same name.
It's the same as in [Kubernetes](https://medium.com/@muppedaanvesh/rolling-update-recreate-deployment-strategies-in-kubernetes-️-327b59f27202)

### Yetis Server Configuration
Configure Yetis server when starting it: `yetis start -f /path/to/config.yml`
```yaml
logdir: /tmp # yetis.log will be stored in there. Defaults to /tmp
alerting: # Alerts when a managed process fails or recovers.
mail: # add SMPT creds of your smpt server for alerting
host: smtp.host.com
port: 587
from: noreply@mail.com
to:
- yourmail@mail.com
username: authUser
password: authPass
```
Binary file modified build/yetis
Binary file not shown.
33 changes: 26 additions & 7 deletions client/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,36 @@ func init() {
fetch.SetBaseURL(baseURL)
}

func StartBackground(logdir string) {
type Settings struct {
Alerting
}

type Alerting interface {
}

type MailAlerting struct {
Host string
From string
Username string
Password string
}

func StartBackground(pathToConfig string) {
if !unix.ExecutableExists("yetis") {
fmt.Println("yetis is not installed")
}

logFilePath := filepath.Join(logdir, "yetis.log")
c := common.YetisConfig{}.WithDefaults()
if pathToConfig != "" {
c = common.ReadServerConfig(pathToConfig)
}

logFilePath := filepath.Join(c.Logdir, "yetis.log")
file, err := os.OpenFile(logFilePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0750)
if err != nil {
fmt.Println("Failed to open log file at", logFilePath, err)
}
cmd := exec.Command("nohup", "yetis", "run")
cmd := exec.Command("nohup", "yetis", "run", "-f", pathToConfig)
cmd.Stdout = file
cmd.Stderr = file
err = cmd.Start()
Expand Down Expand Up @@ -66,7 +85,7 @@ func WatchGetDeployments() {
}

func printDeploymentTable() (int, bool) {
views, err := fetch.Get[[]server.DeploymentView]("/deployments")
views, err := fetch.Get[[]server.DeploymentInfo]("/deployments")
if err != nil {
fmt.Println(err)
return 0, false
Expand Down Expand Up @@ -133,8 +152,8 @@ func DescribeDeployment(name string) {
}
}

func GetDeployment(name string) (server.GetResponse, error) {
return fetch.Get[server.GetResponse]("/deployments/" + name)
func GetDeployment(name string) (server.DeploymentFullInfo, error) {
return fetch.Get[server.DeploymentFullInfo]("/deployments/" + name)
}

func DeleteDeployment(name string) {
Expand Down Expand Up @@ -172,7 +191,7 @@ func Apply(path string) []error {
}

func Logs(name string, stream bool) {
r, err := fetch.Get[server.GetResponse]("/deployments/" + name)
r, err := fetch.Get[server.DeploymentFullInfo]("/deployments/" + name)
if err != nil {
fmt.Println(err)
} else {
Expand Down
2 changes: 1 addition & 1 deletion client/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
)

func TestInfo(t *testing.T) {
go server.Run()
go server.Run("")
t.Cleanup(server.Stop)
time.Sleep(5 * time.Millisecond)
res, err := fetch.Get[server.InfoResponse](baseHost + "/info")
Expand Down
1 change: 1 addition & 0 deletions common/alert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package common
97 changes: 97 additions & 0 deletions common/server_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package common

import (
"fmt"
"io"
"log"
"net/smtp"
"os"
"reflect"
yaml "sigs.k8s.io/yaml/goyaml.v2"
)

type YetisConfig struct {
Logdir string
Alerting Alerting
}

func (yc YetisConfig) WithDefaults() YetisConfig {
if yc.Logdir == "" {
yc.Logdir = "/tmp"
}
return yc
}

type Alerting struct {
Mail Mail
}

func (a Alerting) Send(title, description string) error {
var errStr string
if a.Mail.Validate() == nil {
err := a.Mail.Send(title, description)
if err != nil {
errStr += err.Error() + " + "
}
}
if errStr != "" {
return fmt.Errorf("alert failure: %s", errStr)
}
return nil
}

type Mail struct {
Host string
Port int
From string
To []string
Username string
Password string
}

func (m Mail) Validate() error {
if m.Host == "" {
return fmt.Errorf("mail: host can't be empty")
}
if m.From == "" {
return fmt.Errorf("mail: from field can't be empty")
}
if len(m.To) == 0 {
return fmt.Errorf("mail: to field can't be empty")
}
return nil
}

func (m Mail) Send(title, description string) error {
smtpAuth := smtp.PlainAuth("", m.Username, m.Password, m.Host)
address := fmt.Sprintf("%s:%d", m.Host, m.Port)
msg := fmt.Sprintf("From: %s\nSubject: %s\n\n%s", m.From, title, description)
return smtp.SendMail(address, smtpAuth, m.From, m.To, []byte(msg))
}

func ReadServerConfig(path string) YetisConfig {
f, err := os.Open(path)
if err != nil {
log.Fatalf("Couldn't open server config: %s", err)
}
return readServerConfig(f).WithDefaults()
}

func readServerConfig(f io.Reader) YetisConfig {
str, err := io.ReadAll(f)
if err != nil {
log.Fatalf("Couldn't read server config: %s", err)
}
var c YetisConfig
err = yaml.Unmarshal(str, &c)
if err != nil {
log.Fatalf("Failed to unmarshal config: %s", err)
}
if !reflect.ValueOf(c.Alerting.Mail).IsZero() {
err = c.Alerting.Mail.Validate()
if err != nil {
log.Fatalf("Mail validation failed: %s", err)
}
}
return c
}
36 changes: 36 additions & 0 deletions common/server_config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package common

import (
"bytes"
"testing"
)

func TestYetisConfig(t *testing.T) {
in := `
logdir: /tmp # yetis.log will be stored in there. Defaults to /tmp
alerting:
mail: # add SMPT creds of your smpt server for alerting
host: smtp.host.com
port: 587
from: noreply@mail.com
to:
- yourmail@mail.com
username: authUser
password: authPass
`

res := readServerConfig(bytes.NewBufferString(in))
if res.Logdir != "/tmp" || res.Alerting.Mail.Host != "smtp.host.com" || len(res.Alerting.Mail.To) != 1 {
t.Fatal("Wrong config:", res)
}
assert(t, res.Alerting.Mail.Validate(), nil)
}

func TestSendEmail(t *testing.T) {
t.SkipNow()

c := ReadServerConfig("../tmp/yetis.yml")
assert(t, c.Alerting.Mail.Validate(), nil)
err := c.Alerting.Mail.Send("Hello Test", "This came from TestSendEmail")
assert(t, err, nil)
}
4 changes: 2 additions & 2 deletions common/unix/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
)

func TestIsProcessAlive(t *testing.T) {
cmd := exec.Command("sleep", "0.03")
cmd := exec.Command("sleep", "0.05")
err := cmd.Start()
if err != nil {
t.Fatalf("error launching process: %s", err)
Expand All @@ -26,7 +26,7 @@ func TestIsProcessAlive(t *testing.T) {
t.Fatal("pid shouldn't exist") // probs:)
}

time.Sleep(35 * time.Millisecond)
time.Sleep(55 * time.Millisecond)
if IsProcessAlive(pid) {
t.Fatal("sleep should have terminated")
}
Expand Down
2 changes: 1 addition & 1 deletion common/version.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
package common

const YetisVersion = "v0.3.2"
const YetisVersion = "v0.4.0"
6 changes: 3 additions & 3 deletions itests/iptables_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import (

func TestProxyUpdatesWhenDeploymentRestartsOnLivenessFailure(t *testing.T) {
skipIfNotIptables(t)
go server.Run()
go server.Run("")
t.Cleanup(server.Stop)
// let the server start
time.Sleep(5 * time.Millisecond)
Expand Down Expand Up @@ -89,7 +89,7 @@ func TestProxyUpdatesWhenDeploymentRestartsOnLivenessFailure(t *testing.T) {

func TestRestart_RollingUpdate_ZeroDowntime(t *testing.T) {
skipIfNotIptables(t)
go server.Run()
go server.Run("")
t.Cleanup(server.Stop)
// let the server start
time.Sleep(5 * time.Millisecond)
Expand Down Expand Up @@ -148,7 +148,7 @@ func TestRestart_RollingUpdate_ZeroDowntime(t *testing.T) {

func TestDeploymentRestartWithNewYetisPort(t *testing.T) {
skipIfNotIptables(t)
go server.Run()
go server.Run("")
t.Cleanup(server.Stop)
// let the server start
time.Sleep(5 * time.Millisecond)
Expand Down
6 changes: 3 additions & 3 deletions itests/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import (
// server_test.go:42: before first healthcheck: expected Pending status, got Running, restarts 0
func TestLivenessRestart(t *testing.T) {
unix.KillByPort(server.YetisServerPort, true)
go server.Run()
go server.Run("")
t.Cleanup(server.Stop)
// let the server start
time.Sleep(time.Millisecond)
Expand All @@ -31,7 +31,7 @@ func TestLivenessRestart(t *testing.T) {
t.Fatalf("apply errors: %v", errs)
}

check := func(f func(server.GetResponse)) {
check := func(f func(server.DeploymentFullInfo)) {
dr, err := client.GetDeployment("hello")
if err != nil {
t.Fatal(err)
Expand All @@ -40,7 +40,7 @@ func TestLivenessRestart(t *testing.T) {
}

checkSR := func(description string, s server.ProcessStatus, restarts int) {
check(func(r server.GetResponse) {
check(func(r server.DeploymentFullInfo) {
if r.Status != s.String() {
t.Fatalf("%s: expected %s status, got %s, restarts %d", description, s.String(), r.Status, r.Restarts)
}
Expand Down
45 changes: 31 additions & 14 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,17 @@ func main() {
fmt.Printf("Client: version=%s\n", common.YetisVersion)
case "run":
// starts Yetis server in the foreground
server.Run()
if len(args) == 2 {
server.Run("")
return
}

if len(args) != 4 || args[2] != "-f" {
printFlags("run", Flag{Def: "-f FILENAME", Des: "path to the yetis config"})
return
}

server.Run(args[3])
case "start":
currentUser, err := user.Current()
if err != nil {
Expand All @@ -46,16 +56,16 @@ func main() {
if currentUser.Username != "root" {
log.Println("Warning: not running as root, Yetis won't be to create a proxy")
}
logdir := "/tmp"
if len(args) > 3 {
if args[2] == "-d" {
logdir = args[3]
} else {
printFlags("start", "-d directory for the server log")
return
}
if len(args) == 2 {
client.StartBackground("")
return
}
if len(args) != 4 || args[2] != "-f" {
printFlags("start", Flag{Def: "-f FILENAME", Des: "path to the yetis config"})
return
}
client.StartBackground(logdir)

client.StartBackground(args[3])
case "shutdown":
if len(args) == 2 {
client.ShutdownServer(5 * time.Minute)
Expand Down Expand Up @@ -130,10 +140,17 @@ func main() {
}
}

func printFlags(cmd string, flags ...string) {
type Flag struct {
// Definition e.g. -f FILENAME
Def string
// Description e.g. path to the config
Des string
}

func printFlags(cmd string, flags ...Flag) {
fmt.Printf("The flags for %s command are:\n", cmd)
for _, flag := range flags {
fmt.Println(" " + flag)
for _, f := range flags {
fmt.Println(" " + f.Def + " " + f.Des)
}
}

Expand All @@ -144,7 +161,7 @@ func needName() {
func printHelp() {
fmt.Printf(`The commands are:
Server Commands:
start [-d] start Yetis server
start [-f FILENAME] start Yetis server
shutdown terminate Yetis server
info print server status
Resources Commands:
Expand Down
Loading