From ae66c4f31f59ca2754396376443dfdd054ca5c25 Mon Sep 17 00:00:00 2001 From: Simon Pasquier Date: Wed, 21 Nov 2018 15:32:29 +0100 Subject: [PATCH 1/3] cmd/alertmanager: fix route prefix for the API v2 Signed-off-by: Simon Pasquier --- cmd/alertmanager/main.go | 70 ++++++++++++++++++++++++++-------------- 1 file changed, 45 insertions(+), 25 deletions(-) diff --git a/cmd/alertmanager/main.go b/cmd/alertmanager/main.go index b10c4306..dafb0ee7 100644 --- a/cmd/alertmanager/main.go +++ b/cmd/alertmanager/main.go @@ -129,6 +129,10 @@ func newMarkerMetrics(marker types.Marker) { const defaultClusterAddr = "0.0.0.0:9094" func main() { + os.Exit(run()) +} + +func run() int { if os.Getenv("DEBUG") != "" { runtime.SetBlockProfileRate(20) runtime.SetMutexProfileFraction(20) @@ -177,7 +181,7 @@ func main() { err := os.MkdirAll(*dataDir, 0777) if err != nil { level.Error(logger).Log("msg", "Unable to create data directory", "err", err) - os.Exit(1) + return 1 } var peer *cluster.Peer @@ -197,7 +201,7 @@ func main() { ) if err != nil { level.Error(logger).Log("msg", "unable to initialize gossip mesh", "err", err) - os.Exit(1) + return 1 } } @@ -216,7 +220,7 @@ func main() { notificationLog, err := nflog.New(notificationLogOpts...) if err != nil { level.Error(logger).Log("err", err) - os.Exit(1) + return 1 } if peer != nil { c := peer.AddState("nfl", notificationLog, prometheus.DefaultRegisterer) @@ -236,7 +240,7 @@ func main() { silences, err := silence.New(silenceOpts) if err != nil { level.Error(logger).Log("err", err) - os.Exit(1) + return 1 } if peer != nil { c := peer.AddState("sil", silences, prometheus.DefaultRegisterer) @@ -277,7 +281,7 @@ func main() { alerts, err := mem.NewAlerts(context.Background(), marker, *alertGCInterval, logger) if err != nil { level.Error(logger).Log("err", err) - os.Exit(1) + return 1 } defer alerts.Close() @@ -306,13 +310,13 @@ func main() { ) if err != nil { level.Error(logger).Log("err", fmt.Errorf("failed to create API v2: %v", err.Error())) - os.Exit(1) + return 1 } amURL, err := extURL(*listenAddress, *externalURL) if err != nil { level.Error(logger).Log("err", err) - os.Exit(1) + return 1 } waitFunc := func() time.Duration { return 0 } @@ -387,7 +391,7 @@ func main() { } if err := reload(); err != nil { - os.Exit(1) + return 1 } // Make routePrefix default to externalURL path if empty string. @@ -409,9 +413,30 @@ func main() { apiV1.Register(router.WithPrefix("/api/v1")) - // TODO: How about having a http.handler for each (web, apiv1, apiv2) and - // combine them all together in `listen()` - go listen(*listenAddress, router, apiV2.Handler, logger) + mux := http.NewServeMux() + mux.Handle("/", router) + + apiPrefix := "" + if *routePrefix != "/" { + apiPrefix = *routePrefix + } + mux.Handle(apiPrefix+"/api/v2/", http.StripPrefix(apiPrefix+"/api/v2", apiV2.Handler)) + + srv := http.Server{Addr: *listenAddress, Handler: mux} + srvc := make(chan struct{}) + + go func() { + level.Info(logger).Log("msg", "Listening", "address", *listenAddress) + if err := srv.ListenAndServe(); err != http.ErrServerClosed { + level.Error(logger).Log("msg", "Listen error", "err", err) + close(srvc) + } + defer func() { + if err := srv.Close(); err != nil { + level.Error(logger).Log("msg", "Error on closing the server", "err", err) + } + }() + }() var ( hup = make(chan os.Signal, 1) @@ -437,9 +462,15 @@ func main() { // Wait for reload or termination signals. close(hupReady) // Unblock SIGHUP handler. - <-term - - level.Info(logger).Log("msg", "Received SIGTERM, exiting gracefully...") + for { + select { + case <-term: + level.Info(logger).Log("msg", "Received SIGTERM, exiting gracefully...") + return 0 + case <-srvc: + return 1 + } + } } // clusterWait returns a function that inspects the current peer state and returns @@ -478,17 +509,6 @@ func extURL(listen, external string) (*url.URL, error) { return u, nil } -func listen(listen string, apiV1Handler *route.Router, apiV2Handler http.Handler, logger log.Logger) { - level.Info(logger).Log("msg", "Listening", "address", listen) - mux := http.NewServeMux() - mux.Handle("/", apiV1Handler) - mux.Handle("/api/v2/", http.StripPrefix("/api/v2", apiV2Handler)) - if err := http.ListenAndServe(listen, mux); err != nil { - level.Error(logger).Log("msg", "Listen error", "err", err) - os.Exit(1) - } -} - func md5HashAsMetricValue(data []byte) float64 { sum := md5.Sum(data) // We only want 48 bits as a float64 only has a 53 bit mantissa. From d6f8437b9b0683c59f5bacb43c120c3521cd3740 Mon Sep 17 00:00:00 2001 From: Simon Pasquier Date: Wed, 21 Nov 2018 17:22:35 +0100 Subject: [PATCH 2/3] test/with_api_v2: add test for route prefix Signed-off-by: Simon Pasquier --- test/with_api_v2/acceptance.go | 50 +++++++++++++++---------- test/with_api_v2/acceptance/web_test.go | 42 +++++++++++++++++++++ 2 files changed, 72 insertions(+), 20 deletions(-) create mode 100644 test/with_api_v2/acceptance/web_test.go diff --git a/test/with_api_v2/acceptance.go b/test/with_api_v2/acceptance.go index 786db77e..62f641f2 100644 --- a/test/with_api_v2/acceptance.go +++ b/test/with_api_v2/acceptance.go @@ -53,8 +53,9 @@ type AcceptanceTest struct { // AcceptanceOpts defines configuration paramters for an acceptance test. type AcceptanceOpts struct { - Tolerance time.Duration - baseTime time.Time + RoutePrefix string + Tolerance time.Duration + baseTime time.Time } func (opts *AcceptanceOpts) alertString(a *models.Alert) string { @@ -141,7 +142,7 @@ func (t *AcceptanceTest) AlertmanagerCluster(conf string, size int) *Alertmanage am.apiAddr = freeAddress() am.clusterAddr = freeAddress() - transport := httptransport.New(am.apiAddr, "/api/v2/", nil) + transport := httptransport.New(am.apiAddr, t.opts.RoutePrefix+"/api/v2/", nil) am.clientV2 = apiclient.New(transport, strfmt.Default) amc.ams = append(amc.ams, am) @@ -290,6 +291,7 @@ func (amc *AlertmanagerCluster) Start() error { // Start the alertmanager and wait until it is ready to receive. func (am *Alertmanager) Start(additionalArg []string) error { + am.t.Helper() args := []string{ "--config.file", am.confFile.Name(), "--log.level", "debug", @@ -298,6 +300,9 @@ func (am *Alertmanager) Start(additionalArg []string) error { "--cluster.listen-address", am.clusterAddr, "--cluster.settle-timeout", "0s", } + if am.opts.RoutePrefix != "" { + args = append(args, "--web.route-prefix", am.opts.RoutePrefix) + } args = append(args, additionalArg...) cmd := exec.Command("../../../alertmanager", args...) @@ -324,16 +329,20 @@ func (am *Alertmanager) Start(additionalArg []string) error { time.Sleep(50 * time.Millisecond) for i := 0; i < 10; i++ { - resp, err := http.Get(fmt.Sprintf("http://%s/status", am.apiAddr)) - if err == nil { - _, err := ioutil.ReadAll(resp.Body) - if err != nil { - return fmt.Errorf("starting alertmanager failed: %s", err) - } - resp.Body.Close() - return nil + resp, err := http.Get(am.getURL("/")) + if err != nil { + time.Sleep(500 * time.Millisecond) + continue } - time.Sleep(500 * time.Millisecond) + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("starting alertmanager failed: expected HTTP status '200', got '%d'", resp.StatusCode) + } + _, err = ioutil.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("starting alertmanager failed: %s", err) + } + return nil } return fmt.Errorf("starting alertmanager failed: timeout") } @@ -377,6 +386,7 @@ func (amc *AlertmanagerCluster) Terminate() { // Terminate kills the underlying Alertmanager process and remove intermediate // data. func (am *Alertmanager) Terminate() { + am.t.Helper() if err := syscall.Kill(am.cmd.Process.Pid, syscall.SIGTERM); err != nil { am.t.Fatalf("Error sending SIGTERM to Alertmanager process: %v", err) } @@ -391,18 +401,14 @@ func (amc *AlertmanagerCluster) Reload() { // Reload sends the reloading signal to the Alertmanager process. func (am *Alertmanager) Reload() { + am.t.Helper() if err := syscall.Kill(am.cmd.Process.Pid, syscall.SIGHUP); err != nil { am.t.Fatalf("Error sending SIGHUP to Alertmanager process: %v", err) } } -func (amc *AlertmanagerCluster) cleanup() { - for _, am := range amc.ams { - am.cleanup() - } -} - func (am *Alertmanager) cleanup() { + am.t.Helper() if err := os.RemoveAll(am.confFile.Name()); err != nil { am.t.Errorf("Error removing test config file %q: %v", am.confFile.Name(), err) } @@ -458,7 +464,7 @@ func (am *Alertmanager) SetSilence(at float64, sil *TestSilence) { return } - resp, err := http.Post(fmt.Sprintf("http://%s/api/v1/silences", am.apiAddr), "application/json", &buf) + resp, err := http.Post(am.getURL("/api/v1/silences"), "application/json", &buf) if err != nil { am.t.Errorf("Error setting silence %v: %s", sil, err) return @@ -494,7 +500,7 @@ func (amc *AlertmanagerCluster) DelSilence(at float64, sil *TestSilence) { // DelSilence deletes the silence with the sid at the given time. func (am *Alertmanager) DelSilence(at float64, sil *TestSilence) { am.t.Do(at, func() { - req, err := http.NewRequest("DELETE", fmt.Sprintf("http://%s/api/v1/silence/%s", am.apiAddr, sil.ID()), nil) + req, err := http.NewRequest("DELETE", am.getURL(fmt.Sprintf("/api/v1/silence/%s", sil.ID())), nil) if err != nil { am.t.Errorf("Error deleting silence %v: %s", sil, err) return @@ -541,3 +547,7 @@ func (amc *AlertmanagerCluster) GenericAPIV2Call(at float64, f func()) { func (am *Alertmanager) GenericAPIV2Call(at float64, f func()) { am.t.Do(at, f) } + +func (am *Alertmanager) getURL(path string) string { + return fmt.Sprintf("http://%s%s%s", am.apiAddr, am.opts.RoutePrefix, path) +} diff --git a/test/with_api_v2/acceptance/web_test.go b/test/with_api_v2/acceptance/web_test.go new file mode 100644 index 00000000..639c2e22 --- /dev/null +++ b/test/with_api_v2/acceptance/web_test.go @@ -0,0 +1,42 @@ +// Copyright 2018 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "testing" + + a "github.com/prometheus/alertmanager/test/with_api_v2" +) + +func TestWebWithPrefix(t *testing.T) { + t.Parallel() + + conf := ` +route: + receiver: "default" + group_by: [] + group_wait: 1s + group_interval: 1s + repeat_interval: 1h + +receivers: +- name: "default" +` + + // The test framework polls the API with the given prefix during + // Alertmanager startup and thereby ensures proper configuration. + at := a.NewAcceptanceTest(t, &a.AcceptanceOpts{RoutePrefix: "/foo"}) + at.AlertmanagerCluster(conf, 1) + at.Run() +} From 5fd944a60357003595e3689181550db5ab0aff10 Mon Sep 17 00:00:00 2001 From: Simon Pasquier Date: Wed, 21 Nov 2018 17:45:50 +0100 Subject: [PATCH 3/3] test/with_api_v1: add test for route prefix Signed-off-by: Simon Pasquier --- test/with_api_v1/acceptance.go | 46 ++++++++++++++++--------- test/with_api_v1/acceptance/web_test.go | 42 ++++++++++++++++++++++ 2 files changed, 72 insertions(+), 16 deletions(-) create mode 100644 test/with_api_v1/acceptance/web_test.go diff --git a/test/with_api_v1/acceptance.go b/test/with_api_v1/acceptance.go index 0d302155..97265ce7 100644 --- a/test/with_api_v1/acceptance.go +++ b/test/with_api_v1/acceptance.go @@ -50,8 +50,9 @@ type AcceptanceTest struct { // AcceptanceOpts defines configuration paramters for an acceptance test. type AcceptanceOpts struct { - Tolerance time.Duration - baseTime time.Time + RoutePrefix string + Tolerance time.Duration + baseTime time.Time } func (opts *AcceptanceOpts) alertString(a *model.Alert) string { @@ -135,7 +136,7 @@ func (t *AcceptanceTest) Alertmanager(conf string) *Alertmanager { t.Logf("AM on %s", am.apiAddr) c, err := api.NewClient(api.Config{ - Address: fmt.Sprintf("http://%s", am.apiAddr), + Address: am.getURL(""), }) if err != nil { t.Fatal(err) @@ -257,14 +258,18 @@ type Alertmanager struct { // Start the alertmanager and wait until it is ready to receive. func (am *Alertmanager) Start() { - cmd := exec.Command("../../../alertmanager", + args := []string{ "--config.file", am.confFile.Name(), "--log.level", "debug", "--web.listen-address", am.apiAddr, "--storage.path", am.dir, "--cluster.listen-address", am.clusterAddr, "--cluster.settle-timeout", "0s", - ) + } + if am.opts.RoutePrefix != "" { + args = append(args, "--web.route-prefix", am.opts.RoutePrefix) + } + cmd := exec.Command("../../../alertmanager", args...) if am.cmd == nil { var outb, errb buffer @@ -288,16 +293,21 @@ func (am *Alertmanager) Start() { time.Sleep(50 * time.Millisecond) for i := 0; i < 10; i++ { - resp, err := http.Get(fmt.Sprintf("http://%s/status", am.apiAddr)) - if err == nil { - _, err := ioutil.ReadAll(resp.Body) - if err != nil { - am.t.Fatalf("Starting alertmanager failed: %s", err) - } - resp.Body.Close() - return + resp, err := http.Get(am.getURL("/")) + if err != nil { + time.Sleep(500 * time.Millisecond) + continue } - time.Sleep(500 * time.Millisecond) + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + am.t.Fatalf("Starting alertmanager failed: expected HTTP status '200', got '%d'", resp.StatusCode) + } + _, err = ioutil.ReadAll(resp.Body) + if err != nil { + am.t.Fatalf("Starting alertmanager failed: %s", err) + } + resp.Body.Close() + return } am.t.Fatalf("Starting alertmanager failed: timeout") } @@ -363,7 +373,7 @@ func (am *Alertmanager) SetSilence(at float64, sil *TestSilence) { return } - resp, err := http.Post(fmt.Sprintf("http://%s/api/v1/silences", am.apiAddr), "application/json", &buf) + resp, err := http.Post(am.getURL("/api/v1/silences"), "application/json", &buf) if err != nil { am.t.Errorf("Error setting silence %v: %s", sil, err) return @@ -392,7 +402,7 @@ func (am *Alertmanager) SetSilence(at float64, sil *TestSilence) { // DelSilence deletes the silence with the sid at the given time. func (am *Alertmanager) DelSilence(at float64, sil *TestSilence) { am.t.Do(at, func() { - req, err := http.NewRequest("DELETE", fmt.Sprintf("http://%s/api/v1/silence/%s", am.apiAddr, sil.ID()), nil) + req, err := http.NewRequest("DELETE", am.getURL(fmt.Sprintf("/api/v1/silence/%s", sil.ID())), nil) if err != nil { am.t.Errorf("Error deleting silence %v: %s", sil, err) return @@ -418,3 +428,7 @@ func (am *Alertmanager) UpdateConfig(conf string) { return } } + +func (am *Alertmanager) getURL(path string) string { + return fmt.Sprintf("http://%s%s%s", am.apiAddr, am.opts.RoutePrefix, path) +} diff --git a/test/with_api_v1/acceptance/web_test.go b/test/with_api_v1/acceptance/web_test.go new file mode 100644 index 00000000..ff6cdbc3 --- /dev/null +++ b/test/with_api_v1/acceptance/web_test.go @@ -0,0 +1,42 @@ +// Copyright 2018 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "testing" + + a "github.com/prometheus/alertmanager/test/with_api_v1" +) + +func TestWebWithPrefix(t *testing.T) { + t.Parallel() + + conf := ` +route: + receiver: "default" + group_by: [] + group_wait: 1s + group_interval: 1s + repeat_interval: 1h + +receivers: +- name: "default" +` + + // The test framework polls the API with the given prefix during + // Alertmanager startup and thereby ensures proper configuration. + at := a.NewAcceptanceTest(t, &a.AcceptanceOpts{RoutePrefix: "/foo"}) + at.Alertmanager(conf) + at.Run() +}