From e92201d97824c6da0592ff8fc7332ac487bf2c04 Mon Sep 17 00:00:00 2001 From: Morten Lied Johansen Date: Wed, 21 Aug 2024 14:51:01 +0200 Subject: [PATCH 1/5] Tell clients not to cache the response --- pkg/election/manager.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/election/manager.go b/pkg/election/manager.go index 0637b5c..ecd32c4 100644 --- a/pkg/election/manager.go +++ b/pkg/election/manager.go @@ -62,6 +62,7 @@ func (o *Official) Start(ctx context.Context) error { } w.Header().Set("Content-Type", "application/json") + w.Header().Set("Cache-Control", "no-cache") _, err = w.Write(bytes) if err != nil { o.Logger.Errorf("failed to write response: %v", err) From 9f70f5b2c7595b61b3dadf37ce917b9bd982cfee Mon Sep 17 00:00:00 2001 From: Morten Lied Johansen Date: Wed, 21 Aug 2024 20:10:20 +0200 Subject: [PATCH 2/5] Split election package in two: candidate and official --- cmd/elector/main.go | 7 +- pkg/election/{ => candidate}/candidate.go | 2 +- .../{ => candidate}/candidate_test.go | 2 +- pkg/election/manager.go | 94 ------------ pkg/election/official/official.go | 142 ++++++++++++++++++ 5 files changed, 148 insertions(+), 99 deletions(-) rename pkg/election/{ => candidate}/candidate.go (99%) rename pkg/election/{ => candidate}/candidate_test.go (99%) delete mode 100644 pkg/election/manager.go create mode 100644 pkg/election/official/official.go diff --git a/cmd/elector/main.go b/cmd/elector/main.go index 3bc2277..491543d 100644 --- a/cmd/elector/main.go +++ b/cmd/elector/main.go @@ -4,7 +4,8 @@ import ( "context" "fmt" "github.com/go-logr/logr" - "github.com/nais/elector/pkg/election" + "github.com/nais/elector/pkg/election/candidate" + "github.com/nais/elector/pkg/election/official" "github.com/nais/elector/pkg/logging" "k8s.io/apimachinery/pkg/types" "os" @@ -154,13 +155,13 @@ func main() { terminator := context.Background() electionResults := make(chan string) - err = election.AddCandidateToManager(mgr, logger, electionResults, electionName) + err = candidate.AddCandidateToManager(mgr, logger, electionResults, electionName) if err != nil { logger.Error(err) os.Exit(ExitCandidateAdded) } - err = election.AddOfficialToManager(mgr, logger, electionResults, viper.GetString(ElectionAddress)) + err = official.AddOfficialToManager(mgr, logger, electionResults, viper.GetString(ElectionAddress)) if err != nil { logger.Error(fmt.Errorf("failed to add election official to controller-runtime manager: %w", err)) os.Exit(ExitOfficialAdded) diff --git a/pkg/election/candidate.go b/pkg/election/candidate/candidate.go similarity index 99% rename from pkg/election/candidate.go rename to pkg/election/candidate/candidate.go index 9f5acc8..9bbf258 100644 --- a/pkg/election/candidate.go +++ b/pkg/election/candidate/candidate.go @@ -1,4 +1,4 @@ -package election +package candidate import ( "context" diff --git a/pkg/election/candidate_test.go b/pkg/election/candidate/candidate_test.go similarity index 99% rename from pkg/election/candidate_test.go rename to pkg/election/candidate/candidate_test.go index 26de972..7f975af 100644 --- a/pkg/election/candidate_test.go +++ b/pkg/election/candidate/candidate_test.go @@ -1,6 +1,6 @@ //go:build integration -package election +package candidate import ( "context" diff --git a/pkg/election/manager.go b/pkg/election/manager.go deleted file mode 100644 index ecd32c4..0000000 --- a/pkg/election/manager.go +++ /dev/null @@ -1,94 +0,0 @@ -package election - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "time" - - "github.com/sirupsen/logrus" - "sigs.k8s.io/controller-runtime/pkg/manager" - - "github.com/nais/elector/pkg/logging" -) - -type Official struct { - Logger logrus.FieldLogger - ElectionResults <-chan string - ElectionAddress string - lastResult result -} - -type result struct { - Name string `json:"name,omitempty"` - LastUpdate string `json:"last_update,omitempty"` -} - -func AddOfficialToManager(mgr manager.Manager, logger logrus.FieldLogger, electionResults <-chan string, electionAddress string) error { - official := Official{ - Logger: logger.WithField(logging.FieldComponent, "Manager"), - ElectionResults: electionResults, - ElectionAddress: electionAddress, - } - - err := mgr.AddReadyzCheck("official", official.readyz) - if err != nil { - return fmt.Errorf("failed to add official readiness check to controller-runtime manager: %w", err) - } - - err = mgr.Add(&official) - if err != nil { - return fmt.Errorf("failed to add official runnable to controller-runtime manager: %w", err) - } - - return nil -} - -func (o *Official) readyz(_ *http.Request) error { - if o.lastResult.Name == "" { - return fmt.Errorf("no election has run") - } - return nil -} - -func (o *Official) Start(ctx context.Context) error { - leaderHandler := func(w http.ResponseWriter, req *http.Request) { - bytes, err := json.Marshal(o.lastResult) - if err != nil { - o.Logger.Errorf("failed to marshal JSON response: %v", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Cache-Control", "no-cache") - _, err = w.Write(bytes) - if err != nil { - o.Logger.Errorf("failed to write response: %v", err) - return - } - } - - http.HandleFunc("/", leaderHandler) - ctx, cancel := context.WithCancel(ctx) - go func() { - o.Logger.Infof("Starting election service on %s", o.ElectionAddress) - err := http.ListenAndServe(o.ElectionAddress, nil) - o.Logger.Errorf("Failed to serve: %v", err) - cancel() - }() - - for { - select { - case <-ctx.Done(): - return ctx.Err() - case name := <-o.ElectionResults: - o.lastResult = result{ - Name: name, - LastUpdate: time.Now().Format(time.RFC3339), - } - o.Logger.Debugf("Updated election results. Current leader: %s", name) - } - } -} diff --git a/pkg/election/official/official.go b/pkg/election/official/official.go new file mode 100644 index 0000000..76db344 --- /dev/null +++ b/pkg/election/official/official.go @@ -0,0 +1,142 @@ +package official + +import ( + "context" + "encoding/json" + "fmt" + "github.com/nais/elector/pkg/logging" + "github.com/sirupsen/logrus" + "net/http" + "sigs.k8s.io/controller-runtime/pkg/manager" + "time" +) + +type official struct { + Logger logrus.FieldLogger + ElectionResults <-chan string + ElectionAddress string + lastResult result + sseSubscribers []chan<- result +} + +type result struct { + Name string `json:"name,omitempty"` + LastUpdate string `json:"last_update,omitempty"` +} + +func (o *official) readyz(_ *http.Request) error { + if o.lastResult.Name == "" { + return fmt.Errorf("no election has run") + } + return nil +} + +func (o *official) leaderHandler(w http.ResponseWriter, _ *http.Request) { + bytes, done := o.marshalResult(w, o.lastResult) + if done { + return + } + + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Cache-Control", "no-cache") + _, err := w.Write(bytes) + if err != nil { + o.Logger.Errorf("failed to write response: %v", err) + return + } +} + +func (o *official) marshalResult(w http.ResponseWriter, lastResult result) ([]byte, bool) { + bytes, err := json.Marshal(lastResult) + if err != nil { + o.Logger.Errorf("failed to marshal JSON response: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return nil, true + } + return bytes, false +} + +func (o *official) sseHandler(ctx context.Context) func(w http.ResponseWriter, _ *http.Request) { + return func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + + ch := make(chan result) + defer close(ch) + o.sseSubscribers = append(o.sseSubscribers, ch) + + bytes, done := o.marshalResult(w, o.lastResult) + if done { + return + } + + fmt.Fprintf(w, "data: %s\n\n", bytes) + w.(http.Flusher).Flush() + + for { + select { + case <-ctx.Done(): + return + case r := <-ch: + bytes, done = o.marshalResult(w, r) + if done { + return + } + fmt.Fprintf(w, "data: %s\n\n", bytes) + w.(http.Flusher).Flush() + } + } + } +} + +func (o *official) Start(ctx context.Context) error { + ctx, cancel := context.WithCancel(ctx) + + http.HandleFunc("/", o.leaderHandler) + http.HandleFunc("/sse", o.sseHandler(ctx)) + + go func() { + o.Logger.Infof("Starting election service on %s", o.ElectionAddress) + err := http.ListenAndServe(o.ElectionAddress, nil) + o.Logger.Errorf("Failed to serve: %v", err) + cancel() + }() + + for { + select { + case <-ctx.Done(): + return ctx.Err() + case name := <-o.ElectionResults: + o.lastResult = result{ + Name: name, + LastUpdate: time.Now().Format(time.RFC3339), + } + for _, ch := range o.sseSubscribers { + ch <- o.lastResult + } + o.Logger.Debugf("Updated election results. Current leader: %s", name) + } + } +} + +func AddOfficialToManager(mgr manager.Manager, logger logrus.FieldLogger, electionResults <-chan string, electionAddress string) error { + o := &official{ + Logger: logger.WithField(logging.FieldComponent, "Manager"), + ElectionResults: electionResults, + ElectionAddress: electionAddress, + sseSubscribers: make([]chan<- result, 0), + } + + err := mgr.AddReadyzCheck("official", o.readyz) + if err != nil { + return fmt.Errorf("failed to add official readiness check to controller-runtime manager: %w", err) + } + + err = mgr.Add(o) + if err != nil { + return fmt.Errorf("failed to add official runnable to controller-runtime manager: %w", err) + } + + return nil +} From b1fb1dafcdb9e18cdbce6616be06a907545dfb6a Mon Sep 17 00:00:00 2001 From: Morten Lied Johansen Date: Wed, 21 Aug 2024 21:18:32 +0200 Subject: [PATCH 3/5] Tests for official written using ginkgo Run all tests with ginkgo. The candidate test isn't really an integration test, it just uses envtest. --- Dockerfile | 8 +- Makefile | 9 +-- go.mod | 16 ++-- go.sum | 81 +++++++++++++++----- pkg/election/candidate/candidate_test.go | 2 - pkg/election/official/official.go | 4 + pkg/election/official/official_suite_test.go | 13 ++++ pkg/election/official/official_test.go | 75 ++++++++++++++++++ tools.go | 1 + 9 files changed, 170 insertions(+), 39 deletions(-) create mode 100644 pkg/election/official/official_suite_test.go create mode 100644 pkg/election/official/official_test.go diff --git a/Dockerfile b/Dockerfile index ccd575d..a6b94bd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build the manager binary -FROM --platform=$BUILDPLATFORM golang:1.23 as builder +FROM --platform=$BUILDPLATFORM golang:1.23 AS builder WORKDIR /workspace @@ -17,15 +17,9 @@ RUN go build std # Copy rest of project COPY . /workspace -# Download envtest tools early to avoid re-downloading on every code change -RUN make .envtest - # Run tests RUN make test -# Run integration tests -RUN make integration_test - # Build RUN CGO_ENABLED=0 go build -a -installsuffix cgo -o elector cmd/elector/main.go diff --git a/Makefile b/Makefile index cb90189..55b516f 100644 --- a/Makefile +++ b/Makefile @@ -1,16 +1,13 @@ -K8S_VERSION := 1.28.3 +K8S_VERSION := 1.29 SHELL := /bin/bash elector: go build -o bin/elector cmd/elector/*.go -test: - go test ./... -count=1 -coverprofile cover.out -short - .ONESHELL: -integration_test: .envtest +test: .envtest source .envtest - go test ./pkg/election/... -tags=integration -v -count=1 + go run github.com/onsi/ginkgo/v2/ginkgo run pkg/... .envtest: go run sigs.k8s.io/controller-runtime/tools/setup-envtest use -p env $(K8S_VERSION) > .envtest diff --git a/go.mod b/go.mod index eed69b2..23769b0 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,11 @@ go 1.22.0 toolchain go1.22.2 require ( + github.com/benjamintf1/unmarshalledmatchers v1.0.0 github.com/go-logr/logr v1.4.2 github.com/nais/liberator v0.0.0-20231114130128-a3a9edbe0da1 + github.com/onsi/ginkgo/v2 v2.20.0 + github.com/onsi/gomega v1.34.1 github.com/prometheus/client_golang v1.20.0 github.com/sirupsen/logrus v1.9.3 github.com/spf13/pflag v1.0.5 @@ -32,12 +35,14 @@ require ( github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect + github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/imdario/mergo v0.3.16 // indirect @@ -65,13 +70,14 @@ require ( github.com/x448/float16 v0.8.4 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect - golang.org/x/net v0.26.0 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect + golang.org/x/net v0.28.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect - golang.org/x/sys v0.22.0 // indirect - golang.org/x/term v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/sys v0.23.0 // indirect + golang.org/x/term v0.23.0 // indirect + golang.org/x/text v0.17.0 // indirect golang.org/x/time v0.5.0 // indirect + golang.org/x/tools v0.24.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index 3d26146..7500af5 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/benjamintf1/unmarshalledmatchers v1.0.0 h1:JUhctHQVNarMXg5x3m0Tkp7WnDLzNVxeWc1qbKQPylI= +github.com/benjamintf1/unmarshalledmatchers v1.0.0/go.mod h1:IVZdtAzpNyBTuhobduAjo5CjTLczWWbiXnWDVxIgSko= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -14,6 +16,8 @@ github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0 github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= @@ -28,29 +32,39 @@ github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2Rrd27c3VGxi6a/6HNq8QmHRKM= -github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= +github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k= +github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -82,10 +96,18 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/nais/liberator v0.0.0-20231114130128-a3a9edbe0da1 h1:xfSQH042X7sC8vUwZyjqhorb9rNbEHMwXROUpXfBLC4= github.com/nais/liberator v0.0.0-20231114130128-a3a9edbe0da1/go.mod h1:cWThp1WBBbkRFhMI2DQMvBTTEN+6GPzmmh+Xjv8vffE= -github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= -github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= -github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= -github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= +github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo/v2 v2.20.0 h1:PE84V2mHqoT1sglvHc8ZdQtPcwmvvt29WLEEO3xmdZw= +github.com/onsi/ginkgo/v2 v2.20.0/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -145,47 +167,63 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= -golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= +golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -193,11 +231,16 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/election/candidate/candidate_test.go b/pkg/election/candidate/candidate_test.go index 7f975af..60f835c 100644 --- a/pkg/election/candidate/candidate_test.go +++ b/pkg/election/candidate/candidate_test.go @@ -1,5 +1,3 @@ -//go:build integration - package candidate import ( diff --git a/pkg/election/official/official.go b/pkg/election/official/official.go index 76db344..60f25ad 100644 --- a/pkg/election/official/official.go +++ b/pkg/election/official/official.go @@ -103,6 +103,10 @@ func (o *official) Start(ctx context.Context) error { cancel() }() + return o.run(ctx) +} + +func (o *official) run(ctx context.Context) error { for { select { case <-ctx.Done(): diff --git a/pkg/election/official/official_suite_test.go b/pkg/election/official/official_suite_test.go new file mode 100644 index 0000000..c997f84 --- /dev/null +++ b/pkg/election/official/official_suite_test.go @@ -0,0 +1,13 @@ +package official_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestOfficial(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Official Suite") +} diff --git a/pkg/election/official/official_test.go b/pkg/election/official/official_test.go new file mode 100644 index 0000000..d46eff5 --- /dev/null +++ b/pkg/election/official/official_test.go @@ -0,0 +1,75 @@ +package official + +import ( + "context" + . "github.com/benjamintf1/unmarshalledmatchers" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/sirupsen/logrus" + "io" + "net/http/httptest" + "time" +) + +var _ = Describe("Official", func() { + var ctx context.Context + var o *official + var logger logrus.FieldLogger + var electionResults chan string + + BeforeEach(func() { + var cancel context.CancelFunc + ctx, cancel = context.WithCancel(context.Background()) + DeferCleanup(cancel) + + logger = logrus.New() + electionResults = make(chan string) + o = &official{ + Logger: logger, + ElectionResults: electionResults, + } + + go func() { + _ = o.run(ctx) + }() + }) + + Context("simple api", func() { + var w *httptest.ResponseRecorder + + BeforeEach(func() { + w = httptest.NewRecorder() + o.lastResult = result{ + Name: "last result", + LastUpdate: "then", + } + }) + + It("should return initial election result", func() { + o.leaderHandler(w, nil) + + res := w.Result() + defer res.Body.Close() + + Expect(res.StatusCode).To(Equal(200)) + Expect(res.Header.Get("Content-Type")).To(Equal("application/json")) + Expect(res.Header.Get("Cache-Control")).To(Equal("no-cache")) + Expect(io.ReadAll(res.Body)).To(MatchJSON(`{"name":"last result","last_update":"then"}`)) + }) + + It("should return election result update", func() { + electionResults <- "new result" + time.Sleep(10 * time.Millisecond) + + o.leaderHandler(w, nil) + + res := w.Result() + defer res.Body.Close() + + Expect(res.StatusCode).To(Equal(200)) + Expect(res.Header.Get("Content-Type")).To(Equal("application/json")) + Expect(res.Header.Get("Cache-Control")).To(Equal("no-cache")) + Expect(io.ReadAll(res.Body)).To(ContainUnorderedJSON(`{"name":"new result"}`)) + }) + }) +}) diff --git a/tools.go b/tools.go index c804139..a222888 100644 --- a/tools.go +++ b/tools.go @@ -4,5 +4,6 @@ package tools import ( + _ "github.com/onsi/ginkgo/v2/ginkgo" _ "sigs.k8s.io/controller-runtime/tools/setup-envtest" ) From 0d83db384b5157980fc8058255c396f49098bd67 Mon Sep 17 00:00:00 2001 From: Morten Lied Johansen Date: Wed, 21 Aug 2024 21:46:08 +0200 Subject: [PATCH 4/5] Tests for SSE API --- pkg/election/official/official_test.go | 52 ++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/pkg/election/official/official_test.go b/pkg/election/official/official_test.go index d46eff5..7e67bd2 100644 --- a/pkg/election/official/official_test.go +++ b/pkg/election/official/official_test.go @@ -2,6 +2,7 @@ package official import ( "context" + "fmt" . "github.com/benjamintf1/unmarshalledmatchers" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -72,4 +73,55 @@ var _ = Describe("Official", func() { Expect(io.ReadAll(res.Body)).To(ContainUnorderedJSON(`{"name":"new result"}`)) }) }) + + Context("sse api", func() { + var w *httptest.ResponseRecorder + + BeforeEach(func() { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, 100*time.Millisecond) + DeferCleanup(cancel) + + w = httptest.NewRecorder() + o.lastResult = result{ + Name: "last result", + LastUpdate: "then", + } + }) + + It("should return initial election result", func() { + go o.sseHandler(ctx)(w, nil) + time.Sleep(10 * time.Millisecond) + + line, err := w.Body.ReadString('\n') + Expect(err).ToNot(HaveOccurred()) + Expect(line).To(HavePrefix("data: ")) + data := line[6:] + Expect(data).To(MatchJSON(`{"name":"last result","last_update":"then"}`)) + }) + + It("should continue to update election results", func() { + go o.sseHandler(ctx)(w, nil) + time.Sleep(10 * time.Millisecond) + + line, err := w.Body.ReadString('\n') + Expect(err).ToNot(HaveOccurred()) + Expect(line).To(HavePrefix("data: ")) + data := line[6:] + Expect(data).To(MatchJSON(`{"name":"last result","last_update":"then"}`)) + Expect(w.Body.ReadString('\n')).To(Equal("\n")) + + for _, result := range []string{"first result", "second result", "third result"} { + electionResults <- result + time.Sleep(10 * time.Millisecond) + + line, err := w.Body.ReadString('\n') + Expect(err).ToNot(HaveOccurred()) + Expect(line).To(HavePrefix("data: ")) + data := line[6:] + Expect(data).To(ContainUnorderedJSON(fmt.Sprintf(`{"name":"%s"}`, result))) + Expect(w.Body.ReadString('\n')).To(Equal("\n")) + } + }) + }) }) From 094d7c009fb9dc71ca64a50167eb483ea6f3ba0f Mon Sep 17 00:00:00 2001 From: Morten Lied Johansen Date: Wed, 21 Aug 2024 21:53:25 +0200 Subject: [PATCH 5/5] Document new SSE API --- README.md | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index db088de..b1880f2 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,30 @@ Users should make sure to have alerts to detect when a leader is stuck. API --- -When running, elector can be queried on the election port to get the name of the current leader. +Elector has two API endpoints on the election port for getting information about the currently elected leader. +The endpoints return the same information, but one is a simple JSON object and the other is a [Server Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) stream. + +The object returned looks like this: + +```json +{ + "name": "pod-name", + "last_update": "timestamp of last update" +} +``` + +### Original API: `/` + +Simple GET with immediate return of the described object. + + +### SSE API: `/sse` + +The SSE API is a stream of server sent events that will send a message whenever there is an update. +Each event will be a JSON object as described above. + + +### Ports Default election port is 6060 (override with `--http`). Metrics are available on port 9090 (override with `--metrics-address`). @@ -26,8 +49,8 @@ Probes are available on port 8080 (override with `--probe-address`). Development ----------- -The integration tests uses envtest to simulate a kubernetes cluster. -Using the `integration_test` target in make will configure envtest for you before running the tests. +Some of the tests uses envtest to simulate a kubernetes cluster. +Using the `test` target in make will configure envtest for you before running the tests. If you would rather control the setup of envtest yourself, use the setup-envtest command to install and configure envtest.