Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Healthcheck credential parameters #23

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"`
}