diff --git a/README.md b/README.md index a02e65c..0feee63 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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 diff --git a/build/yetis b/build/yetis index 3c4d692..1af8f92 100755 Binary files a/build/yetis and b/build/yetis differ diff --git a/client/commands.go b/client/commands.go index a66d453..0f7d646 100644 --- a/client/commands.go +++ b/client/commands.go @@ -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) + } } } } diff --git a/common/version.go b/common/version.go index 372492e..8fd0fcc 100644 --- a/common/version.go +++ b/common/version.go @@ -1,3 +1,3 @@ package common -const YetisVersion = "v0.4.0" +const YetisVersion = "v0.4.1" diff --git a/itests/iptables_test.go b/itests/iptables_test.go index c3343c3..010ff01 100644 --- a/itests/iptables_test.go +++ b/itests/iptables_test.go @@ -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() diff --git a/itests/server_test.go b/itests/server_test.go index 86d2f9d..31a0537 100644 --- a/itests/server_test.go +++ b/itests/server_test.go @@ -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" @@ -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 = ".." diff --git a/main.go b/main.go index 4a7fafc..6b7186e 100644 --- a/main.go +++ b/main.go @@ -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 diff --git a/server/handlers_deployment.go b/server/handlers_deployment.go index fe57d58..69a36f6 100644 --- a/server/handlers_deployment.go +++ b/server/handlers_deployment.go @@ -2,6 +2,7 @@ package server import ( "cmp" + "context" "fmt" "github.com/glossd/fetch" "github.com/glossd/yetis/common" @@ -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) } } @@ -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) { @@ -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) } @@ -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) } @@ -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) } diff --git a/server/server.go b/server/server.go index cff4a84..7d6016b 100644 --- a/server/server.go +++ b/server/server.go @@ -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)) diff --git a/server/store_deployment.go b/server/store_deployment.go index cf73c4f..a50e8f8 100644 --- a/server/store_deployment.go +++ b/server/store_deployment.go @@ -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 {