Skip to content

Commit

Permalink
Merge pull request lawrencegripper#23 from gheibia/fb/22_healthcheck_…
Browse files Browse the repository at this point in the history
…credentials

Healthcheck credential parameters
  • Loading branch information
lawrencegripper authored Jan 14, 2019
2 parents bc72588 + fb15857 commit a7e73a9
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 13 deletions.
3 changes: 2 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
vendor
bin
bin
.vscode
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ traefik-appinsights-watchdog
debug
bin
**/debug.test
.vscode
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# build stage
FROM golang:1.9.2-alpine
FROM golang:1.11.4-alpine
ENV GOBIN /go/bin
RUN apk add --update --no-progress openssl git wget bash gcc musl-dev && \
rm -rf /var/cache/apk/* && \
Expand Down
24 changes: 21 additions & 3 deletions health/health.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package health
import (
"context"
"crypto/tls"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"time"

"github.com/lawrencegripper/traefik-appinsights-watchdog/types"
Expand All @@ -24,14 +26,14 @@ func StartCheck(ctx context.Context, config types.Configuration, healthChannel c
case <-ctx.Done():
return
default:
ev := getStatsEvent(config.TraefikHealthEndpoint, tlsConfig)
ev := getStatsEvent(config.TraefikHealthEndpoint, config.APIEndpointUsername, config.APIEndpointPassword, tlsConfig)
healthChannel <- ev
time.Sleep(intervalDuration)
}
}
}

func getStatsEvent(endpoint string, tlsConfig *tls.Config) types.StatsEvent {
func getStatsEvent(endpoint string, username string, password string, tlsConfig *tls.Config) types.StatsEvent {
event := types.StatsEvent{
Source: "HealthCheck",
SourceTime: time.Now(),
Expand All @@ -46,7 +48,18 @@ func getStatsEvent(endpoint string, tlsConfig *tls.Config) types.StatsEvent {
},
}

resp, err := client.Get(endpoint)
req, err := http.NewRequest("GET", endpoint, nil)
if err != nil {
event.IsSuccess = false
event.ErrorDetails = err.Error()
return event
}

// credentials are optional, but if provided, only username is mandatory
if len(strings.TrimSpace(username)) != 0 {
req.Header.Add("Authorization", "Basic "+basicAuth(username, password))
}
resp, err := client.Do(req)
elapsed := time.Since(start)
event.RequestDuration = elapsed
if err != nil {
Expand Down Expand Up @@ -76,3 +89,8 @@ func getStatsEvent(endpoint string, tlsConfig *tls.Config) types.StatsEvent {
event.Data = data
return event
}

func basicAuth(username, password string) string {
auth := username + ":" + password
return base64.StdEncoding.EncodeToString([]byte(auth))
}
93 changes: 85 additions & 8 deletions health/health_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package health

import (
"context"
"encoding/base64"
"errors"
"io/ioutil"
"log"
"net/http"
Expand All @@ -13,6 +15,11 @@ import (
"github.com/lawrencegripper/traefik-appinsights-watchdog/types"
)

const (
testUserName = "sp3cialUs3r"
testPassword = "Sup3rSecr3t"
)

func TestHealthRetreiveMetrics(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(handleHealthSuceed))
defer server.Close()
Expand Down Expand Up @@ -94,13 +101,48 @@ func TestHealthRetreiveMetrics_Timeout(t *testing.T) {
}
}

func TestHealthRetreiveMetrics_Authorized(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(handleHealthWithCredentials))
defer server.Close()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

config := types.Configuration{TraefikHealthEndpoint: server.URL + "/health", APIEndpointUsername: testUserName, APIEndpointPassword: testPassword}
channel := make(chan types.StatsEvent)

go StartCheck(ctx, config, channel)

timeout := time.After(time.Second * 3)

select {
case statEvent := <-channel:
if !statEvent.IsSuccess {
t.Error("Stats event was a failure")
}
t.Log(statEvent)
return
case <-timeout:
t.Error("Timeout occurred")
return
}
}

func handleHealthSuceed(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/health" {
http.NotFound(w, r)
return
}

body, err := ioutil.ReadFile("testdata/healthresponse_normal.json")
writeOkResponse(w)
}

func handleHealthInvalid(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/health" {
http.NotFound(w, r)
return
}

body, err := ioutil.ReadFile("testdata/healthresponse_invalid.json")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
_, err = w.Write([]byte(err.Error()))
Expand All @@ -117,13 +159,33 @@ func handleHealthSuceed(w http.ResponseWriter, r *http.Request) {
}
}

func handleHealthInvalid(w http.ResponseWriter, r *http.Request) {
func handleHealthTimeout(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/health" {
http.NotFound(w, r)
return
}

body, err := ioutil.ReadFile("testdata/healthresponse_invalid.json")
time.Sleep(time.Second * 5)
}

func handleHealthWithCredentials(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/health" {
http.NotFound(w, r)
return
}

err := authenticate(r)

if err != nil {
log.Fatal(err)
w.WriteHeader(http.StatusUnauthorized)
} else {
writeOkResponse(w)
}
}

func writeOkResponse(w http.ResponseWriter) {
body, err := ioutil.ReadFile("testdata/healthresponse_normal.json")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
_, err = w.Write([]byte(err.Error()))
Expand All @@ -140,11 +202,26 @@ func handleHealthInvalid(w http.ResponseWriter, r *http.Request) {
}
}

func handleHealthTimeout(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/health" {
http.NotFound(w, r)
return
func authenticate(r *http.Request) error {
auth := strings.SplitN(r.Header.Get("Authorization"), " ", 2)

if len(auth) != 2 || auth[0] != "Basic" {
return errors.New("Invalid or missing Authorization header")
}

time.Sleep(time.Second * 5)
payload, _ := base64.StdEncoding.DecodeString(auth[1])
pair := strings.SplitN(string(payload), ":", 2)

if len(pair) != 2 || !validate(pair[0], pair[1]) {
return errors.New("Invalid credentials")
}

return nil
}

func validate(username, password string) bool {
if username == testUserName && password == testPassword {
return true
}
return false
}
2 changes: 2 additions & 0 deletions types/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ type Configuration struct {
TraefikHealthEndpoint string `description:"The traeifk health endpoint http://localhost:port/health"`
PollIntervalSec int `description:"The time waited between requests to the health endpoint"`
AllowInvalidCert bool `description:"Allow invalid certificates when performing routing checks on localhost"`
APIEndpointUsername string `description:"Stores username required to call APIs including healthcheck"`
APIEndpointPassword string `description:"Stores password required to call APIs including healthcheck"`
}

0 comments on commit a7e73a9

Please sign in to comment.