Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue 320: support clean shutdown #953

Merged
merged 5 commits into from
May 25, 2020
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
27 changes: 27 additions & 0 deletions cmd/drain.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package cmd

import (
"github.com/runatlantis/atlantis/drain"
"github.com/runatlantis/atlantis/server/logging"
"github.com/spf13/cobra"
)

// DrainCmd performs a drain of the local Atlantis server for all running operations.
// The server itself is not shutdown but drained from all running operations.
// When the command returns, the "atlantis server" process can be stopped securely.
type DrainCmd struct {
Logger *logging.SimpleLogger
}

// Drain returns the runnable cobra command.
func (v *DrainCmd) Init() *cobra.Command {
return &cobra.Command{
Use: "drain",
Short: "Perform a drain of the local Atlantis server, waiting for completion before returning",
RunE: func(cmd *cobra.Command, args []string) error {
err := drain.Start(v.Logger)
return err
},
SilenceErrors: true,
}
}
106 changes: 106 additions & 0 deletions drain/drain.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package drain

import (
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"time"

"github.com/runatlantis/atlantis/server"
"github.com/runatlantis/atlantis/server/logging"
)

// Start begins the shutdown process.
func Start(logger *logging.SimpleLogger) error {
logger.Info("Drain starting")
httpClient := &http.Client{}
resp, err := startDrain(httpClient, logger)
if err != nil {
return err
}
logger.Info("Drain of server initiated succesfully")
for {
if resp.DrainCompleted {
logger.Info("Drain of server completed successfully. You can now send a TERM signal to the server.")
break
}
logger.Info("Drain of server still ongoing, waiting a little bit ...")
time.Sleep(5 * time.Second)
resp, err = getDrainStatus(httpClient, logger)
if err != nil {
return err
}
}
return nil
}

func startDrain(httpClient *http.Client, logger *logging.SimpleLogger) (*server.DrainResponse, error) {

req, err := http.NewRequest("POST", "http://localhost:4141/drain", nil)
if err != nil {
logger.Err("Failed to create POST request to /drain endpoint: %s", err)
return nil, err
}

resp, err := httpClient.Do(req)
if err != nil {
logger.Err("Failed to make POST request to /drain endpoint: %s", err)
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
logger.Err("Failed to read reponse body of POST request to /drain endpoint: %s", err)
return nil, err
}

if resp.StatusCode != http.StatusCreated {
logger.Err("Unexpected status code while making POST request to /drain endpoint: %d", resp.StatusCode)
logger.Info("Response content: %s", string(body))
return nil, errors.New("Unexpected status code")
}

var response server.DrainResponse
err = json.Unmarshal(body, &response)
if err != nil {
logger.Err("Failed to parse reponse body of POST request to /drain endpoint: %s", err)
return nil, err
}
return &response, nil
}

func getDrainStatus(httpClient *http.Client, logger *logging.SimpleLogger) (*server.DrainResponse, error) {

req, err := http.NewRequest("GET", "http://localhost:4141/drain", nil)
if err != nil {
logger.Err("Failed to create GET request to /drain endpoint: %s", err)
return nil, err
}

resp, err := httpClient.Do(req)
if err != nil {
logger.Err("Failed to make GET request to /drain endpoint: %s", err)
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
logger.Err("Failed to read reponse body of GET request to /drain endpoint: %s", err)
return nil, err
}

if resp.StatusCode != http.StatusOK {
logger.Err("Unexpected status code while making GET request to /drain endpoint: %d", resp.StatusCode)
logger.Info("Response content: %s", string(body))
return nil, errors.New("Unexpected status code")
}

var response server.DrainResponse
err = json.Unmarshal(body, &response)
if err != nil {
logger.Err("Failed to parse reponse body of GET request to /drain endpoint: %s", err)
return nil, err
}
return &response, nil
}
4 changes: 4 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,12 @@ func main() {
}
version := &cmd.VersionCmd{AtlantisVersion: atlantisVersion}
testdrive := &cmd.TestdriveCmd{}
drainCmd := &cmd.DrainCmd{
Logger: logging.NewSimpleLogger("cmd", false, logging.Info),
}
cmd.RootCmd.AddCommand(server.Init())
cmd.RootCmd.AddCommand(version.Init())
cmd.RootCmd.AddCommand(testdrive.Init())
cmd.RootCmd.AddCommand(drainCmd.Init())
cmd.Execute()
}
51 changes: 51 additions & 0 deletions server/drain_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package server

import (
"encoding/json"
"fmt"
"net/http"

"github.com/runatlantis/atlantis/server/events"
"github.com/runatlantis/atlantis/server/logging"
)

// DrainController handles all requests relating to Atlantis drainage (to shutdown properly).
type DrainController struct {
Logger *logging.SimpleLogger
Drainer events.Drainer
}

type DrainResponse struct {
DrainStarted bool `json:"started"`
DrainCompleted bool `json:"completed"`
OngoingOperationsCounter int `json:"ongoingOperations"`
}

// Get is the GET /drain route. It renders the current drainage status.
func (d *DrainController) Get(w http.ResponseWriter, r *http.Request) {
d.respondStatus(http.StatusOK, w)
}

// Post is the POST /drain route. It asks atlantis to finish all ongoing operations and to refuse to start new ones.
func (d *DrainController) Post(w http.ResponseWriter, r *http.Request) {
d.Drainer.StartDrain()
d.respondStatus(http.StatusCreated, w)
}

func (d *DrainController) respondStatus(responseCode int, w http.ResponseWriter) {
status := d.Drainer.GetStatus()
data, err := json.MarshalIndent(&DrainResponse{
DrainStarted: status.DrainStarted,
DrainCompleted: status.DrainCompleted,
OngoingOperationsCounter: status.OngoingOperationsCounter,
}, "", " ")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "Error creating status json response: %s", err)
return
}
d.Logger.Log(logging.Info, "Drain status: %s", string(data))
w.WriteHeader(responseCode)
w.Header().Set("Content-Type", "application/json")
w.Write(data) // nolint: errcheck
}
Loading