From fcae6ce697fd6bc7987d398630330a30ad004ca3 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Thu, 27 Jun 2019 12:46:29 -0500 Subject: [PATCH] switchover: hardening & refactor instrumentation (#23) * fix migration template typo * automatic trigger generation * handle pgx migration errors * make no-pause-api and extra sync default behavior * proper db identification/validation * add progress for long operations * normalize table scanning * cancelation fixes * first pass at switchover docs * include version in DB application_name * status information improvements --- Makefile | 11 +- app/cmd.go | 19 +- app/version.go | 9 - go.mod | 13 +- go.sum | 31 +- migrate/inline_data_gen.go | 135 ++++--- migrate/migrate.go | 11 +- ...90613114217-remove-switchover-triggers.sql | 375 ++++++++++++++++++ ...190613120345-drop-switchover-resources.sql | 44 ++ switchover/README.md | 96 +++++ switchover/dbsync/changelog.go | 188 +++++++++ switchover/dbsync/dbid.go | 15 + switchover/dbsync/diffsync.go | 26 +- switchover/dbsync/initsync.go | 27 +- switchover/dbsync/listen.go | 9 +- switchover/dbsync/shell.go | 154 +++++-- switchover/dbsync/status.go | 43 +- switchover/dbsync/sync.go | 35 +- switchover/dbsync/table.go | 16 +- switchover/deadlineconfig.go | 1 + switchover/handler.go | 20 +- switchover/mainloop.go | 37 +- switchover/notify.go | 67 +++- switchover/status.go | 41 +- version/version.go | 26 ++ 25 files changed, 1186 insertions(+), 263 deletions(-) delete mode 100644 app/version.go create mode 100644 migrate/migrations/20190613114217-remove-switchover-triggers.sql create mode 100644 migrate/migrations/20190613120345-drop-switchover-resources.sql create mode 100644 switchover/README.md create mode 100644 switchover/dbsync/changelog.go create mode 100644 switchover/dbsync/dbid.go create mode 100644 version/version.go diff --git a/Makefile b/Makefile index 8ca30d19a5..6c423d696e 100644 --- a/Makefile +++ b/Makefile @@ -18,10 +18,10 @@ GIT_COMMIT=$(shell git rev-parse HEAD || echo '?') GIT_TREE=$(shell git diff-index --quiet HEAD -- && echo clean || echo dirty) BUILD_DATE=$(shell date -u +"%Y-%m-%dT%H:%M:%SZ") -LD_FLAGS+=-X github.com/target/goalert/app.gitCommit=$(GIT_COMMIT) -LD_FLAGS+=-X github.com/target/goalert/app.gitVersion=$(GIT_VERSION) -LD_FLAGS+=-X github.com/target/goalert/app.gitTreeState=$(GIT_TREE) -LD_FLAGS+=-X github.com/target/goalert/app.buildDate=$(BUILD_DATE) +LD_FLAGS+=-X github.com/target/goalert/version.gitCommit=$(GIT_COMMIT) +LD_FLAGS+=-X github.com/target/goalert/version.gitVersion=$(GIT_VERSION) +LD_FLAGS+=-X github.com/target/goalert/version.gitTreeState=$(GIT_TREE) +LD_FLAGS+=-X github.com/target/goalert/version.buildDate=$(BUILD_DATE) ifdef LOG_DIR @@ -31,6 +31,7 @@ endif export CGO_ENABLED = 0 export PATH := $(PWD)/bin:$(PATH) export GOOS = $(shell go env GOOS) +export GOALERT_DB_URL_NEXT = $(DB_URL_NEXT) ifdef BUNDLE GOFILES += web/inline_data_gen.go @@ -167,5 +168,5 @@ lint: $(GOFILES) new-migration: @test "$(NAME)" != "" || (echo "NAME is required" && false) @test ! -f migrate/migrations/*-$(NAME).sql || (echo "Migration already exists with the name $(NAME)." && false) - @echo "-- +migrate up\n\n\n-- +migrate Down\n" >migrate/migrations/$(shell date +%Y%m%d%H%M%S)-$(NAME).sql + @echo "-- +migrate Up\n\n\n-- +migrate Down\n" >migrate/migrations/$(shell date +%Y%m%d%H%M%S)-$(NAME).sql @echo "Created: migrate/migrations/$(shell date +%Y%m%d%H%M%S)-$(NAME).sql" diff --git a/app/cmd.go b/app/cmd.go index f0589feb50..57dfb51b0f 100644 --- a/app/cmd.go +++ b/app/cmd.go @@ -29,6 +29,7 @@ import ( "github.com/target/goalert/user" "github.com/target/goalert/util/log" "github.com/target/goalert/validation" + "github.com/target/goalert/version" "go.opencensus.io/trace" "golang.org/x/crypto/ssh/terminal" ) @@ -89,9 +90,9 @@ var RootCmd = &cobra.Command{ } q := u.Query() if cfg.DBURLNext != "" { - q.Set("application_name", "GoAlert (Switch-Over Mode)") + q.Set("application_name", fmt.Sprintf("GoAlert %s (S/O Mode)", version.GitVersion())) } else { - q.Set("application_name", "GoAlert") + q.Set("application_name", fmt.Sprintf("GoAlert %s", version.GitVersion())) } u.RawQuery = q.Encode() cfg.DBURL = u.String() @@ -108,7 +109,7 @@ var RootCmd = &cobra.Command{ return errors.Wrap(err, "parse next URL") } q := u.Query() - q.Set("application_name", "GoAlert (Switch-Over Mode)") + q.Set("application_name", fmt.Sprintf("GoAlert %s (S/O Mode)", version.GitVersion())) u.RawQuery = q.Encode() cfg.DBURLNext = u.String() @@ -179,12 +180,6 @@ var ( Short: "Output the current version.", RunE: func(cmd *cobra.Command, args []string) error { - date := buildDate - t, err := time.Parse(time.RFC3339, date) - if err == nil { - date = t.Local().Format(time.RFC3339) - } - migrations := migrate.Names() fmt.Printf(`Version: %s @@ -193,9 +188,9 @@ BuildDate: %s GoVersion: %s (%s) Platform: %s/%s Migration: %s (#%d) -`, gitVersion, - gitCommit, gitTreeState, - date, +`, version.GitVersion(), + version.GitCommit(), version.GitTreeState(), + version.BuildDate().Local().Format(time.RFC3339), runtime.Version(), runtime.Compiler, runtime.GOOS, runtime.GOARCH, migrations[len(migrations)-1], len(migrations), diff --git a/app/version.go b/app/version.go deleted file mode 100644 index 426a5c2d38..0000000000 --- a/app/version.go +++ /dev/null @@ -1,9 +0,0 @@ -package app - -var ( - gitVersion = "dev" - gitTreeState = "unknown" - gitCommit = "?" - - buildDate = "1970-01-01T00:00:00Z" -) diff --git a/go.mod b/go.mod index a7b7fb6e5e..6d2e4d40e5 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,6 @@ require ( contrib.go.opencensus.io/exporter/jaeger v0.1.0 contrib.go.opencensus.io/exporter/stackdriver v0.12.1 github.com/99designs/gqlgen v0.8.3 - github.com/VividCortex/ewma v1.1.1 // indirect github.com/abiosoft/ishell v2.0.0+incompatible github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db // indirect github.com/agnivade/levenshtein v1.0.2 // indirect @@ -29,7 +28,6 @@ require ( github.com/golang/mock v1.3.1 github.com/google/go-github v17.0.0+incompatible github.com/google/go-querystring v1.0.0 // indirect - github.com/google/pprof v0.0.0-20190515194954-54271f7e092f // indirect github.com/gordonklaus/ineffassign v0.0.0-20190601041439-ed7b1b5ee0f8 github.com/gorilla/context v1.1.1 // indirect github.com/gorilla/mux v1.6.2 // indirect @@ -38,7 +36,6 @@ require ( github.com/jackc/pgx v3.4.0+incompatible github.com/joho/godotenv v1.3.0 github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect - github.com/kr/pty v1.1.5 // indirect github.com/lib/pq v1.1.1 github.com/magiconair/properties v1.8.1 // indirect github.com/mattn/go-colorable v0.1.2 @@ -53,25 +50,19 @@ require ( github.com/spf13/cobra v0.0.5 github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/viper v1.4.0 - github.com/stretchr/objx v0.2.0 // indirect github.com/stretchr/testify v1.3.0 github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 // indirect github.com/ttacon/libphonenumber v1.0.1 - github.com/vbauerster/mpb v3.4.0+incompatible + github.com/vbauerster/mpb/v4 v4.8.4 github.com/vektah/gqlparser v1.1.2 go.opencensus.io v0.22.0 golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8 - golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522 // indirect - golang.org/x/image v0.0.0-20190523035834-f03afa92d3ff // indirect - golang.org/x/mobile v0.0.0-20190607214518-6fa95d984e88 // indirect - golang.org/x/mod v0.1.0 // indirect golang.org/x/net v0.0.0-20190611141213-3f473d35a33a golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae // indirect golang.org/x/tools v0.0.0-20190612180059-59534d075a87 google.golang.org/appengine v1.6.1 // indirect - google.golang.org/genproto v0.0.0-20190611190212-a7e196e89fd3 + google.golang.org/genproto v0.0.0-20190611190212-a7e196e89fd3 // indirect google.golang.org/grpc v1.21.1 // indirect gopkg.in/square/go-jose.v2 v2.3.1 // indirect - honnef.co/go/tools v0.0.0-20190607181801-497c8f037f5a // indirect ) diff --git a/go.sum b/go.sum index c5c4ab8f21..751df8f552 100644 --- a/go.sum +++ b/go.sum @@ -12,7 +12,6 @@ github.com/99designs/gqlgen v0.8.3 h1:I6bMglXNKkn4KlvkSMzqZw53e1N2FF9Gud4NmsOxqi github.com/99designs/gqlgen v0.8.3/go.mod h1:aLyJw9xUgdJxZ8EqNQxo2pGFhXXJ/hq8t7J4yn8TgI4= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= @@ -46,8 +45,6 @@ github.com/apache/thrift v0.12.0 h1:pODnxUFNcjP9UTLZGTdeh+j16A8lJbRvD3rOtrk/7bs= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/aws/aws-sdk-go v1.19.18/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.19.48 h1:YhKzuc9xggUt8jNDc5CmIBeB8GmGtazzq0aCXO4sj6w= -github.com/aws/aws-sdk-go v1.19.48/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.19.49 h1:GUlenK625g5iKrIiRcqRS/CvPMLc8kZRtMxXuXBhFx4= github.com/aws/aws-sdk-go v1.19.49/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= @@ -133,8 +130,6 @@ github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASu github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/googleapis/gax-go/v2 v2.0.4 h1:hU4mGcQI4DaAYW+IbTun+2qEZVFxK0ySjQLTbS0VQKc= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/gordonklaus/ineffassign v0.0.0-20190601041439-ed7b1b5ee0f8 h1:ehVe1P3MbhHjeN/Rn66N2fGLrP85XXO1uxpLhv0jtX8= @@ -181,7 +176,6 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4= @@ -243,7 +237,6 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rubenv/sql-migrate v0.0.0-20190327083759-54bad0a9b051 h1:p32bQkgLiadYiOqs294BAx/7f1Aerfva8rj+rVvzR0A= github.com/rubenv/sql-migrate v0.0.0-20190327083759-54bad0a9b051/go.mod h1:WS0rl9eEliYI8DPnr3TOwz4439pay+qNgzJoVya/DmY= @@ -278,7 +271,6 @@ github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= @@ -292,8 +284,8 @@ github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGr github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/vbauerster/mpb v3.4.0+incompatible h1:mfiiYw87ARaeRW6x5gWwYRUawxaW1tLAD8IceomUCNw= -github.com/vbauerster/mpb v3.4.0+incompatible/go.mod h1:zAHG26FUhVKETRu+MWqYXcI70POlC6N8up9p1dID7SU= +github.com/vbauerster/mpb/v4 v4.8.4 h1:KaZ/bVd3m7p2qy1R487T53rouanep/AWjnaU7fv/c4k= +github.com/vbauerster/mpb/v4 v4.8.4/go.mod h1:MxQf3eHZgvVuUmfdEO8DId0YhjNqqFQBEwrzZa06BH4= github.com/vektah/dataloaden v0.2.0/go.mod h1:vxM6NuRlgiR0M6wbVTJeKp9vQIs81ZMfCYO+4yq/jbE= github.com/vektah/gqlparser v1.1.2 h1:ZsyLGn7/7jDNI+y4SEhI4yAxRChlv15pUHMjijT+e68= github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= @@ -310,24 +302,16 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8 h1:1wopBVtVdWnn03fZelqdXTqk7U7zPQCb+T4rbU9ZEoU= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190523035834-f03afa92d3ff/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190607214518-6fa95d984e88/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/net v0.0.0-20180404174746-b3c676e531a6/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -368,11 +352,11 @@ golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae h1:xiXzMMEQdQcric9hXtr1QU98MHunKK7OTtsoU6bYWs4= golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -390,14 +374,10 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190530171427-2b03ca6e44eb/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190612165135-ecc01b77164e h1:muqikcYdRl/dTs2s93+BgyCGIvo7rrTfV+ZTCo+0aH4= -golang.org/x/tools v0.0.0-20190612165135-ecc01b77164e/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190612180059-59534d075a87 h1:RZgRBvCwrKVLcEd+O/QbyISA6RAPPLTYHdPw4Nk0Wz0= golang.org/x/tools v0.0.0-20190612180059-59534d075a87/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= @@ -433,7 +413,6 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4= @@ -450,8 +429,6 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a h1:/8zB6iBfHCl1qAnEAWwGPNr honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a h1:LJwr7TCTghdatWv40WobzlKXc9c4s8oGa7QKJUtHhWA= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190607181801-497c8f037f5a h1:8hwJsTVj2SosXzXScLqWbGd62RvzHgSc5Ro8LIHCx8k= -honnef.co/go/tools v0.0.0-20190607181801-497c8f037f5a/go.mod h1:JlmFZigtG9vBVR3QGIQ9g/Usz4BzH+Xm6Z8iHQWRYUw= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= sourcegraph.com/sourcegraph/appdash v0.0.0-20180110180208-2cc67fd64755/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k= diff --git a/migrate/inline_data_gen.go b/migrate/inline_data_gen.go index f0dc27d5fe..8d6e249806 100644 --- a/migrate/inline_data_gen.go +++ b/migrate/inline_data_gen.go @@ -492,64 +492,79 @@ VAv6YYmLJrQVOMf5skpLXLmspFjXIW4yZJfpl2jw6tWbV_948-rV4M0b6eft1PsxdhLENhwRsrpL6_im uMUldxaNKiMXepqoJOZpPtfEPPpoXWEW45lcAYnLncHZoMVn9NNsdgokzV-XJRHBWGMYPpNFH_88Of7n sPg8cpKWu98uPo_NLyitT12u8ZhPtjFfuD2yI9G7DcxVQHlRl1FeMd2Tlc3WLtmhux1O36HJb9OLywuU JuO-5giaJuin6XtWp1K8UD421pC0YhaNsBelzJyUukYhiy1BIMZr_jzoxb0FMMBxW4DjcSbeAkDwyM0g -WZVoHw6JOWAzAJ5w24cD4onObQRIUx9IsXkL4EDDdgIGl2WxFcQ4Y7YG4_9n72p728aR8OfkV3AXPdjG -urcb4PaAPcMF0tgtfJfaC8c5XD8Jiq2mvtpSINtps4v97wcOKb4OKUqWt9q99EsRizMcPnwRORzNww4T -TRuiai03RafpaMAKXWGgAexmArbVm-y-KSt0rbgp5gulyiuAuOwcuBftQBn8e5lAWcfXJmHSxhdRYUKO -r5TChLGpXF2UDfhqcupUCZQ0qHsqCOmj0SNZsinxHL7gzKQzUEnVLLUlrXugbwYnb2DvZhWTe3Exz-yd -YfaAMUXuIUxNkncbweXZZwfD5P5LxKqbTPlHNcstXH0tiztMKruK9zH5581s-lpzS2JnziJWxJOdlrx4 -cT4aX11f8ryty0Pu3ef-Y8j3owPlw1-eWA82uFKBtqmFciyjg7UNJUVe3MkbRfybob6110kA1IMkQUgE -uou30exnMiQdduDv9HR5dU-udnr20Ff6r897q8_6ps87RCbqL3bqUFufLN5GMKqi6eW7cZ99Jju7Hv11 -vaIHGNrlPapqvYo4ON1eHz59Xm7jLwpHgfrpMzFp_48yvS_GUKVGsBRGnkbQAsvtGj6biv67y9I7KtOz -24T3WPEQEq_4HANXs3fvJouauTr4CqF9ti-XiF9-iX76Sa5RxbQBcIuUQjwK33BCeCQNT4TMB6F-ZC4l -dQ4Y2y1hzujfCxAUDC8Q9UBoJQDmQdWAwqJ6xEFxawmDx0ea2SKggAAQg4gzA7rBMSXDYQHJtgIiHAwI -JpIc0w0LIh-OjPCWtBQcjQMUQ8ggCXXD5NIUjpWmoYWALbP0w_o-2qy3a2s0ac8cMLnkgxDShFsIDu5E -00FypJ5EwSrTFwQa7jFsIXh2BmsTOruECzivrjDYbBV_ANBETpYS6ES5QAAxvbVgFIr-EGAyj28ZlPwE -GQikqbMmjHbqp_aCmDyUj0eeqDoYRF1nbRDNjOrtAFHwAUfbLF3vM-stYhdwQOfVFISaraGFgEGQSs76 -9lPyZMJlPnaA5dESBJUp30KgOJm7gY-geEdhsWWC0OBiLQTBk5dYx8WXYQmFKkhzEHqe3IXtA9ROl6Tj -aD13wOfTE4Sanb-ofWDlyT373tZEST5wwINKBuEiJdsICJa1z8QGK-OCqURfGGKYkjaDh-1V9YdlcNXa -mRrhFu0FyDWiykZR7ZHTxtHiCCHRkcELOWAq1RiEmSNmpsUAwkenLuDyg3uL4NRQDSiQbjFALmzKYKmN -SCvB4MQBJhYull-nVBgSGMlvO4BAY8V0ULAiDoBKtAWBhQbFtRo4Hk_mRI09L4fM0lMVLx4u11qwtPA7 -FC61hB8wh64qkGkxhu0DDftg2EANK-KArURbEG6YjrYCJ8MsEcjEQx9YmIZwmGTcaEsBMiJAEZT0Ej6o -nLrC8TLCXFsKmp3eBAPOLuUDz6szHEBbTVtBzB6TPF-vcOzEQx9kmIZwpIR0WwF6THLlm8vMgZRdygeZ -V2c4draaloKIQeYDqBYcJ288EnZ_fHxi_YC-ZiPhjg8Xayq-qvlQpKZidU4V1nKKiI9TB0ScMkbglFfn -zd8oN3vlesy95Omv85q-3zr-CuhUdybNXScc63c_nau6OR_usc7O4_yDp3GqNe1xOoVT5jQ-i-YO9Kc4 --Z7yYNjceemUB4u6-20jqYuUtHfGpV_mmXkhrM8IBRYWu7RUaCAq2JKXH5PlJyyLgrsWZwZBl3qW-YF0 -u4Jr7hVLvdMr4dE-SF5o-NWocKcxq7NBjecrHI3fjOdzqjOkqY1UOZ0t3NUa7TTurAnCE268Bx1MgVB1 -H-ugCvKOfOx6STQ3u05zaCQXms7IJZsN4b3xfwHMfHyzmE-uFv4x4oq4RYfq7nFJT0kVx37tKkrGunGS -X-XZA1mnq-QLWX9giQx3QHJ32MQbmFLr1eB8mSdUmpVbZin_rHLzZBYlWYp-Td1lj-kS468RcuJUqFYp -76xbKVNugKT4DahdJFNzVa0yoZT4VMJ6ogJ6oe2sOUD42iuY7csAM8sDkyanw5G_HgOUbVHNphVcyMGD -wRKgjQvhKz2muYiVJakJSpDTmJ_CulMTET0aMWIvtZFBdX-8q1YxlDdr3R3u6APuqCib84q2dVqx3UwA -q9_wb1QwQiSjC7aiSKyGmFEkyzt6Thkjo0JfVgG-Ej51h3q2VY5d5UAbxYGy12Z2VYt4ezrbRo_0NBNW -c1HUUSt-UArr8AoAhTXG3C41ku-2cJV0BpxyXnfAkMmUjP9zdX17M_n3mLybQfZ1XrERC3czXrB8gsWp -Z8jSRqzoPnOfH9Jlt8Mo4Tt9Uaj3XeeCE8Ur2QOBRp6qsNIQ9pT0p0UtaAWyRO_cZ7JlbvH3y4qGcWjB -GLr9xRrLFN0nOfkL-ZEMyQW2ReWJVC44U767d919CfdVrMlfqVsrozeok78ZIVZWxrMjvyTs_TfxXbLZ -BeX32d9re6rb28lIJsNUyZS419CX3ZeeHpCkP4_x5oDmCSpYInQb-lRP79wi8qErBmuYavBsKlqrq0Gz -ScJ506NOc-Cwp2U5JI-85uUm6H6nomb0oteSCLrpZVJf4ar3iOabmZdrpyWOV49xukyiLAdnVp7tI36M -j1bJxpmiWM0alSafQQBmyEBQ9Twm6q9KTmO-zjn8Cjzt8WKmKpEJpBBPCVsKVF7EIaRVUn6RqaFUy75h -5dYrNLuUzL9kZpfaJZtkuScX5EOebeW9Cvn8MckTgtXfo3Vd6NXwAehqVXDLLBM5viqURRchOGqf2YhK -hQneysnldCRKrlcKoOqDh2y3pgL03dP9gaW8Kn777oKnm56PxnPy-r0sPBrfXMGj68m7yQLemIR3gBhw -kxtYi08Hq5p5iy8dDp30DeUY0WSo41-_Z9WRWS-BNpJF-Hnp-J2Wjj_1vHyelsdMy1oHPP4HGZK_eelM -SlMSE3FbACk5azPOGHqE74oltbTpMMwLAm3HrCYJlVQi2M5XaZOo0nEFb3kHl5skTg8PJNusCAt3VAcq -ihaDGvhXKBA8C2ZXGXLqNBcTW-GktmczYUfb_WEHmS-Xm2yXrDpkNtdKMKcGcH_wi0XyknQuyCp-Ug5b -IGI0b3V42KyXkJezSiM5nQBdH9QVbBt_gZ7bxl-KVdetiT59O5_d_gyriMYQQ21haUz5gEcV0KEu-nUI -QsGDml_m6MORKiyGsBxP_MSFDivGz-AcU3ar0BNW1Vntp0EKAkCdRHxa27NLMBGaE9gkslJAM06LJlbP -B5SveUAxdwPaHFZXKN1iNmR6z6eU51PKV94OPZ9Snh0cz0vH89Jx7NJBNx4WuxTEDoBbNU02jD9c8pNS -Izo315dX_0Koo3SuPKZAuUqACwKbP0pu2msdqMBAl-H6pYGLdMB5y7BN9jGBfO0WL5fFMIU23qKZsm1E -aAL9sVhFnkqib0YLnQXQymnS0S_lh0uFhSmO9nRxi5ZxGmXp5im6S6IsTdBQOmfpIgBUzIIusNsCe20R -S5Bm-_Sw2ZA9_fGCJJtdQn4gSboi3wkxKSXu5CpLqlOvmqSCtFOwB5dwVK7nhCjNInHulN1S3Gzhn9Mw -sgJhAD4qw8ePesyRWut1atGBkxs2Vy-nI6171N9V8OnvxfyezUmImqI4pgpRoxYPtej2-rokKNlO6XZ2 -iul4ph3Si2ZBxymMw1ohKxLZLq73bbblXy5s6TaQm8R5-s7Pzrrm3IzTFVKJ9lQ2f72jv_bOz86ynCpD -pg-ujwnCw8IAoctgGrVCWJHOMdc0tNF9Uc6aE3hHqM6S8n4wXCvWyl_9pv9HrxOkGXJpuYackF9ar6S6 -Sq-P13sPz8cNe0eb9bH9z2jEeGAMQqQO9x92BhqNpdtq0uXm9STxbKEDjzpQNrMetVbTpWEV2n5Qw68K -GtsSANgEMR1uCmeLsU3ZHe4iZQUw2YiD1kaYRTCDLF-38QE4r0X7ALyLmBBlD7vyd6kS46g5DjV9_peG -AYy5JinmZ2lS9MHAiIRQaH3N8EQZBF7CyhOkQwsPOXdz8dCpNyXAg5Q8JumejkCtJ3dJuu_Ae5aWoYDB -sBoCGW3e6Xk5hpHoduAa9r8B0CFYAnSxlaEjXI1itXcRaKAtWkyLB1ZLwEdPf4Z-rdd7viA0lgRAnhxd -EWh0M7eN6aJrUvzH-5i8fr8YXxoP6p82teMuRh1SMaqKSVWPqnJEppXkWsCSLGgRa8VPDWxJ_u7dknB9 -6EIvYirh4D1kR-9ol-y79Ic-6fz6Obn7mGWfbufXv3UUNjU4wff6ZJ8fikhYdqgfgqqXr17Bm2oy6ii2 -UcOYK8O3q_EbatYi7ev01ZbAfy-J-txlSdN7QJfDR37RudvEy0-MOlF31oiZoMw8izDd60iIlzB8gcZd -97CUe1QM29xjE980UJQ4-p1ff-vwPRL8wo4ROhEn3s_EvCUFeaq6WByo7r6nnLLd976vqtRvHOc8hbiV -Dd43gp1PxbqSJx_yZPcRvyagdr14Af9JOkz6bzpbTN68J99-f5_BS-F7pu0l1_atlxdxwPVWdHwi70ne -hsPDSnzdqC3cYpEGp7n6XgRTsJeeAx3PUHfZY63TAZ0wOP9fAAAA__9Go9o3A-EDAA== +WZVoHw6JOWAzAJ5w24cD4onObQRIUx9IsXkL4EDDdgIGl2WxFcQ4Y7YG4_9n71p727bV8OfmV3BDD2xj +7rYAZwfYAg9IY7fwTmoXiXO2fhIcWU18akuGL0m6Yf_9gDfx9pIiJXnVdNIvRSzyIfXwIvLly-elm4m6 +KyKjFldFDdNRQy1UQM8K0JMJsqxeZXd11UJFhauif1BCPgHIVs8z-6TtmQe-L-OZ13LbxC-3diPKL5Pl +lpJfZmgoh2elHT4snzxUPHNqoXsCMqm90ZGzYFHi2HyRPZMagUpAU2lLXPaZuhgcvyFrNyOZWIvn48xc +GWYbKFLknripieDdmnN59miJMLl_imhx4wm7VBOvydFXzM8wcd7FfD9Hv1xPJ68VsyS05-S-Ig51WvTy +5clwdHF5znRb48PWuc79acDWo2fSxV8mrEcWuAJAWdSSdFTRwViGIq6LO34jZf9qoC7t1SAA8kYSAUEE +urO30fQ9GqAO3fB3emp-eU0uN3q26Uvt12et1adt02cNIoT6-UqdlNZHs7cR6VXR5PzdqE-vyU4vh98u +F3gDg5u8h6GWi4iR0-31ydXneD1_kmIUyFefkR72v1LV-3kfCnoJKmHkeAmcIF4vybWp6L-7LL3FeXrm +O8Etxh8S4RWXYeBi-u7deFZSq4PNEMq1fTFF_P579OOPYo7iw4aQyyWFmBe-ZoRw5NQsEUIPQr5kLnKq +MWBMs4Q-ov8qQkAynESUI6GRBOgbVY0KI9QjTIodxY8eV9DMBhFFAgBCFLHIgHZy9Jz-tJCcTSUkNzAA +nIjgmHZagPz-zOTWkoaSo8QAhRjSgoTaabIh-XOlIDSQsDhLPy7votVyvTR6k_LMQpMtvxdDSuYGkgMb +0VSSLNKTIFlFeF6kwRbDBpJnKljr1JkpbMQ5sfxoMyH-BqTlmiwF1OXpPAmEcEvRmAP9LcikFt8iKtkO +0pNIHbMkjab0U3NJTDbF_ZEJVXuTqGKWJlFXVG8GiXk84Gidpct9ZnxFzAQW6pxIXqyZCA0kjDipbGnb +fko-63Tpjy1kOVC8qNLzN5AoFsxd4ycP8Q7SYubxYoNlayAJDl1ilReXwhJIlReyF3sO7cLmEWrKJak8 +Gs8t9LlwvFgz9YuaR9Y2uaP3bXWWxAMLPWBOL15EziYSAqn26dxAaWw0FeD5MQaBNJk8aK2qPiyiq9TK +VHO3aC5Bth5V1ItK95wm9haLC4nKDJzIQlMhohdnFp-ZBhNILp3aiNse7EsEK0IYUSR3gwmycVNES2lG +GkkGCxygc2GL8mvN5ccEFOS3GUSAvmIqKVASC0EFaF5kgU5xjSaO-ZNZWaPPiykzcEL5Yu5yjSVLcb8D +6ZJTuAmzYIVQpvgYNo806MKwxhqUxEJbAZoXbxBGU4kTbpYAZflDF1kQgj9Nwm-0oQRpHqAAS2oKF1VW +LH--NDfXhpJmyptAxJmpXOQ5Mf0JNGGaSmL2kGy3ywXMXf7QRRmE4M9UnrupBD0kW-nOZWZhykzlosyJ +6c-dCdNQEiHKXASVouPoLw-43Vf3Tyzv0FevJ1x1d7G6_Kvqd0Wqy1fnWG4tx_D4OLZDxDF9BI55dF7_ +iXK9R65VziWPf5xX9_lW9SOgY52Z1HecUNXufjxTdX023KrGzmr2weMY1eq2OB3DKHMcm0V9G_pj7HyP +uTGsb790zI1F2fW2Juoicpor48KbebouhHGNMOfCiC4tADVG82jJ8X0Sf4JUFOylWBUEbfBU-QF1u3ms +uZ-p9E6vII72QcSFJr9qBe6UyOq0U8N6hcPRm9HVFcb0edVaipxMZ_ZitffUzqwRECdc-w5aIgWSovtQ +AwXkt-ixqylBbXY1zKEmLjSZonM6Gvxb4_-CmKvR9exqfDFz9xGbxy3YVXcPMd4lBfb90kUU9HVtJ7_Y +Zhu0TBfJE1p-pEKGOxLk7rCar8iQWi7OTuJtgnPTdHGWsmuVq896UpSl4G3qLn2Mpxh3iUQTJ6BYKb21 +bClNcQVEiF-P0nMxNVvRciSUApuKX0sEsOf7niU7CJt788j2RYTp6UkkTRYOR_xahSizRiVfjcdC9u4M +Rgb8cj7xSqu8LlDLAmmCAuaUyE9-zalkyVs0ooG95Jf0Kvv-Nqxgkl4vdXe4xQ-YoaJozEtoyzTwvWkG +qHzNvhFQiVyMzrsWXFgNqAYXy6s8prSeEdCWIcQH8VO2q2dradtVTLSWnITsNSO7ykmcLZ2towe8m_Er +mSe1lApvlPwaPIAgv5fRl0u16N1yU0nnjIWcVw0waDxBo98uLm-ux_8ZoXdTor7OCtZ84a5HM6onyHc9 +AyobscDrzP32kMbdDg0J3-nniXrfdE5ZoHhJPZCEkccQhgxhT5I_5aWABYgUvRNXlY3q8r9fBVaMUUsq +g5e_0MtSoLtki_6BfkADdAotUZmQyimLlG9vXXtbkvMq-spfqFmD2Tsro98MBFaW-rNFX5Ks_Vfz22S1 +89L32d8pa6qbm_FQiGHKwZSY1dCl7ot3D4Doz8N8dQB1gniUCLUOfYzTOzEC-eAZg76YXOHpJH9bFQZU +kyT7TQecYsChT4s0JCse87IqqHYnXjJ40Gvk8Drppbm-wFFvhdfXlZdLyxLPFw_zNE6ibEuMWdtsH7Ft +fLRIVlaJYlk1Kk0eSQYyQs7yUD0PifyrpGnM5jmLXYHJHs-mMogQkAIsJXQqkOMiDoiskvSLkIaSa_YV +TbdcgOpSQn9JV5faJask3qNT9HGbrcW5Cnq8T7YJgsrv4bJO1WJYB7S9lfebGVVk_MpU8iYCeFSu2eSF +5lVwFo7OJ8M85XIhESo_2GS7Jc6Avz3d76nkFf_tm1MmN301HF2h1x9E4uHo-oI8uhy_G8_IFxOxBsg7 +3PiazMXHo1VW3mJThwUTf6EsPRoNVP7Lt6zcM8sJaAMqws9Tx180dbR6XD4PyyrDstQGj_2BBuifznAm +hZLEKD8tIJKcpSPOaDi57YqKWprhMPQDAmXFLIuEilAi0MpXeqe8SMsRvGEdjFfJPD1sULZaIOruKHdU +kC1KNYm_golgKphdqcvJwzwf2FJManM0I7q13R92RPkyXmW7ZNFB0yslBTVqkNgf7GARvUKdU7SYf5Y2 +WySL9nqLw2a1jIkuZ8hLsnACeH6QZ7D1_Im03Hr-xGddOxJ--vZqevOezCJKhBhcFypjyjo8CIC7et6u +A5LJu1Ozwxy1O2JA3oVFf2I7LrBb0fgM1j5lvhW4wwod1e4wSF4EyIOIDWtzdOWRCPUBrAeykkjTdos6 +V88blC-5QdFXA8oYlmcotca0y_SedynPu5QvvBx63qU8Gziep47nqaPq1IEXHkZ0KeI7QMyqabKi8cNF +fFJcic715fnFv4HQUWqsPAogHSWQAwIzfpRYtJfaUJEK2iquHhrYgg5YTxnWyX6OiF67EZfLiDAFvrwR +ZsqsIxAm0O2LxXUqkboY5ZicaGk3aWmX4s2lFIVpHu3x5BbF8zTK0tXn6DaJsjQBXemsqbkDaD4KuiS6 +LYley30J0myfHlYrtMc_nqJktUvQ9yhJF-ibPJvIlZ_JBeeUh15YTolpa8YeOYTD-XpWitIsyvedoln4 +yRZ8nYYGK8grAPdK__4jb3MEarlG5Q04vqZj9XwyVJpH_l0mH__Ox_f0CvnA8OQQFAAjJ_et0c3lZYFT +sinp9uIYw_GFsknnr0UaToo4rCQyPJHN5GrbZmt2c2GNl4GsSixO38mLF119bM7TBVCI8lS8_nKHf-2d +vHiRbTEYMHxgPJqRPOQVyLG0SKOGCyvQOPqcBr50P09njAm4IWRjSXE7aKYVY-YPP-n_wWkEqSe4tJhD +jhhfWi0kHNJp43Wew7N-Q7_Renl0_TMc0jgwWkCkDrMfds6UMJb2WqMuq15PBJ7lGLDXgbSYdcAary4q +FvDuB9n9ioexLSCADhDd4CbFbNGWKbvDbSTNAHo0Yq-5kYwiMoIMW7d2AZyVolwA7wJViLLNrvhbKvk4 +KoZDBc_90dCI0eckqfpZmvA2ONM8IaSwvrp7onACL4jK44WhuIec2GPx4KE3QSQOUvKQpHvcA5WW3CXp +vkO-szgNJox0qwEJRrvt9JwxhgHvdhJr2P0FALtgAdF8KYN7uOzFaq4iQEdbMJniDyynIJee2tCu5VrP +5YRGRQDEztHmgYYXc-s5nnT1EP_z_Ry9_jAbnWsPyu82le0uFDok0KuK5gr3qrJ4phVoLUAiC4rHGv-p +hiXJv5xLEoYHTvS5TyXZeA_o1jvaJfsu_qGPOn88Jrf3Wfbp5uryz44UTY3s4Ht9tN8euCcs3dQPCNSr +n38mX6rxsCPVDVeMmjJcqxp3RfVSRP06fflNyH-vkPzcVpO614A2g4-40blbzeNPNHSiaqzJR4I08oyA +6U5Dwjwm3ZeEcVctLMUWFa1u9r4JLxowS4z9zh9_dtgaifxCtxFqIE64nZF-SkryY2g-OWDsviOdtNx3 +fq9Cyte2c45ErJY1njeSen7m88o2-bhNdvfwMQGu18uX5D8RDhP_m0xn4zcf0Nff3WXko_AdRXvF0L52 +xkU8Y7iBhk_gO8ne4bBZ5LcblYk7n6SJ0Vz-LpKqQB89CzuOrm6rjzFPezRCkcNxOe2kzeF2tYy_rSah +JINUVFLiUDUJKslwVXWVZKy65JVkzBIqSyCYugAoIdOk4NSn1sRgjyPaxMGPpt1kKaBeCSdrIfUpOTmK +qEHQiaHXrevEYGuRd2JYfipP4D0EhsCvI9QjE8VAj6YWxfBrE41ieLVqR3HMuiWkdNwqSlIaVi2CUgyz +fl0pHbiKvJSGVVJliqPUJjbFAI-gOWUi1yQ9pQLXq0DFsOsWopJh69OjklGPIEslw9erTiUj1ytSJSPX +olUlAe4gu2rAvcXhFO_M6G7PPw480g1m3EwmTGfANgQV2c5oBtOENvrtYvQeb6VOiB_ORLh_RxlZs58g +4lKEqJVgNBmily9L6vX6EVKJjLYQ4dj1hVCiwbSIHGP3GkoLAWgRIdAuPJQTjtEiWqzWhFBuFKB2EGRa +RQJIYedSbSLCtO4E88Ew2kFLoZUqgB4QqyU0ua1tISSZSO2kCLQaViAqx2srXYaZphJZBK29VGlW3IpU +JZuW9Cq3NTqAJROoHQS5rOoB9Ogw7SAHOB0I4ITlbgcV5ilHABNMZqsVRPid1gSQ49BqbQVhzlOnAJ5M +FcpW0AOfngXwIgBaQkjRKWAIN6DuertoqrJMVkHaRUzVPtOSflJ8uhzADQzWMqKMc8IyBFE12VYRU5WT +ttABeC2EsMGyt4OMIu-LAGJA7ai2kWQ6e5RjiOK0ih6bN0w4QTJSOygq8uoJ4AiCahFJoHdSKD05SIuI +sXtZhbKjBRpqD0Vub7FQmky0FlEFer2FMiTiFbaHGLf3XihDJlp7qKpCTHNo0O_VqVe1wGqgrrj5Ta7- +2aN80oti6t1oQZpPkI5sAwlb7ee3qySy6WFts8dI1zRl-Z6EXjG7UL0mwiYxVw3CeclN0l-up5PXxiVP +_6uGetMBFwyRpkMYH7bMkEW0LHaPy318jycZ9utPA9RZLkgIFHEfkYnlEeEOAcDiIUkab0SCQ4ekdxG5 +LqDI_hUuKY022-xum-x2HVBfUL7UqOoLzt5G0_dogDp0GHQ0zVFZa0TuDtmmL7Vsn7Vjn7ZanzVVL4fh +CiSktD6avY1ID4sm5-9GfRrLhon5zXekM_Qw1HIRMXK6PSrlF6_nTz3jyqZQT5Q19CpVvZ_3rqCXmIx- +db8EThCvl6l0oXwy-rVnvhPcYvwhueRbSpXvfwEAAP__xk_2yLkaBAA= ` dataRange := func(start, end int) func() []byte { return func() []byte { @@ -565,7 +580,7 @@ hsPDSnzdqC3cYpEGp7n6XgRTsJeeAx3PUHfZY63TAZ0wOP9fAAAA__9Go9o3A-EDAA== defer r.Close() buf := new(bytes.Buffer) - buf.Grow(43242) + buf.Grow(44445) _, err = io.Copy(buf, r) if err != nil { @@ -759,5 +774,7 @@ hsPDSnzdqC3cYpEGp7n6XgRTsJeeAx3PUHfZY63TAZ0wOP9fAAAA__9Go9o3A-EDAA== {Data: dataRange(253205, 253408), Name: "migrations/20190313125552-slack-user-link.sql"}, {Data: dataRange(253408, 253724), Name: "migrations/20190404105850-nc-no-meta.sql"}, {Data: dataRange(253724, 254211), Name: "migrations/20190517144224-trigger-config-sync.sql"}, + {Data: dataRange(254211, 267892), Name: "migrations/20190613114217-remove-switchover-triggers.sql"}, + {Data: dataRange(267892, 268985), Name: "migrations/20190613120345-drop-switchover-resources.sql"}, } } diff --git a/migrate/migrate.go b/migrate/migrate.go index 90b27bf49a..80332c7ee9 100644 --- a/migrate/migrate.go +++ b/migrate/migrate.go @@ -6,17 +6,18 @@ import ( "bytes" "context" "database/sql" - "github.com/target/goalert/lock" - "github.com/target/goalert/util/log" "io/ioutil" "os" "path/filepath" "strings" "time" + "github.com/jackc/pgx" "github.com/lib/pq" "github.com/pkg/errors" "github.com/rubenv/sql-migrate/sqlparse" + "github.com/target/goalert/lock" + "github.com/target/goalert/util/log" ) // Names will return all AssetNames without the timestamps and extensions @@ -112,7 +113,11 @@ func ensureTableQuery(ctx context.Context, db *sql.DB, fn func() error) error { } // 42P01 is undefined_table // https://www.postgresql.org/docs/9.6/static/errcodes-appendix.html - if pErr, ok := err.(*pq.Error); !ok || pErr.Code != "42P01" { + if pqErr, ok := err.(*pq.Error); ok && pqErr.Code == "42P01" { + // continue + } else if pgxErr, ok := err.(pgx.PgError); ok && pgxErr.Code == "42P01" { + // continue + } else { return err } diff --git a/migrate/migrations/20190613114217-remove-switchover-triggers.sql b/migrate/migrations/20190613114217-remove-switchover-triggers.sql new file mode 100644 index 0000000000..234314d338 --- /dev/null +++ b/migrate/migrations/20190613114217-remove-switchover-triggers.sql @@ -0,0 +1,375 @@ +-- +migrate Up notransaction + +DROP TRIGGER IF EXISTS zz_99_alert_logs_change_log ON public.alert_logs; +DROP TRIGGER IF EXISTS zz_99_alerts_change_log ON public.alerts; +DROP TRIGGER IF EXISTS zz_99_auth_basic_users_change_log ON public.auth_basic_users; +DROP TRIGGER IF EXISTS zz_99_auth_nonce_change_log ON public.auth_nonce; +DROP TRIGGER IF EXISTS zz_99_auth_subjects_change_log ON public.auth_subjects; +DROP TRIGGER IF EXISTS zz_99_auth_user_sessions_change_log ON public.auth_user_sessions; +DROP TRIGGER IF EXISTS zz_99_config_change_log ON public.config; +DROP TRIGGER IF EXISTS zz_99_config_limits_change_log ON public.config_limits; +DROP TRIGGER IF EXISTS zz_99_ep_step_on_call_users_change_log ON public.ep_step_on_call_users; +DROP TRIGGER IF EXISTS zz_99_escalation_policies_change_log ON public.escalation_policies; +DROP TRIGGER IF EXISTS zz_99_escalation_policy_actions_change_log ON public.escalation_policy_actions; +DROP TRIGGER IF EXISTS zz_99_escalation_policy_state_change_log ON public.escalation_policy_state; +DROP TRIGGER IF EXISTS zz_99_escalation_policy_steps_change_log ON public.escalation_policy_steps; +DROP TRIGGER IF EXISTS zz_99_heartbeat_monitors_change_log ON public.heartbeat_monitors; +DROP TRIGGER IF EXISTS zz_99_integration_keys_change_log ON public.integration_keys; +DROP TRIGGER IF EXISTS zz_99_keyring_change_log ON public.keyring; +DROP TRIGGER IF EXISTS zz_99_labels_change_log ON public.labels; +DROP TRIGGER IF EXISTS zz_99_notification_policy_cycles_change_log ON public.notification_policy_cycles; +DROP TRIGGER IF EXISTS zz_99_outgoing_messages_change_log ON public.outgoing_messages; +DROP TRIGGER IF EXISTS zz_99_region_ids_change_log ON public.region_ids; +DROP TRIGGER IF EXISTS zz_99_rotation_participants_change_log ON public.rotation_participants; +DROP TRIGGER IF EXISTS zz_99_rotation_state_change_log ON public.rotation_state; +DROP TRIGGER IF EXISTS zz_99_rotations_change_log ON public.rotations; +DROP TRIGGER IF EXISTS zz_99_schedule_on_call_users_change_log ON public.schedule_on_call_users; +DROP TRIGGER IF EXISTS zz_99_schedule_rules_change_log ON public.schedule_rules; +DROP TRIGGER IF EXISTS zz_99_schedules_change_log ON public.schedules; +DROP TRIGGER IF EXISTS zz_99_services_change_log ON public.services; +DROP TRIGGER IF EXISTS zz_99_twilio_sms_callbacks_change_log ON public.twilio_sms_callbacks; +DROP TRIGGER IF EXISTS zz_99_twilio_sms_errors_change_log ON public.twilio_sms_errors; +DROP TRIGGER IF EXISTS zz_99_twilio_voice_errors_change_log ON public.twilio_voice_errors; +DROP TRIGGER IF EXISTS zz_99_user_contact_methods_change_log ON public.user_contact_methods; +DROP TRIGGER IF EXISTS zz_99_user_favorites_change_log ON public.user_favorites; +DROP TRIGGER IF EXISTS zz_99_user_last_alert_log_change_log ON public.user_last_alert_log; +DROP TRIGGER IF EXISTS zz_99_user_notification_rules_change_log ON public.user_notification_rules; +DROP TRIGGER IF EXISTS zz_99_user_overrides_change_log ON public.user_overrides; +DROP TRIGGER IF EXISTS zz_99_user_verification_codes_change_log ON public.user_verification_codes; +DROP TRIGGER IF EXISTS zz_99_users_change_log ON public.users; + + +-- +migrate Down notransaction + +-- +migrate StatementBegin +DO $$ BEGIN +CREATE TRIGGER zz_99_alert_logs_change_log AFTER INSERT OR DELETE OR UPDATE ON public.alert_logs FOR EACH ROW EXECUTE PROCEDURE public.process_change(); +EXCEPTION + WHEN duplicate_object + THEN null; +END $$; +-- +migrate StatementEnd + +-- +migrate StatementBegin +DO $$ BEGIN +CREATE TRIGGER zz_99_alerts_change_log AFTER INSERT OR DELETE OR UPDATE ON public.alerts FOR EACH ROW EXECUTE PROCEDURE public.process_change(); +EXCEPTION + WHEN duplicate_object + THEN null; +END $$; +-- +migrate StatementEnd + +-- +migrate StatementBegin +DO $$ BEGIN +CREATE TRIGGER zz_99_auth_basic_users_change_log AFTER INSERT OR DELETE OR UPDATE ON public.auth_basic_users FOR EACH ROW EXECUTE PROCEDURE public.process_change(); +EXCEPTION + WHEN duplicate_object + THEN null; +END $$; +-- +migrate StatementEnd + +-- +migrate StatementBegin +DO $$ BEGIN +CREATE TRIGGER zz_99_auth_nonce_change_log AFTER INSERT OR DELETE OR UPDATE ON public.auth_nonce FOR EACH ROW EXECUTE PROCEDURE public.process_change(); +EXCEPTION + WHEN duplicate_object + THEN null; +END $$; +-- +migrate StatementEnd + +-- +migrate StatementBegin +DO $$ BEGIN +CREATE TRIGGER zz_99_auth_subjects_change_log AFTER INSERT OR DELETE OR UPDATE ON public.auth_subjects FOR EACH ROW EXECUTE PROCEDURE public.process_change(); +EXCEPTION + WHEN duplicate_object + THEN null; +END $$; +-- +migrate StatementEnd + +-- +migrate StatementBegin +DO $$ BEGIN +CREATE TRIGGER zz_99_auth_user_sessions_change_log AFTER INSERT OR DELETE OR UPDATE ON public.auth_user_sessions FOR EACH ROW EXECUTE PROCEDURE public.process_change(); +EXCEPTION + WHEN duplicate_object + THEN null; +END $$; +-- +migrate StatementEnd + +-- +migrate StatementBegin +DO $$ BEGIN +CREATE TRIGGER zz_99_config_change_log AFTER INSERT OR DELETE OR UPDATE ON public.config FOR EACH ROW EXECUTE PROCEDURE public.process_change(); +EXCEPTION + WHEN duplicate_object + THEN null; +END $$; +-- +migrate StatementEnd + +-- +migrate StatementBegin +DO $$ BEGIN +CREATE TRIGGER zz_99_config_limits_change_log AFTER INSERT OR DELETE OR UPDATE ON public.config_limits FOR EACH ROW EXECUTE PROCEDURE public.process_change(); +EXCEPTION + WHEN duplicate_object + THEN null; +END $$; +-- +migrate StatementEnd + +-- +migrate StatementBegin +DO $$ BEGIN +CREATE TRIGGER zz_99_ep_step_on_call_users_change_log AFTER INSERT OR DELETE OR UPDATE ON public.ep_step_on_call_users FOR EACH ROW EXECUTE PROCEDURE public.process_change(); +EXCEPTION + WHEN duplicate_object + THEN null; +END $$; +-- +migrate StatementEnd + +-- +migrate StatementBegin +DO $$ BEGIN +CREATE TRIGGER zz_99_escalation_policies_change_log AFTER INSERT OR DELETE OR UPDATE ON public.escalation_policies FOR EACH ROW EXECUTE PROCEDURE public.process_change(); +EXCEPTION + WHEN duplicate_object + THEN null; +END $$; +-- +migrate StatementEnd + +-- +migrate StatementBegin +DO $$ BEGIN +CREATE TRIGGER zz_99_escalation_policy_actions_change_log AFTER INSERT OR DELETE OR UPDATE ON public.escalation_policy_actions FOR EACH ROW EXECUTE PROCEDURE public.process_change(); +EXCEPTION + WHEN duplicate_object + THEN null; +END $$; +-- +migrate StatementEnd + +-- +migrate StatementBegin +DO $$ BEGIN +CREATE TRIGGER zz_99_escalation_policy_state_change_log AFTER INSERT OR DELETE OR UPDATE ON public.escalation_policy_state FOR EACH ROW EXECUTE PROCEDURE public.process_change(); +EXCEPTION + WHEN duplicate_object + THEN null; +END $$; +-- +migrate StatementEnd + +-- +migrate StatementBegin +DO $$ BEGIN +CREATE TRIGGER zz_99_escalation_policy_steps_change_log AFTER INSERT OR DELETE OR UPDATE ON public.escalation_policy_steps FOR EACH ROW EXECUTE PROCEDURE public.process_change(); +EXCEPTION + WHEN duplicate_object + THEN null; +END $$; +-- +migrate StatementEnd + +-- +migrate StatementBegin +DO $$ BEGIN +CREATE TRIGGER zz_99_heartbeat_monitors_change_log AFTER INSERT OR DELETE OR UPDATE ON public.heartbeat_monitors FOR EACH ROW EXECUTE PROCEDURE public.process_change(); +EXCEPTION + WHEN duplicate_object + THEN null; +END $$; +-- +migrate StatementEnd + +-- +migrate StatementBegin +DO $$ BEGIN +CREATE TRIGGER zz_99_integration_keys_change_log AFTER INSERT OR DELETE OR UPDATE ON public.integration_keys FOR EACH ROW EXECUTE PROCEDURE public.process_change(); +EXCEPTION + WHEN duplicate_object + THEN null; +END $$; +-- +migrate StatementEnd + +-- +migrate StatementBegin +DO $$ BEGIN +CREATE TRIGGER zz_99_keyring_change_log AFTER INSERT OR DELETE OR UPDATE ON public.keyring FOR EACH ROW EXECUTE PROCEDURE public.process_change(); +EXCEPTION + WHEN duplicate_object + THEN null; +END $$; +-- +migrate StatementEnd + +-- +migrate StatementBegin +DO $$ BEGIN +CREATE TRIGGER zz_99_labels_change_log AFTER INSERT OR DELETE OR UPDATE ON public.labels FOR EACH ROW EXECUTE PROCEDURE public.process_change(); +EXCEPTION + WHEN duplicate_object + THEN null; +END $$; +-- +migrate StatementEnd + +-- +migrate StatementBegin +DO $$ BEGIN +CREATE TRIGGER zz_99_notification_policy_cycles_change_log AFTER INSERT OR DELETE OR UPDATE ON public.notification_policy_cycles FOR EACH ROW EXECUTE PROCEDURE public.process_change(); +EXCEPTION + WHEN duplicate_object + THEN null; +END $$; +-- +migrate StatementEnd + +-- +migrate StatementBegin +DO $$ BEGIN +CREATE TRIGGER zz_99_outgoing_messages_change_log AFTER INSERT OR DELETE OR UPDATE ON public.outgoing_messages FOR EACH ROW EXECUTE PROCEDURE public.process_change(); +EXCEPTION + WHEN duplicate_object + THEN null; +END $$; +-- +migrate StatementEnd + +-- +migrate StatementBegin +DO $$ BEGIN +CREATE TRIGGER zz_99_region_ids_change_log AFTER INSERT OR DELETE OR UPDATE ON public.region_ids FOR EACH ROW EXECUTE PROCEDURE public.process_change(); +EXCEPTION + WHEN duplicate_object + THEN null; +END $$; +-- +migrate StatementEnd + +-- +migrate StatementBegin +DO $$ BEGIN +CREATE TRIGGER zz_99_rotation_participants_change_log AFTER INSERT OR DELETE OR UPDATE ON public.rotation_participants FOR EACH ROW EXECUTE PROCEDURE public.process_change(); +EXCEPTION + WHEN duplicate_object + THEN null; +END $$; +-- +migrate StatementEnd + +-- +migrate StatementBegin +DO $$ BEGIN +CREATE TRIGGER zz_99_rotation_state_change_log AFTER INSERT OR DELETE OR UPDATE ON public.rotation_state FOR EACH ROW EXECUTE PROCEDURE public.process_change(); +EXCEPTION + WHEN duplicate_object + THEN null; +END $$; +-- +migrate StatementEnd + +-- +migrate StatementBegin +DO $$ BEGIN +CREATE TRIGGER zz_99_rotations_change_log AFTER INSERT OR DELETE OR UPDATE ON public.rotations FOR EACH ROW EXECUTE PROCEDURE public.process_change(); +EXCEPTION + WHEN duplicate_object + THEN null; +END $$; +-- +migrate StatementEnd + +-- +migrate StatementBegin +DO $$ BEGIN +CREATE TRIGGER zz_99_schedule_on_call_users_change_log AFTER INSERT OR DELETE OR UPDATE ON public.schedule_on_call_users FOR EACH ROW EXECUTE PROCEDURE public.process_change(); +EXCEPTION + WHEN duplicate_object + THEN null; +END $$; +-- +migrate StatementEnd + +-- +migrate StatementBegin +DO $$ BEGIN +CREATE TRIGGER zz_99_schedule_rules_change_log AFTER INSERT OR DELETE OR UPDATE ON public.schedule_rules FOR EACH ROW EXECUTE PROCEDURE public.process_change(); +EXCEPTION + WHEN duplicate_object + THEN null; +END $$; +-- +migrate StatementEnd + +-- +migrate StatementBegin +DO $$ BEGIN +CREATE TRIGGER zz_99_schedules_change_log AFTER INSERT OR DELETE OR UPDATE ON public.schedules FOR EACH ROW EXECUTE PROCEDURE public.process_change(); +EXCEPTION + WHEN duplicate_object + THEN null; +END $$; +-- +migrate StatementEnd + +-- +migrate StatementBegin +DO $$ BEGIN +CREATE TRIGGER zz_99_services_change_log AFTER INSERT OR DELETE OR UPDATE ON public.services FOR EACH ROW EXECUTE PROCEDURE public.process_change(); +EXCEPTION + WHEN duplicate_object + THEN null; +END $$; +-- +migrate StatementEnd + +-- +migrate StatementBegin +DO $$ BEGIN +CREATE TRIGGER zz_99_twilio_sms_callbacks_change_log AFTER INSERT OR DELETE OR UPDATE ON public.twilio_sms_callbacks FOR EACH ROW EXECUTE PROCEDURE public.process_change(); +EXCEPTION + WHEN duplicate_object + THEN null; +END $$; +-- +migrate StatementEnd + +-- +migrate StatementBegin +DO $$ BEGIN +CREATE TRIGGER zz_99_twilio_sms_errors_change_log AFTER INSERT OR DELETE OR UPDATE ON public.twilio_sms_errors FOR EACH ROW EXECUTE PROCEDURE public.process_change(); +EXCEPTION + WHEN duplicate_object + THEN null; +END $$; +-- +migrate StatementEnd + +-- +migrate StatementBegin +DO $$ BEGIN +CREATE TRIGGER zz_99_twilio_voice_errors_change_log AFTER INSERT OR DELETE OR UPDATE ON public.twilio_voice_errors FOR EACH ROW EXECUTE PROCEDURE public.process_change(); +EXCEPTION + WHEN duplicate_object + THEN null; +END $$; +-- +migrate StatementEnd + +-- +migrate StatementBegin +DO $$ BEGIN +CREATE TRIGGER zz_99_user_contact_methods_change_log AFTER INSERT OR DELETE OR UPDATE ON public.user_contact_methods FOR EACH ROW EXECUTE PROCEDURE public.process_change(); +EXCEPTION + WHEN duplicate_object + THEN null; +END $$; +-- +migrate StatementEnd + +-- +migrate StatementBegin +DO $$ BEGIN +CREATE TRIGGER zz_99_user_favorites_change_log AFTER INSERT OR DELETE OR UPDATE ON public.user_favorites FOR EACH ROW EXECUTE PROCEDURE public.process_change(); +EXCEPTION + WHEN duplicate_object + THEN null; +END $$; +-- +migrate StatementEnd + +-- +migrate StatementBegin +DO $$ BEGIN +CREATE TRIGGER zz_99_user_last_alert_log_change_log AFTER INSERT OR DELETE OR UPDATE ON public.user_last_alert_log FOR EACH ROW EXECUTE PROCEDURE public.process_change(); +EXCEPTION + WHEN duplicate_object + THEN null; +END $$; +-- +migrate StatementEnd + +-- +migrate StatementBegin +DO $$ BEGIN +CREATE TRIGGER zz_99_user_notification_rules_change_log AFTER INSERT OR DELETE OR UPDATE ON public.user_notification_rules FOR EACH ROW EXECUTE PROCEDURE public.process_change(); +EXCEPTION + WHEN duplicate_object + THEN null; +END $$; +-- +migrate StatementEnd + +-- +migrate StatementBegin +DO $$ BEGIN +CREATE TRIGGER zz_99_user_overrides_change_log AFTER INSERT OR DELETE OR UPDATE ON public.user_overrides FOR EACH ROW EXECUTE PROCEDURE public.process_change(); +EXCEPTION + WHEN duplicate_object + THEN null; +END $$; +-- +migrate StatementEnd + +-- +migrate StatementBegin +DO $$ BEGIN +CREATE TRIGGER zz_99_user_verification_codes_change_log AFTER INSERT OR DELETE OR UPDATE ON public.user_verification_codes FOR EACH ROW EXECUTE PROCEDURE public.process_change(); +EXCEPTION + WHEN duplicate_object + THEN null; +END $$; +-- +migrate StatementEnd + +-- +migrate StatementBegin +DO $$ BEGIN +CREATE TRIGGER zz_99_users_change_log AFTER INSERT OR DELETE OR UPDATE ON public.users FOR EACH ROW EXECUTE PROCEDURE public.process_change(); +EXCEPTION + WHEN duplicate_object + THEN null; +END $$; +-- +migrate StatementEnd diff --git a/migrate/migrations/20190613120345-drop-switchover-resources.sql b/migrate/migrations/20190613120345-drop-switchover-resources.sql new file mode 100644 index 0000000000..25894f987f --- /dev/null +++ b/migrate/migrations/20190613120345-drop-switchover-resources.sql @@ -0,0 +1,44 @@ +-- +migrate Up + +DROP FUNCTION public.process_change (); + +DROP TABLE change_log; + +-- +migrate Down + +CREATE TABLE change_log ( + id BIGSERIAL PRIMARY KEY, + op TEXT NOT NULL, + table_name TEXT NOT NULL, + row_id TEXT NOT NULL, + tx_id BIGINT, + cmd_id cid, + row_data JSONB +); + +-- +migrate StatementBegin +CREATE OR REPLACE FUNCTION process_change() RETURNS TRIGGER AS $$ +DECLARE + cur_state enum_switchover_state := 'idle'; +BEGIN + SELECT INTO cur_state current_state + FROM switchover_state; + + IF cur_state != 'in_progress' THEN + RETURN NEW; + END IF; + + IF (TG_OP = 'DELETE') THEN + INSERT INTO change_log (op, table_name, row_id, tx_id, cmd_id) + VALUES (TG_OP, TG_TABLE_NAME, cast(OLD.id as TEXT), txid_current(), OLD.cmax); + RETURN OLD; + ELSE + INSERT INTO change_log (op, table_name, row_id, tx_id, cmd_id, row_data) + VALUES (TG_OP, TG_TABLE_NAME, cast(NEW.id as TEXT), txid_current(), NEW.cmin, to_jsonb(NEW)); + RETURN NEW; + END IF; + + RETURN NULL; +END; +$$ LANGUAGE 'plpgsql'; +-- +migrate StatementEnd diff --git a/switchover/README.md b/switchover/README.md new file mode 100644 index 0000000000..ccd35b363b --- /dev/null +++ b/switchover/README.md @@ -0,0 +1,96 @@ +# DB Switchover + +Switchover functionality intends to replicate data to a new empty DB and coordinate all GoAlert instances to switch their active DB without downtime, loss of data, and minimal latency impact. + +## High-Level Flow + +1. GoAlert instances start in "switchover mode", and know about both DBs +1. A control shell validates config & state +1. New DB is migrated to be structuraly identical to the old one +1. Old DB is instrumented with a changelog +1. An initial sync is performed of all data old -> new +1. Timetable is broadcast to begin switchover +1. Pause all DB queries +1. Replicate changes since last sync +1. Unpause and use new DB + +If at any time a node is introduced, config changes, or a deadline is exceeded: nodes will broadcast an abort event and resume normal operation. +All operations will end by the deadline and either the old DB or the new one (with all changes included) will be used by all nodes. + +## Switchover Mode + +When in switchover mode, GoAlert instances will operate with a wrapped DB driver that will determine +which DB to use for each connection that enters the pool. + +GoAlert starts in "Switchover Mode" when `--db-url-next` is set. + +### New Connections + +1. Connect to the _old_ DB +1. Acquire a shared advisory lock +1. Check `switchover_state` for `use_next_db` +1. If set, close and return connection to _new_ db +1. If **not** set, return current connection + +When the final sync is performed an exclusive advisory lock is acquired in the transaction. Since this conflicts with the shared lock, it ensures +the final sync is performed without any running queries on the old DB. When the transaction ends `switchover_state` is checked and old or new +will be used for all connections, depending on the success of the final sync. + +### Node/Instance States + +Viewed from logs or with the `status` command from the `switchover-shell`. + +| State | Description | +| -------------- | ---------------------------------------------------------------------------------------- | +| starting | Node has reset or is still starting up. | +| ready | Node is idle and ready for instructions. | +| armed | Node has recieved switchover timetable and is waiting for confirmation from other nodes. | +| armed-waiting | Node is waiting for the pause phase (all known nodes confirmed) | +| pausing | Node is waiting for the engine to finish pausing. | +| paused-waiting | Node is paused. Engine will not run, and idle connections are disabled. | +| complete | Normal operation resumed, next db is in use (`use_db_next` is set). | +| aborted | Something has triggered an abort. Node has resumed normal operation. | + +## Performing a Switchover + +To perform a switchover: + +1. Set/configure `--db-url-next` for all GoAlert instances +1. Run `goalert switchover-shell` with `--db-url` and `--db-url-next` set + +From the switchover shell: + +1. Run `reset` and wait for all nodes to be ready (use `status` or `status -w`) +1. Using `status` validate that there are no problems. You should see "No Problems Found" printed at the bottom, or a list with possible remediations. +1. Enable change tracking (for logical replication) with `enable` +1. Optionally run `sync` (it will be run as part of execute) +1. Run `execute` and confirm to perform the switchover +1. Configure all GoAlert instances to use the **new** `--db-url` and un-set `--db-url-next` + +The `execute` command will ask for confirmation of the proposed timetable: + +``` +Switch-Over Details + Pause API Requests: no # Pause API requests for the full duration, instead of just the final sync + Consensus Timeout : 3s # Deadline for all nodes to confirm they got the timetable and are ready + Pause Starts After: 5s # How long to wait before begining the pause + Pause Timeout : 10s # Max time to wait for all nodes to pause before aborting + Absolute Max Pause: 13s # The maximum possible pause time of the engine (and API requests if set above) + Avail. Sync Time : 1s - 11s # Indicates the possible final sync time alloted with this configuration + Max Alloted Time : 18s # Max time from begining to end of the switchover process + +Ready? + + Cancel + ❯ Go! +``` + +Shell commands also support `-h` for extra information and options. Use `CTRL+C` to cancel an operation (like `status -w` or `sync`) + +To completely reset in the event of an issue: + +1. Run `disable` to remove triggers +1. Run `reset-dest` to truncate all tables in `--db-url-next` +1. Run `reset` to reset all nodes + +If `execute` fails (e.g. due to a deadline) it should be safe to retry. diff --git a/switchover/dbsync/changelog.go b/switchover/dbsync/changelog.go new file mode 100644 index 0000000000..0abb33b5cf --- /dev/null +++ b/switchover/dbsync/changelog.go @@ -0,0 +1,188 @@ +package dbsync + +import ( + "context" + "fmt" + + "github.com/abiosoft/ishell" + "github.com/lib/pq" + "github.com/pkg/errors" + "github.com/vbauerster/mpb/v4" + "github.com/vbauerster/mpb/v4/decor" +) + +const ( + changeLogTableDel = `DROP TABLE IF EXISTS change_log` + changeLogTableDef = ` + CREATE TABLE change_log ( + id BIGSERIAL PRIMARY KEY, + op TEXT NOT NULL, + table_name TEXT NOT NULL, + row_id TEXT NOT NULL, + tx_id BIGINT, + cmd_id cid, + row_data JSONB + )` + + changeLogFuncDel = `DROP FUNCTION IF EXISTS fn_process_change_log()` + changeLogFuncDef = ` + CREATE OR REPLACE FUNCTION fn_process_change_log() RETURNS TRIGGER AS $$ + DECLARE + cur_state enum_switchover_state := 'idle'; + BEGIN + SELECT INTO cur_state current_state + FROM switchover_state; + + IF cur_state != 'in_progress' THEN + RETURN NEW; + END IF; + + IF (TG_OP = 'DELETE') THEN + INSERT INTO change_log (op, table_name, row_id, tx_id, cmd_id) + VALUES (TG_OP, TG_TABLE_NAME, cast(OLD.id as TEXT), txid_current(), OLD.cmax); + RETURN OLD; + ELSE + INSERT INTO change_log (op, table_name, row_id, tx_id, cmd_id, row_data) + VALUES (TG_OP, TG_TABLE_NAME, cast(NEW.id as TEXT), txid_current(), NEW.cmin, to_jsonb(NEW)); + RETURN NEW; + END IF; + + RETURN NULL; + END; + $$ LANGUAGE 'plpgsql'` +) + +func changeLogTrigName(tableName string) string { + return fmt.Sprintf("zz_99_change_log_%s", tableName) +} + +func changeLogTrigDel(tableName string) string { + return fmt.Sprintf(`DROP TRIGGER IF EXISTS %s ON %s`, pq.QuoteIdentifier(changeLogTrigName(tableName)), pq.QuoteIdentifier(tableName)) +} +func changeLogTrigDef(tableName string) string { + return fmt.Sprintf(` + CREATE TRIGGER %s + AFTER INSERT OR UPDATE OR DELETE ON %s + FOR EACH ROW EXECUTE PROCEDURE fn_process_change_log()`, + pq.QuoteIdentifier(changeLogTrigName(tableName)), pq.QuoteIdentifier(tableName)) +} + +// ChangeLogEnable will instrument the database for the sync operation. +func (s *Sync) ChangeLogEnable(ctx context.Context, sh *ishell.Context) error { + var stat string + err := s.oldDB.QueryRowContext(ctx, `select current_state from switchover_state`).Scan(&stat) + if err != nil { + return errors.Wrap(err, "lookup switchover state") + } + if stat != "idle" { + return errors.New("must be idle") + } + + run := func(name, stmt string) { + if err != nil { + return + } + _, err = s.oldDB.ExecContext(ctx, stmt) + err = errors.Wrap(err, name) + } + runNew := func(name, stmt string) { + if err != nil { + return + } + _, err = s.newDB.ExecContext(ctx, stmt) + err = errors.Wrap(err, name) + } + sh.Println("Resetting change log...") + runNew("configure dest change_log", changeLogTableDef) + run("clear change_log", changeLogTableDel) + run("configure change_log", changeLogTableDef) + run("define change hook", changeLogFuncDef) + run("create initial entry", `insert into change_log (op, table_name, row_id) values ('INIT', '', '')`) + + p := mpb.NewWithContext(ctx) + process := make([]Table, 0, len(s.tables)) + for _, t := range s.tables { + if contains(ignoreTriggerTables, t.Name) { + continue + } + process = append(process, t) + } + bar := p.AddBar(int64(len(process)), + mpb.BarClearOnComplete(), + mpb.PrependDecorators( + decor.OnComplete( + decor.StaticName("Adding triggers..."), + "Instrumented all tables.")), + ) + for _, t := range process { + run("clear prev. trigger for "+t.SafeName(), changeLogTrigDel(t.Name)) + run("set trigger for "+t.SafeName(), changeLogTrigDef(t.Name)) + bar.IncrBy(1) + } + p.Wait() + if err != nil { + return err + } + + sh.Println("Activating change tracking...") + res, err := s.oldDB.ExecContext(ctx, `update switchover_state set current_state = 'in_progress' where current_state = 'idle'`) + if err != nil { + return err + } + r, err := res.RowsAffected() + if err != nil { + return err + } + if r != 1 { + return errors.New("not idle") + } + + return nil +} + +// ChangeLogDisable will remove all sync instrumentation. +func (s *Sync) ChangeLogDisable(ctx context.Context, sh *ishell.Context) error { + res, err := s.oldDB.ExecContext(ctx, `update switchover_state set current_state = 'idle' where current_state = 'in_progress'`) + if err != nil { + return err + } + r, err := res.RowsAffected() + if err != nil { + return err + } + if r != 1 { + return errors.New("not in_progress") + } + + run := func(name, stmt string) { + if err != nil { + return + } + _, err = s.oldDB.ExecContext(ctx, stmt) + err = errors.Wrap(err, name) + } + + p := mpb.NewWithContext(ctx) + bar := p.AddBar(int64(len(s.tables)), + mpb.BarClearOnComplete(), + mpb.PrependDecorators( + decor.OnComplete( + decor.StaticName("Removing triggers..."), + "Removed all triggers."), + ), + ) + for _, t := range s.tables { + run("clear trigger for "+t.SafeName(), changeLogTrigDel(t.Name)) + bar.IncrBy(1) + } + p.Wait() + + sh.Println("Resetting change log...") + run("remove change hook", changeLogFuncDel) + run("remove change_log", changeLogTableDel) + if err != nil { + return err + } + + return nil +} diff --git a/switchover/dbsync/dbid.go b/switchover/dbsync/dbid.go new file mode 100644 index 0000000000..0d6c02ed7c --- /dev/null +++ b/switchover/dbsync/dbid.go @@ -0,0 +1,15 @@ +package dbsync + +import ( + "crypto/rand" + "encoding/hex" +) + +func newDBID() string { + b := make([]byte, 20) + _, err := rand.Read(b) + if err != nil { + panic(err) + } + return hex.EncodeToString(b) +} diff --git a/switchover/dbsync/diffsync.go b/switchover/dbsync/diffsync.go index b8707eefb3..adee87cecb 100644 --- a/switchover/dbsync/diffsync.go +++ b/switchover/dbsync/diffsync.go @@ -7,24 +7,12 @@ import ( "github.com/jackc/pgx" "github.com/pkg/errors" - "github.com/vbauerster/mpb" - "github.com/vbauerster/mpb/decor" + "github.com/vbauerster/mpb/v4" + "github.com/vbauerster/mpb/v4/decor" ) const batchSize = 100 -type decorElapsedDone time.Time - -func (d decorElapsedDone) Decor(stats *decor.Statistics) string { - if !stats.Completed { - return "" - } - return " in " + time.Since(time.Time(d)).String() -} -func (d decorElapsedDone) Syncable() (bool, chan int) { - return false, nil -} - func (s *Sync) diffSync(ctx context.Context, txSrc, txDst *pgx.Tx, dstChange int) error { start := time.Now() rows, err := txSrc.QueryEx(ctx, ` @@ -72,6 +60,8 @@ func (s *Sync) diffSync(ctx context.Context, txSrc, txDst *pgx.Tx, dstChange int name := c.OP + ":" + c.Table var query string switch c.OP { + case "INIT": + continue case "DELETE": query = s.table(c.Table).DeleteOneRow() case "INSERT": @@ -94,14 +84,14 @@ func (s *Sync) diffSync(ctx context.Context, txSrc, txDst *pgx.Tx, dstChange int } fmt.Println("Prepared statements in", time.Since(start)) - p := mpb.New() + p := mpb.NewWithContext(ctx) bar := p.AddBar(int64(len(changes)), mpb.BarClearOnComplete(), mpb.PrependDecorators( decor.CountersNoUnit("Synced %d of %d changes"), ), mpb.AppendDecorators( - decorElapsedDone(time.Now()), + decor.OnComplete(decor.Elapsed(decor.ET_STYLE_GO), ""), ), ) @@ -125,7 +115,7 @@ func (s *Sync) diffSync(ctx context.Context, txSrc, txDst *pgx.Tx, dstChange int } err = b.Close() if err != nil { - p.Abort(bar, false) + bar.Abort(false) p.Wait() fmt.Println("SYNC", c.ID) return err @@ -142,7 +132,7 @@ func (s *Sync) diffSync(ctx context.Context, txSrc, txDst *pgx.Tx, dstChange int } err = b.Close() if err != nil { - p.Abort(bar, false) + bar.Abort(false) p.Wait() return errors.Wrap(err, "sync") } diff --git a/switchover/dbsync/initsync.go b/switchover/dbsync/initsync.go index f44fbab984..f201e6d457 100644 --- a/switchover/dbsync/initsync.go +++ b/switchover/dbsync/initsync.go @@ -8,13 +8,17 @@ import ( "github.com/jackc/pgx" "github.com/pkg/errors" - "github.com/vbauerster/mpb" - "github.com/vbauerster/mpb/decor" + "github.com/vbauerster/mpb/v4" + "github.com/vbauerster/mpb/v4/decor" ) func (s *Sync) initialSync(ctx context.Context, txSrc, txDst *pgx.Tx) error { - p := mpb.New() - var err error + err := s.RefreshTables(ctx) + if err != nil { + return err + } + + p := mpb.NewWithContext(ctx) var totalRows int64 var bars []*mpb.Bar var toSync []Table @@ -26,10 +30,11 @@ func (s *Sync) initialSync(ctx context.Context, txSrc, txDst *pgx.Tx) error { ), ) for _, t := range s.tables { - var rowCount int64 err := txSrc.QueryRowEx(ctx, `select count(*) from `+t.SafeName(), nil).Scan(&rowCount) if err != nil { + scanBar.Abort(false) + p.Wait() return err } scanBar.Increment() @@ -56,9 +61,9 @@ func (s *Sync) initialSync(ctx context.Context, txSrc, txDst *pgx.Tx) error { ) abort := func(i int) { for ; i < len(toSync); i++ { - p.Abort(bars[i], false) + bars[i].Abort(false) } - p.Abort(tBar, false) + tBar.Abort(false) p.Wait() } @@ -69,7 +74,13 @@ func (s *Sync) initialSync(ctx context.Context, txSrc, txDst *pgx.Tx) error { pr, pw := io.Pipe() bw := bufio.NewWriter(pw) br := bufio.NewReader(pr) - errCh := make(chan error, 2) + errCh := make(chan error, 3) + go func() { + <-ctx.Done() + go pw.CloseWithError(ctx.Err()) + go pr.CloseWithError(ctx.Err()) + errCh <- ctx.Err() + }() go func() { defer pw.Close() defer bw.Flush() diff --git a/switchover/dbsync/listen.go b/switchover/dbsync/listen.go index 4a3baf1c32..5bab619d38 100644 --- a/switchover/dbsync/listen.go +++ b/switchover/dbsync/listen.go @@ -2,22 +2,23 @@ package dbsync import ( "context" + "database/sql" "fmt" - "github.com/target/goalert/switchover" "time" "github.com/jackc/pgx/stdlib" + "github.com/target/goalert/switchover" ) -func (s *Sync) listen() { +func (s *Sync) listen(db *sql.DB) { for { // ignoring errors (will reconnect) err := func() error { - c, err := stdlib.AcquireConn(s.oldDB) + c, err := stdlib.AcquireConn(db) if err != nil { return err } - defer stdlib.ReleaseConn(s.oldDB, c) + defer stdlib.ReleaseConn(db, c) err = c.Listen(switchover.StateChannel) if err != nil { diff --git a/switchover/dbsync/shell.go b/switchover/dbsync/shell.go index c5c8e89aa3..a608c99ee3 100644 --- a/switchover/dbsync/shell.go +++ b/switchover/dbsync/shell.go @@ -5,18 +5,18 @@ import ( "database/sql" "flag" "fmt" - "github.com/target/goalert/migrate" - "github.com/target/goalert/switchover" "net/url" "strings" "time" - "github.com/vbauerster/mpb" - "github.com/vbauerster/mpb/decor" - "github.com/abiosoft/ishell" _ "github.com/jackc/pgx/stdlib" // load PGX driver "github.com/pkg/errors" + "github.com/target/goalert/migrate" + "github.com/target/goalert/switchover" + "github.com/target/goalert/version" + "github.com/vbauerster/mpb/v4" + "github.com/vbauerster/mpb/v4/decor" ) // RunShell will start the switchover shell. @@ -27,7 +27,7 @@ func RunShell(oldURL, newURL string) error { return errors.Wrap(err, "parse old URL") } q := u.Query() - q.Set("application_name", "GoAlert Switch-Over Shell") + q.Set("application_name", fmt.Sprintf("GoAlert %s (S/O Shell)", version.GitVersion())) u.RawQuery = q.Encode() oldURL = u.String() @@ -36,7 +36,7 @@ func RunShell(oldURL, newURL string) error { return errors.Wrap(err, "parse new URL") } q = u.Query() - q.Set("application_name", "GoAlert Switch-Over Shell") + q.Set("application_name", fmt.Sprintf("GoAlert %s (S/O Shell)", version.GitVersion())) u.RawQuery = q.Encode() newURL = u.String() @@ -73,6 +73,10 @@ func RunShell(oldURL, newURL string) error { if err != nil { return errors.Wrap(err, "prepare notify statement") } + sendNotifNew, err := dbNew.PrepareContext(ctx, `select pg_notify($1, $2)`) + if err != nil { + return errors.Wrap(err, "prepare notify statement (next db)") + } s, err := NewSync(ctx, db, dbNew, newURL) if err != nil { @@ -130,17 +134,10 @@ func RunShell(oldURL, newURL string) error { Name: "enable", Help: "Enable change_log", Func: func(ctx context.Context, sh *ishell.Context) error { - res, err := db.ExecContext(ctx, `update switchover_state set current_state = 'in_progress' where current_state = 'idle'`) - if err != nil { - return err - } - r, err := res.RowsAffected() + err := s.ChangeLogEnable(ctx, sh) if err != nil { return err } - if r != 1 { - return errors.New("not idle") - } status, err := s.status(ctx) if err != nil { @@ -153,19 +150,64 @@ func RunShell(oldURL, newURL string) error { }, }) sh.AddCmd(ctxCmd{ - Name: "disable", - Help: "Enable change_log", + Name: "reset-dest", + Help: "Reset the destination DB (!deletes data!)", Func: func(ctx context.Context, sh *ishell.Context) error { - res, err := db.ExecContext(ctx, `update switchover_state set current_state = 'idle' where current_state = 'in_progress'`) + var stat string + err = s.oldDB.QueryRowContext(ctx, `select current_state from switchover_state`).Scan(&stat) if err != nil { - return err + return errors.Wrap(err, "lookup switchover state") + } + if stat != "idle" { + return errors.New("must be idle") + } + + p := mpb.NewWithContext(ctx) + process := make([]Table, 0, len(s.tables)) + for _, t := range s.tables { + if contains(ignoreSyncTables, t.Name) { + continue + } + process = append(process, t) + } + bar := p.AddBar(int64(len(process)), + mpb.BarClearOnComplete(), + mpb.PrependDecorators( + decor.OnComplete( + decor.StaticName("Truncating tables..."), + "Truncated all destination tables.")), + ) + for _, t := range process { + _, err = s.newDB.ExecContext(ctx, fmt.Sprintf("truncate table %s cascade", t.SafeName())) + if err != nil { + bar.Abort(false) + p.Wait() + return errors.Wrapf(err, "truncate %s", t.Name) + } + bar.IncrBy(1) + } + p.Wait() + _, err = s.newDB.ExecContext(ctx, "drop table if exists change_log") + if err != nil { + return errors.Wrap(err, "drop dest change_log") } - r, err := res.RowsAffected() + + status, err := s.status(ctx) if err != nil { return err } - if r != 1 { - return errors.New("not in_progress") + sh.Println(status) + sh.Println("change_log disabled") + return nil + }, + }) + sh.AddCmd(ctxCmd{ + Name: "disable", + Help: "Disable change_log", + Func: func(ctx context.Context, sh *ishell.Context) error { + err := s.ChangeLogDisable(ctx, sh) + if err != nil { + return err } status, err := s.status(ctx) @@ -187,10 +229,19 @@ func RunShell(oldURL, newURL string) error { delete(s.nodeStatus, key) } s.mx.Unlock() - _, err := sendNotif.ExecContext(ctx, switchover.ControlChannel, "reset") + + _, err = sendNotif.ExecContext(ctx, switchover.DBIDChannel, s.oldDBID) if err != nil { return err + } + _, err = sendNotifNew.ExecContext(ctx, switchover.DBIDChannel, s.newDBID) + if err != nil { + return err + } + _, err := sendNotif.ExecContext(ctx, switchover.ControlChannel, "reset") + if err != nil { + return err } status, err := s.status(ctx) @@ -249,15 +300,24 @@ func RunShell(oldURL, newURL string) error { HasFlags: true, Help: "Execute the switchover procedure.", Func: func(ctx context.Context, sh *ishell.Context) error { + var stat string + err = s.oldDB.QueryRowContext(ctx, `select current_state from switchover_state`).Scan(&stat) + if err != nil { + return errors.Wrap(err, "lookup switchover state") + } + if stat != "in_progress" { + return errors.New("must be in_progress") + } + cfg := switchover.DefaultConfig() fset := flag.NewFlagSet("execute", flag.ContinueOnError) - fset.BoolVar(&cfg.NoPauseAPI, "allow-api", cfg.NoPauseAPI, "Allow API requests during pause phase (DB calls will still pause during final sync).") + pauseAPI := fset.Bool("pause-api", !cfg.NoPauseAPI, "Pause API requests during pause phase (DB calls will still pause during final sync).") fset.DurationVar(&cfg.ConsensusTimeout, "consensus-timeout", cfg.ConsensusTimeout, "Timeout to reach consensus.") fset.DurationVar(&cfg.PauseDelay, "pause-delay", cfg.PauseDelay, "Delay from start until global pause begins.") fset.DurationVar(&cfg.PauseTimeout, "pause-timeout", cfg.PauseTimeout, "Timeout to achieve global pause.") fset.DurationVar(&cfg.MaxPause, "max-pause", cfg.MaxPause, "Maximum duration for any pause/delay/impact during switchover.") - extraSync := fset.Bool("extra-sync", false, "Do a second sync after pausing, immediately before the final sync (useful with -allow-api).") - noSwitch := fset.Bool("no-switch", false, "Run the entire procedure, but omit the final use_next_db update.") + noExtraSync := fset.Bool("no-extra-sync", false, "Skip the second sync after pausing (immediately before the final sync).") + noSwitch := fset.Bool("no-switch", false, "Run the entire procedure, but don't actually switch DB at the end.") err := fset.Parse(sh.Args) if err != nil { if err == flag.ErrHelp { @@ -265,6 +325,7 @@ func RunShell(oldURL, newURL string) error { } return err } + cfg.NoPauseAPI = !*pauseAPI status, err := s.status(ctx) if err != nil { @@ -273,18 +334,20 @@ func RunShell(oldURL, newURL string) error { details := new(strings.Builder) - pauseAPI := "yes" + pauseAPIStr := "yes" if cfg.NoPauseAPI { - pauseAPI = "no" + pauseAPIStr = "no" } + + maxSync := cfg.MaxPause - 2*time.Second fmt.Fprintln(details, status) fmt.Fprintln(details, "Switch-Over Details") - fmt.Fprintln(details, " Pause API Requests:", pauseAPI) + fmt.Fprintln(details, " Pause API Requests:", pauseAPIStr) fmt.Fprintln(details, " Consensus Timeout :", cfg.ConsensusTimeout) fmt.Fprintln(details, " Pause Starts After:", cfg.PauseDelay) fmt.Fprintln(details, " Pause Timeout :", cfg.PauseTimeout) fmt.Fprintln(details, " Absolute Max Pause:", cfg.MaxPause) - fmt.Fprintln(details, " Avail. Sync Time :", cfg.MaxPause-2*time.Second-cfg.PauseTimeout, "-", cfg.MaxPause-2*time.Second) + fmt.Fprintln(details, " Avail. Sync Time :", cfg.MaxPause-2*time.Second-cfg.PauseTimeout, "-", maxSync) fmt.Fprintln(details, " Max Alloted Time :", cfg.PauseDelay+cfg.MaxPause) fmt.Fprintln(details) fmt.Fprintln(details, "Ready?") @@ -294,12 +357,19 @@ func RunShell(oldURL, newURL string) error { return nil } - start := time.Now() - err = s.Sync(ctx, false, false) - if err != nil { - return err + for { + start := time.Now() + err = s.Sync(ctx, false, false) + if err != nil { + return err + } + dur := time.Since(start).Truncate(time.Second / 10) + sh.Printf("Completed sync in %s\n", dur.String()) + if dur < maxSync { + break + } + sh.Println("Took longer than max sync time, re-syncing") } - sh.Printf("Completed sync in %s\n", time.Since(start).Truncate(time.Second/10).String()) nodes := s.NodeStatus() n := len(nodes) @@ -312,7 +382,7 @@ func RunShell(oldURL, newURL string) error { } for _, stat := range nodes { - if !stat.MatchDBNext(newURL) { + if s.oldDBID != stat.DBID || s.newDBID != stat.DBNextID { return errors.New("one or more nodes (or this shell) have mismatched config, check db-url-next") } if stat.At.Before(time.Now().Add(-5 * time.Second)) { @@ -320,7 +390,7 @@ func RunShell(oldURL, newURL string) error { } } - p := mpb.New() + p := mpb.NewWithContext(ctx) var done bool abort := func() { if !done { @@ -357,7 +427,7 @@ func RunShell(oldURL, newURL string) error { defer cCancel() err = s.NodeStateWait(cCtx, n, cBar, switchover.StateArmed, switchover.StateArmWait) if err != nil { - p.Abort(cBar, false) + cBar.Abort(false) p.Wait() return errors.Wrap(err, "wait for consensus") } @@ -382,7 +452,7 @@ func RunShell(oldURL, newURL string) error { } } - p = mpb.New() + p = mpb.NewWithContext(ctx) pBar := p.AddBar(int64(n), mpb.PrependDecorators(decor.Name("STW Pause", decor.WCSyncSpaceR)), mpb.BarClearOnComplete(), @@ -394,14 +464,14 @@ func RunShell(oldURL, newURL string) error { defer pCancel() err = s.NodeStateWait(pCtx, n, pBar, switchover.StatePaused, switchover.StatePauseWait) if err != nil { - p.Abort(pBar, false) + pBar.Abort(false) p.Wait() return errors.Wrap(err, "wait for pause") } p.Wait() - if *extraSync { - start = time.Now() + if !*noExtraSync { + start := time.Now() err = s.Sync(ctx, false, false) if err != nil { return err diff --git a/switchover/dbsync/status.go b/switchover/dbsync/status.go index 498971ea7e..5f7d29bce7 100644 --- a/switchover/dbsync/status.go +++ b/switchover/dbsync/status.go @@ -9,11 +9,12 @@ import ( "github.com/alexeyco/simpletable" "github.com/pkg/errors" + "github.com/target/goalert/switchover" ) func (s *Sync) status(ctx context.Context) (string, error) { rows, err := s.oldDB.QueryContext(ctx, ` - select count(*), application_name, usename + select count(*), coalesce(application_name, ''), coalesce(usename, '') from pg_stat_activity where datname=current_database() group by application_name, usename @@ -31,6 +32,8 @@ func (s *Sync) status(ctx context.Context) (string, error) { {Text: "Connections"}, }, } + + var issues []string for rows.Next() { var num int var name string @@ -39,6 +42,9 @@ func (s *Sync) status(ctx context.Context) (string, error) { if err != nil { return "", errors.Wrap(err, "scan query results") } + if strings.Contains(name, "GoAlert") && !strings.Contains(name, "S/O") { + issues = append(issues, "Non-switchover GoAlert connection: "+name) + } table.Body.Cells = append(table.Body.Cells, []*simpletable.Cell{ {Text: name}, {Text: user}, @@ -61,22 +67,39 @@ func (s *Sync) status(ctx context.Context) (string, error) { }, } nodes := s.NodeStatus() + for _, stat := range nodes { cfg := "Valid" - if !stat.MatchDBNext(s.newURL) { + if s.oldDBID != stat.DBID || s.newDBID != stat.DBNextID { cfg = "Invalid" + issues = append(issues, fmt.Sprintf("Node[%s] has invalid config (try `reset` or checking db urls)", stat.NodeID)) + } + since := time.Since(stat.At).Truncate(time.Millisecond) + if since > 3*time.Second { + issues = append(issues, fmt.Sprintf("Node[%s] has not been seen for >3 seconds (try `reset`)", stat.NodeID)) } + + if stat.State == switchover.StateAbort { + issues = append(issues, fmt.Sprintf("Node[%s] has aborted (try `reset`)", stat.NodeID)) + } else if stat.State != switchover.StateReady { + issues = append(issues, fmt.Sprintf("Node[%s] is not ready", stat.NodeID)) + } + table.Body.Cells = append(table.Body.Cells, []*simpletable.Cell{ {Text: stat.NodeID}, {Text: string(stat.State)}, {Text: stat.Offset.String()}, {Text: cfg}, - {Text: time.Since(stat.At).Truncate(time.Millisecond).String()}, + {Text: since.String()}, {Text: strconv.Itoa(stat.ActiveRequests)}, }) } buf.WriteString(table.String() + "\n\n") + if len(nodes) == 0 { + issues = append(issues, "No nodes detected.") + } + fmt.Fprintf(buf, "Node Count: %d\n", len(nodes)) fmt.Fprintln(buf, "Local offset:", s.Offset()) var stat string @@ -86,6 +109,11 @@ func (s *Sync) status(ctx context.Context) (string, error) { } fmt.Fprintln(buf, "Switchover state:", stat) + if stat == "idle" { + return buf.String(), nil + } else if stat == "use_next_db" { + issues = append(issues, "Switchover has already been completed") + } var changeMax int err = s.oldDB.QueryRowContext(ctx, `select coalesce(max(id),0) from change_log`).Scan(&changeMax) @@ -100,5 +128,14 @@ func (s *Sync) status(ctx context.Context) (string, error) { } fmt.Fprintln(buf, "Max change_log ID (next DB):", changeMax) + if len(issues) > 0 { + fmt.Fprintln(buf, "\nPotential Problems Found:") + for _, s := range issues { + fmt.Fprintln(buf, "- "+s) + } + } else { + fmt.Fprintln(buf, "\nNo Problems Found") + } + return buf.String(), nil } diff --git a/switchover/dbsync/sync.go b/switchover/dbsync/sync.go index 938161695c..6d8473d229 100644 --- a/switchover/dbsync/sync.go +++ b/switchover/dbsync/sync.go @@ -5,8 +5,6 @@ import ( "context" "database/sql" "fmt" - "github.com/target/goalert/lock" - "github.com/target/goalert/switchover" "sort" "strconv" "sync" @@ -15,7 +13,9 @@ import ( "github.com/jackc/pgx" "github.com/jackc/pgx/stdlib" "github.com/pkg/errors" - "github.com/vbauerster/mpb" + "github.com/target/goalert/lock" + "github.com/target/goalert/switchover" + "github.com/vbauerster/mpb/v4" ) type Sync struct { @@ -27,14 +27,21 @@ type Sync struct { nodeStatus map[string]switchover.Status mx sync.Mutex statChange chan struct{} + + oldDBID, newDBID string } -func NewSync(ctx context.Context, oldDB, newDB *sql.DB, newURL string) (*Sync, error) { - tables, err := Tables(ctx, oldDB) +func (s *Sync) RefreshTables(ctx context.Context) error { + s.mx.Lock() + defer s.mx.Unlock() + t, err := Tables(ctx, s.oldDB) if err != nil { - return nil, err + return err } - + s.tables = t + return nil +} +func NewSync(ctx context.Context, oldDB, newDB *sql.DB, newURL string) (*Sync, error) { oldOffset, err := switchover.CalcDBOffset(ctx, oldDB) if err != nil { return nil, err @@ -48,14 +55,22 @@ func NewSync(ctx context.Context, oldDB, newDB *sql.DB, newURL string) (*Sync, e s := &Sync{ oldDB: oldDB, newDB: newDB, + oldDBID: newDBID(), + newDBID: newDBID(), newURL: newURL, - tables: tables, oldOffset: oldOffset, newOffset: newOffset, nodeStatus: make(map[string]switchover.Status), statChange: make(chan struct{}), } - go s.listen() + + err = s.RefreshTables(ctx) + if err != nil { + return nil, err + } + + go s.listen(s.oldDB) + go s.listen(s.newDB) return s, nil } @@ -279,7 +294,7 @@ func (s *Sync) Sync(ctx context.Context, isFinal, enableSwitchOver bool) error { if err != nil { return errors.Wrap(err, "disable triggers") } - fmt.Println("Disabled triggers in", time.Since(start)) + fmt.Println("Disabled destination triggers in", time.Since(start)) } if dstLastChange == 0 { diff --git a/switchover/dbsync/table.go b/switchover/dbsync/table.go index dd3ee3dd5b..07edf75156 100644 --- a/switchover/dbsync/table.go +++ b/switchover/dbsync/table.go @@ -11,11 +11,15 @@ import ( "github.com/pkg/errors" ) -var ignoreTables = []string{ - "switchover_state", - "engine_processing_versions", - "gorp_migrations", -} +var ( + ignoreSyncTables = []string{ + "switchover_state", + "engine_processing_versions", + "gorp_migrations", + } + + ignoreTriggerTables = append([]string{"change_log"}, ignoreSyncTables...) +) type Table struct { Name string @@ -63,7 +67,7 @@ func Tables(ctx context.Context, db *sql.DB) ([]Table, error) { if err != nil { return nil, err } - if contains(ignoreTables, name) { + if contains(ignoreSyncTables, name) { continue } tbl := t[name] diff --git a/switchover/deadlineconfig.go b/switchover/deadlineconfig.go index bc73223acd..2f431a0c28 100644 --- a/switchover/deadlineconfig.go +++ b/switchover/deadlineconfig.go @@ -29,6 +29,7 @@ func DefaultConfig() DeadlineConfig { PauseDelay: 5 * time.Second, PauseTimeout: 10 * time.Second, MaxPause: 13 * time.Second, + NoPauseAPI: true, } } diff --git a/switchover/handler.go b/switchover/handler.go index 17a47be033..881c9bb3cb 100644 --- a/switchover/handler.go +++ b/switchover/handler.go @@ -5,11 +5,12 @@ import ( "database/sql" "database/sql/driver" "fmt" + "io" + "sync" + "github.com/target/goalert/app/lifecycle" "github.com/target/goalert/lock" "github.com/target/goalert/util/log" - "io" - "sync" "github.com/lib/pq" "github.com/pkg/errors" @@ -21,6 +22,8 @@ type Handler struct { id string dbNextURL string + dbID, dbNextID string + sendNotification *sql.Stmt nodeStatus map[string]Status @@ -78,6 +81,10 @@ func NewHandler(ctx context.Context, oldC, newC driver.Connector, oldURL, newURL if err != nil { return nil, errors.Wrap(err, "init DB listener") } + err = h.initNewDBListen(newURL) + if err != nil { + return nil, errors.Wrap(err, "init DB-next listener") + } go h.loop() return h, nil @@ -94,10 +101,11 @@ func (h *Handler) Status() *Status { } func (h *Handler) status() *Status { return &Status{ - State: h.state, - NodeID: h.id, - Offset: h.old.timeOffset, - dbNext: dbNext(h.id, h.dbNextURL), + State: h.state, + NodeID: h.id, + Offset: h.old.timeOffset, + DBID: h.dbID, + DBNextID: h.dbNextID, } } diff --git a/switchover/mainloop.go b/switchover/mainloop.go index 6d68d5e57b..5df55f1a46 100644 --- a/switchover/mainloop.go +++ b/switchover/mainloop.go @@ -3,13 +3,13 @@ package switchover import ( "context" "fmt" - "github.com/target/goalert/app/lifecycle" - "github.com/target/goalert/util/log" "sort" "strings" "time" "github.com/pkg/errors" + "github.com/target/goalert/app/lifecycle" + "github.com/target/goalert/util/log" ) func (h *Handler) setState(ctx context.Context, newState State) { @@ -29,8 +29,11 @@ func (h *Handler) setState(ctx context.Context, newState State) { return } + h.mx.Lock() h.state = newState - _, err := h.sendNotification.ExecContext(ctx, StateChannel, h.status().serialize()) + h.mx.Unlock() + + _, err := h.sendNotification.ExecContext(ctx, StateChannel, h.Status().serialize()) if err != nil { log.Log(ctx, err) } @@ -51,9 +54,31 @@ func (h *Handler) allNodes(state ...State) bool { } return true } + +// CheckDBID will return true if the ID matches the current +// db-next ID. If there is no current ID, then it is set and returns true. +func (h *Handler) CheckDBID(id string) bool { + h.mx.Lock() + defer h.mx.Unlock() + + return h.dbID == id +} + +// CheckDBNextID will return true if the ID matches the current +// db-next ID. If there is no current ID, then it is set and returns true. +func (h *Handler) CheckDBNextID(id string) bool { + h.mx.Lock() + defer h.mx.Unlock() + + return h.dbNextID == id +} func (h *Handler) updateNodeStatus(ctx context.Context, s *Status) bool { - if !s.MatchDBNext(h.dbNextURL) { - log.Logf(ctx, "Switch-Over Abort: NodeID="+s.NodeID+" has mismatched db-next-url") + if !h.CheckDBID(s.DBID) { + log.Logf(ctx, "Switch-Over Abort: NodeID="+s.NodeID+" has mismatched DB ID") + h.setState(ctx, StateAbort) + } + if !h.CheckDBNextID(s.DBNextID) { + log.Logf(ctx, "Switch-Over Abort: NodeID="+s.NodeID+" has mismatched DB Next ID") h.setState(ctx, StateAbort) } @@ -166,7 +191,7 @@ func (h *Handler) loop() { continue } - _, err := h.sendNotification.ExecContext(ctx, StateChannel, h.status().serialize()) + _, err := h.sendNotification.ExecContext(ctx, StateChannel, h.Status().serialize()) if err != nil { log.Log(ctx, err) } diff --git a/switchover/notify.go b/switchover/notify.go index 3699d1892e..02d19e3e8b 100644 --- a/switchover/notify.go +++ b/switchover/notify.go @@ -2,26 +2,80 @@ package switchover import ( "context" - "github.com/target/goalert/util/log" + "fmt" "net/url" "time" "github.com/lib/pq" "github.com/pkg/errors" + "github.com/target/goalert/util/log" + "github.com/target/goalert/version" ) +// Postgres channel names const ( StateChannel = "goalert_switchover_state" ControlChannel = "goalert_switchover_control" + DBIDChannel = "goalert_switchover_db_id" ) +func (h *Handler) initNewDBListen(name string) error { + u, err := url.Parse(name) + if err != nil { + return errors.Wrap(err, "parse db URL") + } + q := u.Query() + q.Set("application_name", fmt.Sprintf("GoAlert %s (S/O Listener)", version.GitVersion())) + u.RawQuery = q.Encode() + name = u.String() + + l := pq.NewListener(name, 0, time.Second, h.listenEvent) + + err = l.Listen(DBIDChannel) + if err != nil { + l.Close() + return err + } + err = l.Listen(StateChannel) + if err != nil { + l.Close() + return err + } + + go func() { + for n := range l.NotificationChannel() { + if n == nil { + // nil can be sent, ignore + continue + } + switch n.Channel { + case DBIDChannel: + h.mx.Lock() + h.dbNextID = n.Extra + h.mx.Unlock() + case StateChannel: + s, err := ParseStatus(n.Extra) + if err != nil { + log.Log(context.Background(), errors.Wrap(err, "parse Status string")) + continue + } + if s.State == StateAbort { + go h.pushState(StateAbort) + } + h.statusCh <- s + } + } + }() + + return nil +} func (h *Handler) initListen(name string) error { u, err := url.Parse(name) if err != nil { return errors.Wrap(err, "parse db URL") } q := u.Query() - q.Set("application_name", "GoAlert Switch-Over Listener") + q.Set("application_name", fmt.Sprintf("GoAlert %s (S/O Listener)", version.GitVersion())) u.RawQuery = q.Encode() name = u.String() @@ -37,6 +91,11 @@ func (h *Handler) initListen(name string) error { h.l.Close() return err } + err = h.l.Listen(DBIDChannel) + if err != nil { + h.l.Close() + return err + } go h.listenLoop() return nil } @@ -53,6 +112,10 @@ func (h *Handler) listenLoop() { continue } switch n.Channel { + case DBIDChannel: + h.mx.Lock() + h.dbID = n.Extra + h.mx.Unlock() case StateChannel: s, err := ParseStatus(n.Extra) if err != nil { diff --git a/switchover/status.go b/switchover/status.go index 1636df11aa..362164fda9 100644 --- a/switchover/status.go +++ b/switchover/status.go @@ -1,9 +1,6 @@ package switchover import ( - "crypto/hmac" - "crypto/sha256" - "encoding/base64" "net/url" "strconv" "time" @@ -18,7 +15,8 @@ type Status struct { ActiveRequests int - dbNext []byte + DBID string + DBNextID string } func ParseStatus(str string) (*Status, error) { @@ -26,20 +24,17 @@ func ParseStatus(str string) (*Status, error) { if err != nil { return nil, err } - dbNext, err := base64.StdEncoding.DecodeString(v.Get("DBNext")) - if err != nil { - return nil, err - } reqs, err := strconv.Atoi(v.Get("ActiveRequests")) if err != nil { return nil, err } s := &Status{ - NodeID: v.Get("NodeID"), - State: State(v.Get("State")), - At: time.Now(), - dbNext: dbNext, + NodeID: v.Get("NodeID"), + State: State(v.Get("State")), + At: time.Now(), + DBID: v.Get("DBID"), + DBNextID: v.Get("DBNextID"), ActiveRequests: reqs, } @@ -51,31 +46,13 @@ func ParseStatus(str string) (*Status, error) { return s, nil } -func stripAppName(urlStr string) string { - u, err := url.Parse(urlStr) - if err != nil { - panic(err) - } - q := u.Query() - q.Del("application_name") - u.RawQuery = q.Encode() - return u.String() -} - -// MatchDBNext will return true if the Status indicates a -// matching db-next-url. -func (s Status) MatchDBNext(dbNextURL string) bool { - return hmac.Equal(s.dbNext, dbNext(s.NodeID, dbNextURL)) -} -func dbNext(id, url string) []byte { - return hmac.New(sha256.New, []byte(stripAppName(url))).Sum([]byte(id)) -} func (s Status) serialize() string { v := make(url.Values) v.Set("NodeID", s.NodeID) v.Set("State", string(s.State)) v.Set("Offset", s.Offset.String()) - v.Set("DBNext", base64.StdEncoding.EncodeToString(s.dbNext)) + v.Set("DBID", s.DBID) + v.Set("DBNextID", s.DBNextID) v.Set("ActiveRequests", strconv.Itoa(s.ActiveRequests)) return v.Encode() } diff --git a/version/version.go b/version/version.go new file mode 100644 index 0000000000..451d3c9229 --- /dev/null +++ b/version/version.go @@ -0,0 +1,26 @@ +package version + +import "time" + +var ( + gitVersion = "dev" + gitTreeState = "unknown" + gitCommit = "?" + + buildDate = "1970-01-01T00:00:00Z" +) + +// GitVersion will return the version of the current app binary. +func GitVersion() string { return gitVersion } + +// GitCommit will return the commit hash of the current app binary. +func GitCommit() string { return gitCommit } + +// GitTreeState will indicate the state of the working directory when the app was built. +func GitTreeState() string { return gitTreeState } + +// BuildDate returns a the timestamp of when the current binary was built. +func BuildDate() time.Time { + t, _ := time.Parse(time.RFC3339, buildDate) + return t +}