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
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ Yetis will start in the background. You can pass [yetis configuration](#yetis-se
```shell
yetis apply -f config.yaml
```
[Configuration](#Configuration-examples)
`apply` will also restart the existing processes.
Have a look at the example [configuration](#Configuration-examples)

#### List the managed processes
`yetis list` will show the list of the processes.
Expand All @@ -36,11 +37,11 @@ Add flag `-w` to watch the updates
#### Full list of commands
```
Server Commands:
start [-d] start Yetis server
start [-f FILENAME] start Yetis server
shutdown terminate Yetis server
info print server status
Resources Commands:
apply -f FILENAME apply a configuration from yaml file
apply -f FILENAME apply a configuration from yaml file. Creates new deployments or restarts existing ones
list [-w] print a list the managed deployment
logs [-f] NAME print the logs of the deployment with NAME
describe NAME print a detailed description of the selected deployment
Expand Down
Binary file modified build/yetis
Binary file not shown.
8 changes: 6 additions & 2 deletions client/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,12 +178,16 @@ func Apply(path string) []error {
switch config.Spec.Kind() {
case common.Deployment:
spec := config.Spec.(common.DeploymentSpec)
_, err := fetch.Post[fetch.Empty]("/deployments", spec)
res, err := fetch.Post[server.CRDeploymentResponse]("/deployments", spec)
if err != nil {
errs = append(errs, err)
fmt.Printf("Failure applying %s deployment: %s\n", spec.Name, err)
} else {
fmt.Printf("Successfully applied %s deployment\n", spec.Name)
if res.Existed {
fmt.Printf("Restarted %s deployment successfully\n", spec.Name)
} else {
fmt.Printf("Created %s deployment successfully\n", spec.Name)
}
}
}
}
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.4.0"
const YetisVersion = "v0.4.1"
30 changes: 30 additions & 0 deletions itests/iptables_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,36 @@ func TestDeploymentRestartWithNewYetisPort(t *testing.T) {
}
}

func TestRestartThroughApply_RollingUpdateStrategy(t *testing.T) {
skipIfNotIptables(t)

go server.Run("")
t.Cleanup(server.Stop)
time.Sleep(time.Millisecond)
errs := client.Apply(pwd(t) + "/specs/app-port.yaml")
if len(errs) != 0 {
t.Fatalf("apply errors: %v", errs)
}

firstDep, err := client.GetDeployment("go")
assert(t, err, nil)

checkDeploymentRunning(t, "go")

errs = client.Apply(pwd(t) + "/specs/app-port.yaml")
if len(errs) != 0 {
t.Fatalf("apply errors: %v", errs)
}

checkDeploymentRunning(t, "go-1")

secondDep, err := client.GetDeployment("go-1")
assert(t, err, nil)
if firstDep.Spec.LivenessProbe.Port() == secondDep.Spec.LivenessProbe.Port() {
t.Error("ports supposed to be different")
}
}

func skipIfNotIptables(t *testing.T) {
if os.Getenv("TEST_IPTABLES") == "" {
t.SkipNow()
Expand Down
35 changes: 35 additions & 0 deletions itests/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package itests

import (
"fmt"
"github.com/glossd/fetch"
"github.com/glossd/yetis/client"
_ "github.com/glossd/yetis/client"
"github.com/glossd/yetis/common"
Expand Down Expand Up @@ -80,6 +81,40 @@ func TestLivenessRestart(t *testing.T) {
}
}

func TestRestartThroughApply_RecreateStrategy(t *testing.T) {
go server.Run("")
t.Cleanup(server.Stop)
time.Sleep(time.Millisecond)
errs := client.Apply(pwd(t) + "/specs/nc.yaml")
if len(errs) != 0 {
t.Fatalf("apply errors: %v", errs)
}

oldD, err := client.GetDeployment("hello")
assert(t, err, nil)

checkDeploymentRunning(t, "hello")

res, err := fetch.Post[server.CRDeploymentResponse]("/deployments", common.DeploymentSpec{
Name: "hello",
Cmd: "nc -lk 27000",
Logdir: "stdout",
LivenessProbe: common.Probe{TcpSocket: common.TcpSocket{Port: 27000}},
Env: []common.EnvVar{{Name: "NEW_ENV", Value: "Hello World"}},
}.WithDefaults())
assert(t, err, nil)
assert(t, res.Existed, true)
d, err := client.GetDeployment("hello")
assert(t, err, nil)
if oldD.Pid == d.Pid {
t.Errorf("expected to restart the deployment: %+v", d)
}
assert(t, oldD.Spec.GetEnv("NEW_ENV"), "")
if d.Spec.GetEnv("NEW_ENV") != "Hello World" {
t.Errorf("expected new env to be set from the restart: %+v", d)
}
}

func TestShutdown_DeleteDeployments(t *testing.T) {
cmd := exec.Command("go", "run", "main.go", "run")
cmd.Dir = ".."
Expand Down
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ Server Commands:
shutdown terminate Yetis server
info print server status
Resources Commands:
apply -f FILENAME apply a configuration from yaml file
apply -f FILENAME apply a configuration from yaml file. Creates new deployments or restarts existing ones
list [-w] print a list the managed deployment
logs [-f] NAME print the logs of the deployment with NAME
describe NAME print a detailed description of the selected deployment
Expand Down
86 changes: 56 additions & 30 deletions server/handlers_deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package server

import (
"cmp"
"context"
"fmt"
"github.com/glossd/fetch"
"github.com/glossd/yetis/common"
Expand All @@ -13,47 +14,52 @@ import (
"time"
)

func PostDeployment(req fetch.Request[common.DeploymentSpec]) error {
type CRDeploymentResponse struct {
// True if restarted, false if created
Existed bool
}

func CreateOrRestartDeployment(req fetch.Request[common.DeploymentSpec]) (*CRDeploymentResponse, error) {
spec := req.Body
// Validation
if spec.Strategy.Type == common.Recreate {
if spec.Proxy.Port == 0 && spec.LivenessProbe.Port() == 0 {
return fmt.Errorf("either livenessProbe.tcpSocket.port or proxy.port must be specified for Recreate strategy")
return nil, fmt.Errorf("either livenessProbe.tcpSocket.port or proxy.port must be specified for Recreate strategy")
}
}
if spec.Strategy.Type == common.RollingUpdate {
// check the name was upgraded
var err error
deploymentStore.Range(func(name string, d deployment) bool {
if spec.Name == rootNameForRollingUpdate(name) {
err = fmt.Errorf("deployment '%s' has a rolling update name: %s", spec.Name, name)
return false
}
return true
})
if err != nil {
return err
}
if spec.LivenessProbe.Port() > 0 {
return fmt.Errorf("livenessProxy.tcpSocket.port can't be specified with RollingUpdate strategy")
return nil, fmt.Errorf("livenessProxy.tcpSocket.port can't be specified with RollingUpdate strategy")
}
if spec.Proxy.Port == 0 {
return fmt.Errorf("proxy.port must be specified with RollingUpdate strategy")
return nil, fmt.Errorf("proxy.port must be specified with RollingUpdate strategy")
}
}

if spec.Proxy.Port > 0 && spec.LivenessProbe.Port() > 0 {
return fmt.Errorf("livenessProxy.tcpSocket.port can't be specified with proxy.port")
return nil, fmt.Errorf("livenessProxy.tcpSocket.port can't be specified with proxy.port")
}

// If the deployment already exists, restart it
if d, ok := getDeploymentByRootName(spec.Name); ok {
nameNum := d.spec.Name
err := restartDeployment(req.Context, nameNum, &spec)
if err != nil {
return nil, err
}
return &CRDeploymentResponse{Existed: true}, nil
}

// Begin creating the deployment
spec, err := setYetisPortEnv(spec.WithDefaults().(common.DeploymentSpec))
if err != nil {
return err
return nil, err
}

if spec.Proxy.Port > 0 {
err := proxy.CreatePortForwarding(spec.Proxy.Port, spec.LivenessProbe.Port())
if err != nil {
return fmt.Errorf("failed to create proxy: %s", err)
return nil, fmt.Errorf("failed to create proxy: %s", err)
}
}

Expand All @@ -62,12 +68,12 @@ func PostDeployment(req fetch.Request[common.DeploymentSpec]) error {
if spec.Proxy.Port > 0 {
_ = proxy.DeletePortForwarding(spec.Proxy.Port, spec.LivenessProbe.Port())
}
return err
return nil, err
}

startLivenessCheck(spec)

return nil
return &CRDeploymentResponse{Existed: false}, nil
}

func startDeploymentWithEnv(spec common.DeploymentSpec, upsert, setYetisPort bool) (common.DeploymentSpec, error) {
Expand Down Expand Up @@ -254,19 +260,35 @@ func RestartDeployment(r fetch.Request[fetch.Empty]) error {
if name == "" {
return fmt.Errorf(`name can't be empty`)
}
return restartDeployment(r.Context, name, nil)
}

func restartDeployment(ctx context.Context, name string, reapplySpec *common.DeploymentSpec) error {
oldDeployment, ok := getDeployment(name)
if !ok {
return fmt.Errorf(`deployment '%s' doesn't exist'`, name)
}

if reapplySpec != nil {
if oldDeployment.spec.Strategy.Type != reapplySpec.Strategy.Type {
return fmt.Errorf("couldn't restart deployment '%s': strategy.type must be the same, delete the existing one and apply again", reapplySpec.Name)
}
if oldDeployment.spec.Proxy.Port != reapplySpec.Proxy.Port {
return fmt.Errorf("couldn't restart deployment '%s': proxy.prot must be the same, delete the existing one and apply again", reapplySpec.Name)
}
}

deleteLivenessCheck(name)
var newSpec common.DeploymentSpec
var err error
if oldDeployment.spec.Strategy.Type == common.RollingUpdate {
newSpec = oldDeployment.spec
newSpec.Name = upgradeNameForRollingUpdate(newSpec.Name)
newSpec, err = startDeploymentWithEnv(newSpec, false, true)
// todo reapplySpec for apply restart
applySpec := oldDeployment.spec
if reapplySpec != nil {
applySpec = *reapplySpec
}
applySpec.Name = upgradeNameForRollingUpdate(oldDeployment.spec.Name)
newSpec, err = startDeploymentWithEnv(applySpec, false, true)
if err != nil {
return fmt.Errorf("rastart failed: the new rolling deployment of '%s' failed to start: %s", oldDeployment.spec.Name, err)
}
Expand Down Expand Up @@ -294,7 +316,7 @@ func RestartDeployment(r fetch.Request[fetch.Empty]) error {
}

// point to the new port
err := proxy.UpdatePortForwarding(oldDeployment.spec.Proxy.Port, oldDeployment.spec.LivenessProbe.Port(), newSpec.LivenessProbe.Port())
err := proxy.UpdatePortForwarding(newSpec.Proxy.Port, oldDeployment.spec.LivenessProbe.Port(), newSpec.LivenessProbe.Port())
if err != nil {
return fmt.Errorf("started new deployment but failed to update proxy: %s", err)
}
Expand All @@ -303,23 +325,27 @@ func RestartDeployment(r fetch.Request[fetch.Empty]) error {
time.Sleep(50 * time.Millisecond)

// delete old deployment
err = DeleteDeployment(fetch.Request[fetch.Empty]{Context: r.Context, PathValues: map[string]string{"name": oldDeployment.spec.Name}})
err = DeleteDeployment(fetch.Request[fetch.Empty]{Context: ctx, PathValues: map[string]string{"name": oldDeployment.spec.Name}})
if err != nil {
return fmt.Errorf("failed to delete old deployment '%s': %s", oldDeployment.spec.Name, err)
}

} else {
err := terminateProcess(r.Context, oldDeployment)
err := terminateProcess(ctx, oldDeployment)
if err != nil {
return fmt.Errorf("failed to terminate deployment's process: %s", err)
}
newSpec, err = startDeploymentWithEnv(oldDeployment.spec, true, true)
var applySpec = oldDeployment.spec
if reapplySpec != nil {
applySpec = *reapplySpec
}
newSpec, err = startDeploymentWithEnv(applySpec, true, true)
if err != nil {
return fmt.Errorf("faield to start deployment: %s", err)
}

if oldDeployment.spec.Proxy.Port > 0 {
err := proxy.UpdatePortForwarding(oldDeployment.spec.Proxy.Port, oldDeployment.spec.LivenessProbe.Port(), newSpec.LivenessProbe.Port())
if newSpec.Proxy.Port > 0 {
err := proxy.UpdatePortForwarding(newSpec.Proxy.Port, oldDeployment.spec.LivenessProbe.Port(), newSpec.LivenessProbe.Port())
if err != nil {
return fmt.Errorf("restarted deployment but failed to update proxy port: %s", err)
}
Expand Down
2 changes: 1 addition & 1 deletion server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func Run(configPath string) {

mux.HandleFunc("GET /deployments", fetch.ToHandlerFuncEmptyIn(ListDeployment))
mux.HandleFunc("GET /deployments/{name}", fetch.ToHandlerFunc(GetDeployment))
mux.HandleFunc("POST /deployments", fetch.ToHandlerFuncEmptyOut(PostDeployment))
mux.HandleFunc("POST /deployments", fetch.ToHandlerFunc(CreateOrRestartDeployment))
mux.HandleFunc("DELETE /deployments/{name}", fetch.ToHandlerFuncEmptyOut(DeleteDeployment))
mux.HandleFunc("PUT /deployments/{name}/restart", fetch.ToHandlerFuncEmptyOut(RestartDeployment))

Expand Down
18 changes: 18 additions & 0 deletions server/store_deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,24 @@ func getDeployment(name string) (deployment, bool) {
return deploymentStore.Load(name)
}

func getDeploymentByRootName(rootName string) (deployment, bool) {
var dep deployment
var found bool
dep, found = getDeployment(rootName)
if found {
return dep, found
}
deploymentStore.Range(func(name string, d deployment) bool {
if rootName == rootNameForRollingUpdate(name) {
found = true
dep = d
return false
}
return true
})
return dep, found
}

func getDeploymentStatus(name string) (ProcessStatus, bool) {
dep, ok := deploymentStore.Load(name)
if !ok {
Expand Down