Skip to content

Commit

Permalink
OSS release
Browse files Browse the repository at this point in the history
  • Loading branch information
opensourcepf authored and djboris9 committed Oct 4, 2018
0 parents commit fe752fc
Show file tree
Hide file tree
Showing 9 changed files with 467 additions and 0 deletions.
27 changes: 27 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Contributing
Thanks for contributing to this repository. Please read the following guidelines before you submit a pull request.

## Pull request and issues
If your commit does not change something fundamental, it is fine to create a pull request directly.
If you want to make a larger change or are unsure of the implication(s) of your change, please first create an issue, so that we can discuss it.

In all cases, please give the change a short and concise description.

## Commit messages
Every commit message must be meaningful.
Please do not write meaningless words or sentences like "fix", but be short and concise as in "fix divide by zero where altitude is zero in CalculateDistance()".
If the added code requires greater explanation, please document it in the code.

## Tests
If you introduce some new function or make a behavioural change, please create tests for the change.
However, it is better to write no tests than make nonsensical ones that convey a false sense of confidence.

## Code quality
Please check what other code in this repository looks like and adhere to language specific guidelines.
The code should be clear and any "magic" must be appropriately documented. Please rethink your work twice, as clear, safe and performant code will make you feel better.

## Security
Please try to ensure that your code has no vulnerabilities and document every possible security impact.

## Documentation
If reasonable, please document the changes either in the code itself or separately.
7 changes: 7 additions & 0 deletions LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Copyright 2018 PostFinance AG

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Kubenurse
kubenurse is a little service that monitors all network connections in a kubernetes
cluster and exports the taken metrics as prometheus endpoint.

## Project state
This project was written in only a few hours without receiving a polish but worked well.
Documentation and polish will come.

## Deployment
TODO

## Configuration
TODO

### SSL
The http client appends the certificate `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt` if found. You
can disable certificate validation with `KUBENURSE_INSECURE=true`.

## Alive Endpoint
The following json will be returned when accessing `http://0.0.0.0:8080/alive`:

```json
{
"api_server_direct": "ok",
"api_server_dns": "ok",
"me_ingress": "ok",
"me_service": "ok",
"hostname": "example.com",
"neighbourhood_state": "ok",
"neighbourhood" : [neighbours],
"headers": {http_request_headers}
}
```

if everything is alright it returns status code 200, else an 500.

## Health Checks
The checks are described in the follwing subsections

### api_server_direct
Checks if the `/version` of the Kubernetes API Server is available through
the direct link provided by the kubelet.

### api_server_dns
Checks if the `/version` of the Kubernetes API Server is available through
the Cluster DNS URL `https://kubernetes.default.svc:PORT`.

### me_ingress
Checks if itself is reachable at the `/alwayshappy` endpoint behind the ingress.
The address is provided by the env var `KUBENURSE_INGRESS_URL` which
could look like `https://kubenurse.example.com`

### me_service
Checks if it isself reachable at the `/alwayshappy` endpoint over the kubernetes service.
The address is provided by the env var `KUBENURSE_SERVICE_URL` which
could look like `http://kubenurse.kube-system.default.svc:8080`
136 changes: 136 additions & 0 deletions cmd/kubenurse/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package main

import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"strconv"
"time"

"github.com/postfinance/kubenurse/pkg/checker"
"github.com/postfinance/kubenurse/pkg/kubediscovery"
"github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
chk = &checker.Checker{}
)

const (
caFile = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
nurse = "I'm ready to help you!"
)

func main() {
// Setup http transport
transport, err := GenerateRoundTripper()
if err != nil {
log.Printf("using default transport: %s", err)
transport = http.DefaultTransport
}

client := &http.Client{
Timeout: 5 * time.Second,
Transport: transport,
}

// Setup checker
chk.KubenurseIngressUrl = os.Getenv("KUBENURSE_SERVICE_URL")
chk.KubenurseServiceUrl = os.Getenv("KUBENURSE_SERVICE_URL")
chk.KubernetesServiceHost = os.Getenv("KUBERNETES_SERVICE_HOST")
chk.KubernetesServicePort = os.Getenv("KUBERNETES_SERVICE_PORT")
chk.KubeNamespace = os.Getenv("KUBE_NAMESPACE")
chk.NeighbourFilter = os.Getenv("KUBENURSE_NEIGHBOUR_FILTER")
chk.HttpClient = client

// Setup http routes
http.HandleFunc("/alive", aliveHandler)
http.HandleFunc("/alwayshappy", func(http.ResponseWriter, *http.Request) {})
http.Handle("/metrics", promhttp.Handler())
http.Handle("/", http.RedirectHandler("/alive", http.StatusMovedPermanently))

fmt.Println(nurse) // most important line of this project

// Start listener and checker
go func() {
chk.RunScheduled(5 * time.Second)
log.Fatalln("checker exited")
}()

log.Fatal(http.ListenAndServe(":8080", nil))
}

func aliveHandler(w http.ResponseWriter, r *http.Request) {
type Output struct {
Hostname string `json:"hostname"`
Headers map[string][]string `json:"headers"`

// checker.Result
APIServerDirect string `json:"api_server_direct"`
APIServerDNS string `json:"api_server_dns"`
MeIngress string `json:"me_ingress"`
MeService string `json:"me_service"`

// kubediscovery
NeighbourhoodState string `json:"neighbourhood_state"`
Neighbourhood []kubediscovery.Neighbour `json:"neighbourhood"`
}

// Run checks now
res, haserr := chk.Run()
if haserr {
w.WriteHeader(http.StatusInternalServerError)
}

// Add additional data
out := Output{
APIServerDNS: res.APIServerDNS,
APIServerDirect: res.APIServerDirect,
MeIngress: res.MeIngress,
MeService: res.MeService,
Headers: r.Header,
Neighbourhood: res.Neighbourhood,
NeighbourhoodState: res.NeighbourhoodState,
}
out.Hostname, _ = os.Hostname()

// Generate output output
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
enc.Encode(out)

}

