mirror of https://github.com/hak5/overseer.git
rename to overseer, allow manual restarts with USR2, fetcher now optional, added fetcher init method
parent
887f12bfaf
commit
6df2e197eb
78
README.md
78
README.md
|
@ -1,15 +1,22 @@
|
|||
# go-upgrade
|
||||
# overseer
|
||||
|
||||
[![GoDoc](https://godoc.org/github.com/jpillora/go-upgrade?status.svg)](https://godoc.org/github.com/jpillora/go-upgrade)
|
||||
[![GoDoc](https://godoc.org/github.com/jpillora/overseer?status.svg)](https://godoc.org/github.com/jpillora/overseer)
|
||||
|
||||
Daemonizable self-upgrading binaries in Go (golang).
|
||||
|
||||
The main goal of this project is to facilitate the creation of self-upgrading binaries which play nice with standard process managers. The secondary goal is user simplicity. :warning: This is beta software.
|
||||
|
||||
### Features
|
||||
|
||||
* Works with process managers
|
||||
* Simple
|
||||
* Graceful, zero-down time restarts
|
||||
* Allows self-upgrading binaries
|
||||
|
||||
### Install
|
||||
|
||||
```
|
||||
go get github.com/jpillora/go-upgrade
|
||||
go get github.com/jpillora/overseer
|
||||
```
|
||||
|
||||
### Quick Usage
|
||||
|
@ -23,33 +30,32 @@ import (
|
|||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/jpillora/go-upgrade"
|
||||
"github.com/jpillora/go-upgrade/fetcher"
|
||||
"github.com/jpillora/overseer"
|
||||
"github.com/jpillora/overseer/fetcher"
|
||||
)
|
||||
|
||||
//convert your 'main()' into a 'prog(state)'
|
||||
//'prog()' is run in a child process
|
||||
func prog(state upgrade.State) {
|
||||
log.Printf("app (%s) listening...", state.ID)
|
||||
http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "app (%s) says hello\n", state.ID)
|
||||
}))
|
||||
http.Serve(state.Listener, nil)
|
||||
}
|
||||
|
||||
//then create another 'main()' which runs the upgrades
|
||||
//'main()' is run in the initial process
|
||||
//convert your main() into a 'prog(state)' and then
|
||||
//create another main() to run the main process
|
||||
func main() {
|
||||
upgrade.Run(upgrade.Config{
|
||||
overseer.Run(overseer.Config{
|
||||
Program: prog,
|
||||
Address: ":3000",
|
||||
Fetcher: &fetcher.HTTP{
|
||||
URL: "http://localhost:4000/binaries/myapp",
|
||||
Interval: 1 * time.Second,
|
||||
},
|
||||
// Log: false, //display log of go-upgrade actions
|
||||
// Log: false, //display log of overseer actions
|
||||
})
|
||||
}
|
||||
|
||||
//prog(state) runs in a child process
|
||||
func prog(state overseer.State) {
|
||||
log.Printf("app (%s) listening...", state.ID)
|
||||
http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "app (%s) says hello\n", state.ID)
|
||||
}))
|
||||
http.Serve(state.Listener, nil)
|
||||
}
|
||||
```
|
||||
|
||||
```sh
|
||||
|
@ -84,36 +90,32 @@ app#3 (286848c2aefcd3f7321a65b5e4efae987fb17911) exiting...
|
|||
|
||||
### Documentation
|
||||
|
||||
* [Core `upgrade` package](https://godoc.org/github.com/jpillora/go-upgrade)
|
||||
* [Common `fetcher.Interface`](https://godoc.org/github.com/jpillora/go-upgrade/fetcher#Interface)
|
||||
* [Basic `fetcher.HTTP` fetcher type](https://godoc.org/github.com/jpillora/go-upgrade/fetcher#HTTP)
|
||||
* [Core `upgrade` package](https://godoc.org/github.com/jpillora/overseer)
|
||||
* [Common `fetcher.Interface`](https://godoc.org/github.com/jpillora/overseer/fetcher#Interface)
|
||||
* [Basic `fetcher.HTTP` fetcher type](https://godoc.org/github.com/jpillora/overseer/fetcher#HTTP)
|
||||
|
||||
### Architecture overview
|
||||
|
||||
* `go-upgrade` uses the main process to check for and install upgrades and a child process to run `Program`
|
||||
* `overseer` uses the main process to check for and install upgrades and a child process to run `Program`
|
||||
* All child process pipes are connected back to the main process
|
||||
* All signals received on the main process are forwarded through to the child process
|
||||
* The provided `fetcher.Interface` will be used to `Fetch()` the latest build of the binary
|
||||
* The `fetcher.HTTP` accepts a `URL`, it polls this URL with HEAD requests and until it detects a change. On change, we `GET` the `URL` and stream it back out to `go-upgrade`.
|
||||
* Once a binary is received, it is run with a simple echo token to confirm it is a `go-upgrade` binary.
|
||||
* Except for scheduled upgrades, the child process exiting will cause the main process to exit with the same code. So, **`go-upgrade` is not a process manager**.
|
||||
* The `fetcher.HTTP` accepts a `URL`, it polls this URL with HEAD requests and until it detects a change. On change, we `GET` the `URL` and stream it back out to `overseer`.
|
||||
* Once a binary is received, it is run with a simple echo token to confirm it is a `overseer` binary.
|
||||
* Except for scheduled upgrades, the child process exiting will cause the main process to exit with the same code. So, **`overseer` is not a process manager**.
|
||||
|
||||
### Docker
|
||||
|
||||
* Create the simple image:
|
||||
* Compile your `overseer`able `app` to a `/path/on/docker/host/myapp/app`
|
||||
* Then run it with
|
||||
|
||||
```Dockerfile
|
||||
FROM alpine:latest
|
||||
RUN mkdir /apphome
|
||||
VOLUME /apphome
|
||||
CMD ["/apphome/app"]
|
||||
```
|
||||
#run app inside alpine linux (5MB linux distro)
|
||||
docker run -d -v /path/on/docker/host/myapp/:/home/ -w /home/ alpine -w /home/app
|
||||
```
|
||||
|
||||
* Mount `-v` your binary directory to into the container as `/apphome`
|
||||
* Compile your application into binary directory`/app`
|
||||
* The application will update itself, which will be kept on disk incase of crashes etc
|
||||
|
||||
Alternatively to mounting, `ADD app /apphome/app` and let it fetch the latest version each time it runs.
|
||||
* For testing, swap out `-d` (daemonize) for `--rm -it` (remove on exit, input, terminal)
|
||||
* `app` can use the current working directory as storage
|
||||
|
||||
### Alternatives
|
||||
|
||||
|
@ -125,7 +127,7 @@ Alternatively to mounting, `ADD app /apphome/app` and let it fetch the latest ve
|
|||
* Github fetcher (given a repo)
|
||||
* S3 fetcher (given a bucket and credentials)
|
||||
* etcd fetcher (given a cluster, watch key)
|
||||
* `go-upgrade` CLI tool ([TODO](cmd/go-upgrade/TODO.md))
|
||||
* `overseer` CLI tool ([TODO](cmd/overseer/TODO.md))
|
||||
* `upgrade` package
|
||||
* Execute and verify calculated delta updates with https://github.com/kr/binarydist
|
||||
* [Omaha](https://coreos.com/docs/coreupdate/custom-apps/coreupdate-protocol/) client support
|
||||
|
|
|
@ -4,8 +4,8 @@ import (
|
|||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/jpillora/go-upgrade"
|
||||
"github.com/jpillora/go-upgrade/fetcher"
|
||||
"github.com/jpillora/overseer"
|
||||
"github.com/jpillora/overseer/fetcher"
|
||||
"github.com/jpillora/opts"
|
||||
)
|
||||
|
||||
|
@ -19,9 +19,9 @@ func main() {
|
|||
Log: true,
|
||||
}
|
||||
opts.Parse(&c)
|
||||
upgrade.Run(upgrade.Config{
|
||||
overseer.Run(overseer.Config{
|
||||
Log: c.Log,
|
||||
Program: func(state upgrade.State) {
|
||||
Program: func(state overseer.State) {
|
||||
//noop
|
||||
select {}
|
||||
},
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
placeholder for the goupgrade binary tool
|
||||
### Placeholder for the `overseer` binary tool
|
||||
|
||||
* Calculate delta updates with https://github.com/kr/binarydist ([courgette](http://dev.chromium.org/developers/design-documents/software-updates-courgette) would be nice)
|
||||
* Signed binaries and updates *(use HTTPS where in the meantime)*
|
||||
* Create signing ECDSA private and private key, store locally
|
||||
* Build binaries and include public key with `-ldflags "-X github.com/jpillora/go-upgrade/fetcher.PublicKey=A" -o myapp`
|
||||
* Build binaries and include public key with `-ldflags "-X github.com/jpillora/overseer/fetcher.PublicKey=A" -o myapp`
|
||||
* Only accept future updates with binaries signed by the matching private key
|
|
@ -1,11 +1,11 @@
|
|||
#!/bin/bash
|
||||
|
||||
#NOTE: DONT CTRL+C OR CLEANUP WONT OCCUR
|
||||
# ENSURE PORTS 3000,4000 ARE UNUSED
|
||||
# ENSURE PORTS 5001,5002 ARE UNUSED
|
||||
|
||||
#http file server
|
||||
go get github.com/jpillora/serve
|
||||
serve --port 4000 --quiet . &
|
||||
serve --port 5002 --quiet . &
|
||||
SERVEPID=$!
|
||||
|
||||
#initial build
|
||||
|
@ -17,33 +17,33 @@ echo "RUNNING APP"
|
|||
APPPID=$!
|
||||
|
||||
sleep 1
|
||||
curl localhost:3000
|
||||
curl localhost:5001
|
||||
sleep 1
|
||||
curl localhost:3000
|
||||
curl localhost:5001
|
||||
sleep 1
|
||||
#request during an update
|
||||
curl localhost:3000?d=5s &
|
||||
curl localhost:5001?d=5s &
|
||||
|
||||
go build -ldflags '-X main.BUILD_ID=2' -o myappnew
|
||||
echo "BUILT APP (2)"
|
||||
|
||||
sleep 2
|
||||
curl localhost:3000
|
||||
curl localhost:5001
|
||||
sleep 1
|
||||
curl localhost:3000
|
||||
curl localhost:5001
|
||||
sleep 1
|
||||
#request during an update
|
||||
curl localhost:3000?d=5s &
|
||||
curl localhost:5001?d=5s &
|
||||
|
||||
go build -ldflags '-X main.BUILD_ID=3' -o myappnew
|
||||
echo "BUILT APP (3)"
|
||||
|
||||
sleep 2
|
||||
curl localhost:3000
|
||||
curl localhost:5001
|
||||
sleep 1
|
||||
curl localhost:3000
|
||||
curl localhost:5001
|
||||
sleep 1
|
||||
curl localhost:3000
|
||||
curl localhost:5001
|
||||
|
||||
#end demo - cleanup
|
||||
kill $SERVEPID
|
||||
|
|
|
@ -5,8 +5,7 @@ import (
|
|||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/jpillora/go-upgrade"
|
||||
"github.com/jpillora/go-upgrade/fetcher"
|
||||
"github.com/jpillora/overseer"
|
||||
)
|
||||
|
||||
//see example.sh for the use-case
|
||||
|
@ -15,7 +14,7 @@ var BUILD_ID = "0"
|
|||
|
||||
//convert your 'main()' into a 'prog(state)'
|
||||
//'prog()' is run in a child process
|
||||
func prog(state upgrade.State) {
|
||||
func prog(state overseer.State) {
|
||||
fmt.Printf("app#%s (%s) listening...\n", BUILD_ID, state.ID)
|
||||
http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
d, _ := time.ParseDuration(r.URL.Query().Get("d"))
|
||||
|
@ -29,13 +28,13 @@ func prog(state upgrade.State) {
|
|||
//then create another 'main' which runs the upgrades
|
||||
//'main()' is run in the initial process
|
||||
func main() {
|
||||
upgrade.Run(upgrade.Config{
|
||||
Log: false, //display log of go-upgrade actions
|
||||
overseer.Run(overseer.Config{
|
||||
Log: true, //display log of overseer actions
|
||||
Program: prog,
|
||||
Address: ":3000",
|
||||
Fetcher: &fetcher.HTTP{
|
||||
URL: "http://localhost:4000/myappnew",
|
||||
Interval: 1 * time.Second,
|
||||
},
|
||||
Address: ":5001",
|
||||
// Fetcher: &fetcher.HTTP{
|
||||
// URL: "http://localhost:5002/myappnew",
|
||||
// Interval: 1 * time.Second,
|
||||
// },
|
||||
})
|
||||
}
|
||||
|
|
|
@ -3,6 +3,11 @@ package fetcher
|
|||
import "io"
|
||||
|
||||
type Interface interface {
|
||||
//Init should perform validation on fields. For
|
||||
//example, ensure the appropriate URLs or keys
|
||||
//are defined or ensure there is connectivity
|
||||
//to the appropriate web service.
|
||||
Init() error
|
||||
//Fetch should check if there is an updated
|
||||
//binary to fetch, and then stream it back the
|
||||
//form of an io.Reader. If io.Reader is nil,
|
||||
|
@ -21,6 +26,10 @@ type fetcher struct {
|
|||
fn func() (io.Reader, error)
|
||||
}
|
||||
|
||||
func (f fetcher) Init() error {
|
||||
return nil //skip
|
||||
}
|
||||
|
||||
func (f fetcher) Fetch() (io.Reader, error) {
|
||||
return f.fn()
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
|
||||
//HTTPFetcher uses HEAD requests to poll the status of a given
|
||||
//file. If it detects this file has been updated, it will fetch
|
||||
//and stream out to the binary writer.
|
||||
//and return its io.Reader stream.
|
||||
type HTTP struct {
|
||||
//URL to poll for new binaries
|
||||
URL string
|
||||
|
@ -23,20 +23,22 @@ type HTTP struct {
|
|||
//if any of these change, the binary has been updated
|
||||
var defaultHTTPCheckHeaders = []string{"ETag", "If-Modified-Since", "Last-Modified", "Content-Length"}
|
||||
|
||||
func (h *HTTP) Fetch() (io.Reader, error) {
|
||||
func (h *HTTP) Init() error {
|
||||
//apply defaults
|
||||
if h.URL == "" {
|
||||
return nil, fmt.Errorf("fetcher.HTTP requires a URL")
|
||||
return fmt.Errorf("URL required")
|
||||
}
|
||||
h.lasts = map[string]string{}
|
||||
if h.Interval == 0 {
|
||||
h.Interval = 5 * time.Minute
|
||||
}
|
||||
if h.CheckHeaders == nil {
|
||||
h.CheckHeaders = defaultHTTPCheckHeaders
|
||||
}
|
||||
if h.lasts == nil {
|
||||
h.lasts = map[string]string{}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *HTTP) Fetch() (io.Reader, error) {
|
||||
//delay fetches after first
|
||||
if h.delay {
|
||||
time.Sleep(h.Interval)
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
package fetcher
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
)
|
||||
|
||||
//S3 uses authenticated HEAD requests to poll the status of a given
|
||||
//object. If it detects this file has been updated, it will perform
|
||||
//an object GET and return its io.Reader stream.
|
||||
type S3 struct {
|
||||
Access string
|
||||
Secret string
|
||||
Region string
|
||||
Bucket string
|
||||
Key string
|
||||
//interal state
|
||||
Interval time.Duration
|
||||
client *s3.S3
|
||||
delay bool
|
||||
lastETag string
|
||||
}
|
||||
|
||||
func (s *S3) Init() error {
|
||||
if s.Bucket == "" {
|
||||
return errors.New("S3 bucket not set")
|
||||
} else if s.Key == "" {
|
||||
return errors.New("S3 key not set")
|
||||
}
|
||||
if s.Access == "" {
|
||||
s.client = s3.New(session.New())
|
||||
} else {
|
||||
if s.Region == "" {
|
||||
s.Region = "ap-southeast-2"
|
||||
}
|
||||
s.client = s3.New(session.New(&aws.Config{
|
||||
Credentials: credentials.NewStaticCredentials(s.Access, s.Secret, ""),
|
||||
Region: &s.Region,
|
||||
}))
|
||||
}
|
||||
//TODO include this? maybe given access to bucket after init
|
||||
// resp, err := s.client.HeadBucketRequest(&s3.HeadBucketInput{Bucket: &s.Bucket})
|
||||
// if err != nil {}
|
||||
|
||||
//apply defaults
|
||||
if s.Interval == 0 {
|
||||
s.Interval = 5 * time.Minute
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *S3) Fetch() (io.Reader, error) {
|
||||
//delay fetches after first
|
||||
if s.delay {
|
||||
time.Sleep(s.Interval)
|
||||
}
|
||||
s.delay = true
|
||||
//status check using HEAD
|
||||
head, err := s.client.HeadObject(&s3.HeadObjectInput{Bucket: &s.Bucket, Key: &s.Key})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("HEAD request failed (%s)", err)
|
||||
}
|
||||
if s.lastETag == *head.ETag {
|
||||
return nil, nil //skip, file match
|
||||
}
|
||||
s.lastETag = *head.ETag
|
||||
//binary fetch using GET
|
||||
get, err := s.client.GetObject(&s3.GetObjectInput{Bucket: &s.Bucket, Key: &s.Key})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GET request failed (%s)", err)
|
||||
}
|
||||
//success!
|
||||
return get.Body, nil
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package upgrade
|
||||
package overseer
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Daemonizable self-upgrading binaries in Go (golang).
|
||||
package upgrade
|
||||
package overseer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -8,7 +8,7 @@ import (
|
|||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/jpillora/go-upgrade/fetcher"
|
||||
"github.com/jpillora/overseer/fetcher"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -19,7 +19,7 @@ const (
|
|||
)
|
||||
|
||||
type Config struct {
|
||||
//Optional allows go-upgrade to fallback to running
|
||||
//Optional allows overseer to fallback to running
|
||||
//running the program in the main process.
|
||||
Optional bool
|
||||
//Program's main function
|
||||
|
@ -28,12 +28,11 @@ type Config struct {
|
|||
Address string
|
||||
//Program's zero-downtime socket listening addresses (set this or Address)
|
||||
Addresses []string
|
||||
//Signal program will accept to initiate graceful
|
||||
//application termination. Defaults to SIGTERM.
|
||||
Signal os.Signal
|
||||
//TerminateTimeout controls how long go-upgrade should
|
||||
//RestartSignal will manually trigger a graceful restart. Defaults to SIGUSR2.
|
||||
RestartSignal os.Signal
|
||||
//TerminateTimeout controls how long overseer should
|
||||
//wait for the program to terminate itself. After this
|
||||
//timeout, go-upgrade will issue a SIGKILL.
|
||||
//timeout, overseer will issue a SIGKILL.
|
||||
TerminateTimeout time.Duration
|
||||
//MinFetchInterval defines the smallest duration between Fetch()s.
|
||||
//This helps to prevent unwieldy fetch.Interfaces from hogging
|
||||
|
@ -42,16 +41,16 @@ type Config struct {
|
|||
//PreUpgrade runs after a binary has been retreived, user defined checks
|
||||
//can be run here and returning an error will cancel the upgrade.
|
||||
PreUpgrade func(tempBinaryPath string) error
|
||||
//NoRestart disables automatic restarts after each upgrade.
|
||||
NoRestart bool
|
||||
//Log enables [go-upgrade] logs to be sent to stdout.
|
||||
//Log enables [overseer] logs to be sent to stdout.
|
||||
Log bool
|
||||
//NoRestartAfterFetch disables automatic restarts after each upgrade.
|
||||
NoRestartAfterFetch bool
|
||||
//Fetcher will be used to fetch binaries.
|
||||
Fetcher fetcher.Interface
|
||||
}
|
||||
|
||||
func fatalf(f string, args ...interface{}) {
|
||||
log.Fatalf("[go-upgrade] "+f, args...)
|
||||
log.Fatalf("[overseer] "+f, args...)
|
||||
}
|
||||
|
||||
func Run(c Config) {
|
||||
|
@ -62,16 +61,18 @@ func Run(c Config) {
|
|||
}
|
||||
//validate
|
||||
if c.Program == nil {
|
||||
fatalf("upgrade.Config.Program required")
|
||||
fatalf("overseer.Config.Program required")
|
||||
}
|
||||
if c.Address != "" {
|
||||
if len(c.Addresses) > 0 {
|
||||
fatalf("upgrade.Config.Address and Addresses cant both be set")
|
||||
fatalf("overseer.Config.Address and Addresses cant both be set")
|
||||
}
|
||||
c.Addresses = []string{c.Address}
|
||||
} else if len(c.Addresses) > 0 {
|
||||
c.Address = c.Addresses[0]
|
||||
}
|
||||
if c.Signal == nil {
|
||||
c.Signal = SIGTERM
|
||||
if c.RestartSignal == nil {
|
||||
c.RestartSignal = SIGUSR2
|
||||
}
|
||||
if c.TerminateTimeout == 0 {
|
||||
c.TerminateTimeout = 30 * time.Second
|
||||
|
@ -79,9 +80,6 @@ func Run(c Config) {
|
|||
if c.MinFetchInterval == 0 {
|
||||
c.MinFetchInterval = 1 * time.Second
|
||||
}
|
||||
if c.Fetcher == nil {
|
||||
fatalf("upgrade.Config.Fetcher required")
|
||||
}
|
||||
//os not supported
|
||||
if !supported {
|
||||
if !c.Optional {
|
|
@ -1,4 +1,4 @@
|
|||
package upgrade
|
||||
package overseer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
@ -21,9 +21,9 @@ import (
|
|||
"github.com/kardianos/osext"
|
||||
)
|
||||
|
||||
var tmpBinPath = filepath.Join(os.TempDir(), "goupgrade")
|
||||
var tmpBinPath = filepath.Join(os.TempDir(), "overseer")
|
||||
|
||||
//a go-upgrade master process
|
||||
//a overseer master process
|
||||
type master struct {
|
||||
Config
|
||||
slaveCmd *exec.Cmd
|
||||
|
@ -38,7 +38,6 @@ type master struct {
|
|||
awaitingUSR1 bool
|
||||
descriptorsReleased chan bool
|
||||
signalledAt time.Time
|
||||
signals chan os.Signal
|
||||
}
|
||||
|
||||
func (mp *master) run() {
|
||||
|
@ -47,14 +46,23 @@ func (mp *master) run() {
|
|||
fatalf("%s", err)
|
||||
}
|
||||
//run program directly
|
||||
mp.logf("%s, disabling go-upgrade.", err)
|
||||
mp.logf("%s, disabling overseer.", err)
|
||||
mp.Program(DisabledState)
|
||||
return
|
||||
}
|
||||
if mp.Config.Fetcher != nil {
|
||||
if err := mp.Config.Fetcher.Init(); err != nil {
|
||||
mp.logf("fetcher init failed (%s)", err)
|
||||
mp.Config.Fetcher = nil
|
||||
}
|
||||
}
|
||||
mp.setupSignalling()
|
||||
mp.retreiveFileDescriptors()
|
||||
if mp.Config.Fetcher != nil {
|
||||
//TODO is required? fatalf("overseer.Config.Fetcher required")
|
||||
mp.fetch()
|
||||
go mp.fetchLoop()
|
||||
}
|
||||
mp.forkLoop()
|
||||
}
|
||||
|
||||
|
@ -98,23 +106,27 @@ func (mp *master) setupSignalling() {
|
|||
mp.restarted = make(chan bool)
|
||||
mp.descriptorsReleased = make(chan bool)
|
||||
//read all master process signals
|
||||
mp.signals = make(chan os.Signal)
|
||||
signal.Notify(mp.signals)
|
||||
signals := make(chan os.Signal)
|
||||
signal.Notify(signals)
|
||||
go func() {
|
||||
for s := range mp.signals {
|
||||
for s := range signals {
|
||||
mp.handleSignal(s)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (mp *master) handleSignal(s os.Signal) {
|
||||
if s.String() == "child exited" {
|
||||
if s == mp.RestartSignal {
|
||||
//user initiated manual restart
|
||||
mp.triggerRestart()
|
||||
} else if s.String() == "child exited" {
|
||||
// will occur on every restart
|
||||
} else
|
||||
//**during a restart** a SIGUSR1 signals
|
||||
//to the master process that, the file
|
||||
//descriptors have been released
|
||||
if mp.awaitingUSR1 && s == SIGUSR1 {
|
||||
mp.logf("signaled, sockets ready")
|
||||
mp.awaitingUSR1 = false
|
||||
mp.descriptorsReleased <- true
|
||||
} else
|
||||
|
@ -122,10 +134,7 @@ func (mp *master) handleSignal(s os.Signal) {
|
|||
//all signals through
|
||||
if mp.slaveCmd != nil && mp.slaveCmd.Process != nil {
|
||||
mp.logf("proxy signal (%s)", s)
|
||||
if err := mp.slaveCmd.Process.Signal(s); err != nil {
|
||||
mp.logf("proxy signal failed (%s)", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
mp.sendSignal(s)
|
||||
} else
|
||||
//otherwise if not running, kill on CTRL+c
|
||||
if s == os.Interrupt {
|
||||
|
@ -136,6 +145,13 @@ func (mp *master) handleSignal(s os.Signal) {
|
|||
}
|
||||
}
|
||||
|
||||
func (mp *master) sendSignal(s os.Signal) {
|
||||
if err := mp.slaveCmd.Process.Signal(s); err != nil {
|
||||
mp.logf("signal failed (%s), assuming slave process died", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func (mp *master) retreiveFileDescriptors() {
|
||||
mp.slaveExtraFiles = make([]*os.File, len(mp.Config.Addresses))
|
||||
for i, addr := range mp.Config.Addresses {
|
||||
|
@ -229,7 +245,7 @@ func (mp *master) fetch() {
|
|||
return
|
||||
}
|
||||
}
|
||||
//go-upgrade sanity check, dont replace our good binary with a text file
|
||||
//overseer sanity check, dont replace our good binary with a text file
|
||||
buff := make([]byte, 8)
|
||||
rand.Read(buff)
|
||||
tokenIn := hex.EncodeToString(buff)
|
||||
|
@ -252,25 +268,37 @@ func (mp *master) fetch() {
|
|||
mp.logf("upgraded binary (%x -> %x)", mp.binHash[:12], newHash[:12])
|
||||
mp.binHash = newHash
|
||||
//binary successfully replaced
|
||||
if !mp.Config.NoRestart && mp.slaveCmd != nil {
|
||||
//if running, perform graceful restart
|
||||
mp.restarting = true
|
||||
mp.awaitingUSR1 = true
|
||||
mp.signalledAt = time.Now()
|
||||
mp.signals <- mp.Config.Signal //ask nicely to terminate
|
||||
select {
|
||||
case <-mp.restarted:
|
||||
//success
|
||||
case <-time.After(mp.TerminateTimeout):
|
||||
//times up process, we did ask nicely!
|
||||
mp.logf("graceful timeout, forcing exit")
|
||||
mp.signals <- os.Kill
|
||||
}
|
||||
if !mp.Config.NoRestartAfterFetch {
|
||||
mp.triggerRestart()
|
||||
}
|
||||
//and keep fetching...
|
||||
return
|
||||
}
|
||||
|
||||
func (mp *master) triggerRestart() {
|
||||
if mp.restarting {
|
||||
mp.logf("already graceful restarting")
|
||||
return //skip
|
||||
} else if mp.slaveCmd == nil || mp.restarting {
|
||||
mp.logf("no slave process")
|
||||
return //skip
|
||||
}
|
||||
mp.logf("graceful restart triggered")
|
||||
mp.restarting = true
|
||||
mp.awaitingUSR1 = true
|
||||
mp.signalledAt = time.Now()
|
||||
mp.sendSignal(mp.Config.RestartSignal) //ask nicely to terminate
|
||||
select {
|
||||
case <-mp.restarted:
|
||||
//success
|
||||
mp.logf("restart success")
|
||||
case <-time.After(mp.TerminateTimeout):
|
||||
//times up process, we did ask nicely!
|
||||
mp.logf("graceful timeout, forcing exit")
|
||||
mp.sendSignal(os.Kill)
|
||||
}
|
||||
}
|
||||
|
||||
//not a real fork
|
||||
func (mp *master) forkLoop() {
|
||||
//loop, restart command
|
||||
|
@ -341,6 +369,6 @@ func (mp *master) fork() {
|
|||
|
||||
func (mp *master) logf(f string, args ...interface{}) {
|
||||
if mp.Log {
|
||||
log.Printf("[go-upgrade master] "+f, args...)
|
||||
log.Printf("[overseer master] "+f, args...)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package upgrade
|
||||
package overseer
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
@ -12,15 +12,15 @@ import (
|
|||
|
||||
var (
|
||||
//DisabledState is a placeholder state for when
|
||||
//go-upgrade is disabled and the program function
|
||||
//overseer is disabled and the program function
|
||||
//is run manually.
|
||||
DisabledState = State{Enabled: false}
|
||||
)
|
||||
|
||||
type State struct {
|
||||
//whether go-upgrade is running enabled. When enabled,
|
||||
//whether overseer is running enabled. When enabled,
|
||||
//this program will be running in a child process and
|
||||
//go-upgrade will perform rolling upgrades.
|
||||
//overseer will perform rolling upgrades.
|
||||
Enabled bool
|
||||
//ID is a SHA-1 hash of the current running binary
|
||||
ID string
|
||||
|
@ -32,9 +32,16 @@ type State struct {
|
|||
//process. These are all passed into this program in the
|
||||
//same order they are specified in Config.Addresses.
|
||||
Listeners []net.Listener
|
||||
//Program's first listening address
|
||||
Address string
|
||||
//Program's listening addresses
|
||||
Addresses []string
|
||||
//GracefulShutdown will be filled when its time to perform
|
||||
//a graceful shutdown.
|
||||
GracefulShutdown chan bool
|
||||
}
|
||||
|
||||
//a go-upgrade slave process
|
||||
//a overseer slave process
|
||||
|
||||
type slave struct {
|
||||
Config
|
||||
|
@ -48,6 +55,9 @@ func (sp *slave) run() {
|
|||
sp.state.Enabled = true
|
||||
sp.state.ID = os.Getenv(envBinID)
|
||||
sp.state.StartedAt = time.Now()
|
||||
sp.state.Address = sp.Config.Address
|
||||
sp.state.Addresses = sp.Config.Addresses
|
||||
sp.state.GracefulShutdown = make(chan bool, 1)
|
||||
sp.watchParent()
|
||||
sp.initFileDescriptors()
|
||||
sp.watchSignal()
|
||||
|
@ -99,24 +109,34 @@ func (sp *slave) initFileDescriptors() {
|
|||
|
||||
func (sp *slave) watchSignal() {
|
||||
signals := make(chan os.Signal)
|
||||
signal.Notify(signals, sp.Config.Signal)
|
||||
signal.Notify(signals, sp.Config.RestartSignal)
|
||||
go func() {
|
||||
<-signals
|
||||
signal.Stop(signals)
|
||||
sp.logf("graceful shutdown requested")
|
||||
//master wants to restart,
|
||||
//perform graceful shutdown:
|
||||
sp.state.GracefulShutdown <- true
|
||||
//release any sockets and notify master
|
||||
if len(sp.listeners) > 0 {
|
||||
//perform graceful shutdown
|
||||
for _, l := range sp.listeners {
|
||||
l.release(sp.Config.TerminateTimeout)
|
||||
}
|
||||
//signal released fds
|
||||
//signal release of held sockets
|
||||
sp.masterProc.Signal(SIGUSR1)
|
||||
//listeners should be waiting on connections to close...
|
||||
sp.logf("graceful shutdown")
|
||||
}
|
||||
//start death-timer
|
||||
go func() {
|
||||
time.Sleep(sp.Config.TerminateTimeout)
|
||||
sp.logf("timeout. forceful shutdown")
|
||||
os.Exit(1)
|
||||
}()
|
||||
}()
|
||||
}
|
||||
|
||||
func (sp *slave) logf(f string, args ...interface{}) {
|
||||
if sp.Log {
|
||||
log.Printf("[go-upgrade slave] "+f, args...)
|
||||
log.Printf("[overseer slave] "+f, args...)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// +build linux darwin
|
||||
|
||||
package upgrade
|
||||
package overseer
|
||||
|
||||
//this file attempts to contain all posix
|
||||
//specific stuff, that needs to be implemented
|
||||
|
@ -15,11 +15,13 @@ const supported = true
|
|||
|
||||
var (
|
||||
SIGUSR1 = syscall.SIGUSR1
|
||||
SIGUSR2 = syscall.SIGUSR2
|
||||
SIGTERM = syscall.SIGTERM
|
||||
)
|
||||
|
||||
func move(dst, src string) error {
|
||||
//HACK: we're shelling out to mv because linux
|
||||
//throws errors when we use Rename/Create.
|
||||
//throws errors when we use Rename/Create a
|
||||
//running binary.
|
||||
return exec.Command("mv", src, dst).Run()
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// +build !linux,!darwin
|
||||
|
||||
package upgrade
|
||||
package overseer
|
||||
|
||||
const supported = false
|
||||
|
|
Loading…
Reference in New Issue