Skip to content
This repository was archived by the owner on May 10, 2024. It is now read-only.

Add health check endpoint #187

Merged
merged 2 commits into from
Feb 28, 2022
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: 3 additions & 0 deletions server-go/Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
all: build run

test_health:
curl -s -X GET 'http://localhost:8080/health'

test_login:
curl -s -X POST -d '{"username":"admin","password":"admin"}' -H 'Content-Type: application/json' 'http://localhost:8080/api/stable/auth/login'
@echo "Now do export TOKEN=..."
Expand Down
1 change: 1 addition & 0 deletions server-go/config/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ type ConfigurationProvider interface {
GetSingleDocumentAnyType(kind string, id string, apiGroup string, apiVersion string) (string, error)

StoreDocument(kind string, document interface{}) error
GetHealth() error
}
16 changes: 16 additions & 0 deletions server-go/config/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package config
import (
"context"
"github.com/fatih/structs"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
Expand All @@ -18,6 +19,21 @@ type ConfigurationInKubernetes struct {
apiVersion string
}

func (o *ConfigurationInKubernetes) GetHealth() error {
resources := []schema.GroupVersionResource{
{Group: o.apiGroup, Version: o.apiVersion, Resource: "backupusers"},
{Group: o.apiGroup, Version: o.apiVersion, Resource: "backupcollections"},
}

for _, resource := range resources {
if _, err := o.api.Resource(resource).Namespace(o.namespace).List(context.Background(), metav1.ListOptions{}); err != nil {
return errors.Wrapf(err, "cannot access Kubrenetes resources: '%v'", resource.String())
}
}

return nil
}

func (o *ConfigurationInKubernetes) GetSingleDocumentAnyType(kind string, id string, apiGroup string, apiVersion string) (string, error) {
resource := schema.GroupVersionResource{Group: apiGroup, Version: apiVersion, Resource: kind}
object, err := o.api.Resource(resource).Namespace(o.namespace).Get(context.Background(), id, metav1.GetOptions{})
Expand Down
3 changes: 3 additions & 0 deletions server-go/core/ctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@ import (
"github.com/riotkit-org/backup-repository/security"
"github.com/riotkit-org/backup-repository/storage"
"github.com/riotkit-org/backup-repository/users"
"gorm.io/gorm"
)

type ApplicationContainer struct {
Db *gorm.DB
Config *config.ConfigurationProvider
Users *users.Service
GrantedAccesses *security.Service
Collections *collections.Service
Storage *storage.Service
JwtSecretKey string
HealthCheckKey string
Locks *concurrency.LocksService
}
3 changes: 1 addition & 2 deletions server-go/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,4 @@ Interactions with server are done using HTTP API that talks JSON in both ways, a

### Collections

### Administrative

### [Administrative](api/administrative/README.md)
37 changes: 37 additions & 0 deletions server-go/docs/api/administrative/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
Administrative API endpoints
============================

## GET `/health``

**Example:**

```bash
curl -s -X GET 'http://localhost:8080/health'
```

**Example response (200):**

```json
{
"data": {
"health": [
{
"message": "OK",
"name": "DbValidator",
"status": true,
"statusText": "DbValidator=true"
},
{
"message": "OK",
"name": "StorageAvailabilityValidator",
"status": true,
"statusText": "StorageAvailabilityValidator=true"
}
]
},
"status": true
}
```

**Other responses:**
- [500](../common-responses.md)
6 changes: 3 additions & 3 deletions server-go/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ go 1.17
require (
github.com/appleboy/gin-jwt/v2 v2.8.0
github.com/fatih/structs v1.1.0
github.com/gin-contrib/timeout v0.0.3
github.com/gin-gonic/gin v1.7.7
github.com/google/uuid v1.3.0
github.com/jessevdk/go-flags v1.5.0
github.com/julianshen/gin-limiter v0.0.0-20161123033831-fc39b5e90fe7
github.com/labstack/gommon v0.3.1
github.com/pkg/errors v0.9.1
github.com/robfig/cron/v3 v3.0.1
github.com/sirupsen/logrus v1.8.1
github.com/stretchr/testify v1.7.0
Expand Down Expand Up @@ -38,7 +41,6 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-contrib/timeout v0.0.3 // indirect
github.com/go-logr/logr v1.2.0 // indirect
github.com/go-playground/locales v0.13.0 // indirect
github.com/go-playground/universal-translator v0.17.0 // indirect
Expand Down Expand Up @@ -66,13 +68,11 @@ require (
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/juju/ratelimit v1.0.1 // indirect
github.com/labstack/gommon v0.3.1 // indirect
github.com/leodido/go-urn v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-sqlite3 v1.14.9 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/tidwall/match v1.1.1 // indirect
Expand Down
13 changes: 7 additions & 6 deletions server-go/health/backupwindow.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ import (

type BackupWindowValidator struct {
svc *storage.Service
c *collections.Collection
}

func (v BackupWindowValidator) Validate(c *collections.Collection) error {
latest, err := v.svc.FindLatestVersion(c.GetId())
func (v BackupWindowValidator) Validate() error {
latest, err := v.svc.FindLatestVersion(v.c.GetId())
if err != nil {
return err
}
Expand All @@ -23,11 +24,11 @@ func (v BackupWindowValidator) Validate(c *collections.Collection) error {
now := time.Now()

// Backup Windows are optional
if len(c.Spec.Windows) == 0 {
if len(v.c.Spec.Windows) == 0 {
return nil
}

for _, window := range c.Spec.Windows {
for _, window := range v.c.Spec.Windows {
matches, err := window.IsInPreviousWindowTimeSlot(now, latest.CreatedAt)
if err != nil {
return errors.New(fmt.Sprintf("failed to calculate previous run for window '%v' - %v", window, err))
Expand All @@ -47,6 +48,6 @@ func (v BackupWindowValidator) Validate(c *collections.Collection) error {
return errors.Errorf("previous backup was not executed in expected time slots: %v", strings.Trim(allowedSlots, ", "))
}

func NewBackupWindowValidator(svc *storage.Service) BackupWindowValidator {
return BackupWindowValidator{svc}
func NewBackupWindowValidator(svc *storage.Service, c *collections.Collection) BackupWindowValidator {
return BackupWindowValidator{svc, c}
}
22 changes: 22 additions & 0 deletions server-go/health/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package health

import (
"github.com/pkg/errors"
"github.com/riotkit-org/backup-repository/config"
)

type ConfigurationProviderValidator struct {
cfg config.ConfigurationProvider
}

func (v ConfigurationProviderValidator) Validate() error {
if err := v.cfg.GetHealth(); err != nil {
return errors.Wrapf(err, "configuration provider is not usable")
}

return nil
}

func NewConfigurationProviderValidator(cfg config.ConfigurationProvider) ConfigurationProviderValidator {
return ConfigurationProviderValidator{cfg}
}
22 changes: 22 additions & 0 deletions server-go/health/db.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package health

import (
"github.com/pkg/errors"
"gorm.io/gorm"
)

type DbValidator struct {
db *gorm.DB
}

func (v DbValidator) Validate() error {
err := v.db.Raw("SELECT 1").Error
if err != nil {
return errors.Wrapf(err, "cannot connect to database")
}
return nil
}

func NewDbValidator(db *gorm.DB) DbValidator {
return DbValidator{db}
}
7 changes: 3 additions & 4 deletions server-go/health/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,19 @@ package health
import (
"encoding/json"
"fmt"
"github.com/riotkit-org/backup-repository/collections"
"reflect"
)

type Validator interface {
Validate(c *collections.Collection) error
Validate() error
}
type Validators []Validator

func (v Validators) Validate(c *collections.Collection) StatusCollection {
func (v Validators) Validate() StatusCollection {
var status StatusCollection

for _, validator := range v {
if err := validator.Validate(c); err != nil {
if err := validator.Validate(); err != nil {
status = append(status, Status{
Name: reflect.TypeOf(validator).Name(),
StatusMsg: err.Error(),
Expand Down
15 changes: 8 additions & 7 deletions server-go/health/size.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,18 @@ import (

type VersionsSizeValidator struct {
svc *storage.Service
c *collections.Collection
}

func (v VersionsSizeValidator) Validate(c *collections.Collection) error {
versions, err := v.svc.FindAllActiveVersionsFor(c.GetId())
func (v VersionsSizeValidator) Validate() error {
versions, err := v.svc.FindAllActiveVersionsFor(v.c.GetId())
if err != nil {
return errors.Wrapf(err, "Cannot list versions for collection id=%v", c.GetId())
return errors.Wrapf(err, "Cannot list versions for collection id=%v", v.c.GetId())
}

maxVersionSize, err := c.GetMaxOneVersionSizeInBytes()
maxVersionSize, err := v.c.GetMaxOneVersionSizeInBytes()
if err != nil {
return errors.Wrapf(err, "Cannot list versions for collection id=%v", c.GetId())
return errors.Wrapf(err, "Cannot list versions for collection id=%v", v.c.GetId())
}

for _, v := range versions {
Expand All @@ -30,6 +31,6 @@ func (v VersionsSizeValidator) Validate(c *collections.Collection) error {
return nil
}

func NewVersionsSizeValidator(svc *storage.Service) VersionsSizeValidator {
return VersionsSizeValidator{svc}
func NewVersionsSizeValidator(svc *storage.Service, c *collections.Collection) VersionsSizeValidator {
return VersionsSizeValidator{svc, c}
}
23 changes: 23 additions & 0 deletions server-go/health/storage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package health

import (
"github.com/pkg/errors"
"github.com/riotkit-org/backup-repository/storage"
)

type StorageAvailabilityValidator struct {
storage *storage.Service
}

func (v StorageAvailabilityValidator) Validate() error {
err := v.storage.TestReadWrite()
if err != nil {
return errors.Wrapf(err, "storage not operable")
}

return nil
}

func NewStorageValidator(storage *storage.Service) StorageAvailabilityValidator {
return StorageAvailabilityValidator{storage}
}
11 changes: 6 additions & 5 deletions server-go/health/sumofversions.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,18 @@ import (

type SumOfVersionsValidator struct {
svc *storage.Service
c *collections.Collection
}

func (v SumOfVersionsValidator) Validate(c *collections.Collection) error {
func (v SumOfVersionsValidator) Validate() error {
var totalSize int64
allActive, _ := v.svc.FindAllActiveVersionsFor(c.GetId())
allActive, _ := v.svc.FindAllActiveVersionsFor(v.c.GetId())

for _, version := range allActive {
totalSize += version.Filesize
}

maxCollectionSize, _ := c.GetCollectionMaxSize()
maxCollectionSize, _ := v.c.GetCollectionMaxSize()

if totalSize > maxCollectionSize {
return errors.Errorf("Summary of all files is %vb, while collection hard limit is %vb", totalSize, maxCollectionSize)
Expand All @@ -27,6 +28,6 @@ func (v SumOfVersionsValidator) Validate(c *collections.Collection) error {
return nil
}

func NewSumOfVersionsValidator(svc *storage.Service) SumOfVersionsValidator {
return SumOfVersionsValidator{svc}
func NewSumOfVersionsValidator(svc *storage.Service, c *collections.Collection) SumOfVersionsValidator {
return SumOfVersionsValidator{svc, c}
}
8 changes: 4 additions & 4 deletions server-go/http/collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,10 +202,10 @@ func addCollectionHealthRoute(r *gin.Engine, ctx *core.ApplicationContainer, rat

// Run all the checks
healthStatuses := health.Validators{
health.NewBackupWindowValidator(ctx.Storage),
health.NewVersionsSizeValidator(ctx.Storage),
health.NewSumOfVersionsValidator(ctx.Storage),
}.Validate(collection)
health.NewBackupWindowValidator(ctx.Storage, collection),
health.NewVersionsSizeValidator(ctx.Storage, collection),
health.NewSumOfVersionsValidator(ctx.Storage, collection),
}.Validate()

if !healthStatuses.GetOverallStatus() {
ServerErrorResponseWithData(c, errors.New("one of checks failed"), gin.H{
Expand Down
39 changes: 39 additions & 0 deletions server-go/http/health.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package http

import (
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
"github.com/riotkit-org/backup-repository/core"
"github.com/riotkit-org/backup-repository/health"
)

func addServerHealthEndpoint(r *gin.Engine, ctx *core.ApplicationContainer, rateLimiter gin.HandlerFunc) {
r.GET("/health", rateLimiter, func(c *gin.Context) {
// Authorization
healthCode := c.GetHeader("Authorization")
if healthCode == "" {
healthCode = c.Query("code")
}
if healthCode != ctx.HealthCheckKey {
UnauthorizedResponse(c, errors.New("health code invalid. Should be provided withing 'Authorization' header or 'code' query string. Must match --health-check-code commandline switch value"))
return
}

healthStatuses := health.Validators{
health.NewDbValidator(ctx.Db),
health.NewStorageValidator(ctx.Storage),
health.NewConfigurationProviderValidator(*ctx.Config),
}.Validate()

if !healthStatuses.GetOverallStatus() {
ServerErrorResponseWithData(c, errors.New("one of checks failed"), gin.H{
"health": healthStatuses,
})
return
}

OKResponse(c, gin.H{
"health": healthStatuses,
})
})
}
Loading