func GenerateRoundTripper() (http.RoundTripper, error) {
insecureEnv := os.Getenv("KUBENURSE_INSECURE")
insecure, _ := strconv.ParseBool(insecureEnv)

rootCAs, _ := x509.SystemCertPool()
if rootCAs == nil {
rootCAs = x509.NewCertPool()
}

caCert, err := ioutil.ReadFile(caFile)
if err != nil {
return nil, fmt.Errorf("could not load certificate %s: %s", caFile, err)
}

if ok := rootCAs.AppendCertsFromPEM(caCert); !ok {
return nil, errors.New("could not append ca cert to system certpool")
}

tlsConfig := &tls.Config{
InsecureSkipVerify: insecure,
RootCAs: rootCAs,
}

transport := &http.Transport{TLSClientConfig: tlsConfig}

return transport, nil
}
95 changes: 95 additions & 0 deletions pkg/checker/checker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package checker

import (
"fmt"
"log"
"time"

"github.com/postfinance/kubenurse/pkg/kubediscovery"
"github.com/postfinance/kubenurse/pkg/metrics"
)

func (c *Checker) Run() (Result, bool) {
var haserr bool
var err error

// Run Checks
res := Result{}

res.APIServerDirect, err = meassure(c.ApiServerDirect, "api_server_direct")
haserr = haserr || (err != nil)

res.APIServerDNS, err = meassure(c.ApiServerDNS, "api_server_dns")
haserr = haserr || (err != nil)

res.MeIngress, err = meassure(c.MeIngress, "me_ingress")
haserr = haserr || (err != nil)

res.MeService, err = meassure(c.MeService, "me_service")
haserr = haserr || (err != nil)

res.Neighbourhood, err = kubediscovery.GetNeighbourhood(c.KubeNamespace, c.NeighbourFilter)
haserr = haserr || (err != nil)

// Neighbourhood special error treating
if err != nil {
res.NeighbourhoodState = err.Error()
} else {
res.NeighbourhoodState = "ok"

// Check all neighbours
c.checkNeighbours(res.Neighbourhood)
}

return res, haserr
}

func (c *Checker) RunScheduled(d time.Duration) {
for range time.Tick(d) {
c.Run()
}
}

func (c *Checker) ApiServerDirect() (string, error) {
apiurl := fmt.Sprintf("https://%s:%s/version", c.KubernetesServiceHost, c.KubernetesServicePort)
return c.doRequest(apiurl)
}

func (c *Checker) ApiServerDNS() (string, error) {
apiurl := fmt.Sprintf("https://kubernetes.default.svc:%s/version", c.KubernetesServicePort)
return c.doRequest(apiurl)
}

func (c *Checker) MeIngress() (string, error) {
return c.doRequest(c.KubenurseIngressUrl + "/alwayshappy")
}

func (c *Checker) MeService() (string, error) {
return c.doRequest(c.KubenurseServiceUrl + "/alwayshappy")
}

func (c *Checker) checkNeighbours(nh []kubediscovery.Neighbour) {
for _, neighbour := range nh {
check := func() (string, error) {
return c.doRequest("http://" + neighbour.PodIP + ":8080/alwayshappy")
}

meassure(check, "path_"+neighbour.NodeName)
}
}

func meassure(check Check, label string) (string, error) {
start := time.Now()

// Execute check
res, err := check()

// Process metrics
metrics.DurationSummary.WithLabelValues(label).Observe(time.Since(start).Seconds())
if err != nil {
log.Printf("failed request for %s with %v", label, err)
metrics.ErrorCounter.WithLabelValues(label).Inc()
}

return res, err
}
22 changes: 22 additions & 0 deletions pkg/checker/transport.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package checker

import (
"errors"
"net/http"
)

func (c *Checker) doRequest(url string) (string, error) {
resp, err := c.HttpClient.Get(url)
if err != nil {
return err.Error(), err
}

// Body is non-nil if err is nil, so close it
resp.Body.Close()

if resp.StatusCode == http.StatusOK {
return "ok", nil
}

return resp.Status, errors.New(resp.Status)
}
35 changes: 35 additions & 0 deletions pkg/checker/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package checker

import (
"net/http"

"github.com/postfinance/kubenurse/pkg/kubediscovery"
)

type Checker struct {
// Ingress and service config
KubenurseIngressUrl string
KubenurseServiceUrl string

// Kubernetes API
KubernetesServiceHost string
KubernetesServicePort string

// Neighbourhood
KubeNamespace string
NeighbourFilter string

// Http Client for https requests
HttpClient *http.Client
}

type Result struct {
APIServerDirect string `json:"api_server_direct"`
APIServerDNS string `json:"api_server_dns"`
MeIngress string `json:"me_ingress"`
MeService string `json:"me_service"`
NeighbourhoodState string `json:"neighbourhood_state"`
Neighbourhood []kubediscovery.Neighbour `json:"neighbourhood"`
}

type Check func() (string, error)
Loading

0 comments on commit fe752fc

Please sign in to comment.