Prepare API

This commit is contained in:
Peter 2022-05-13 15:38:19 +02:00
parent c85c4f3512
commit 81b65b42b6
Signed by: prskr
GPG key ID: C1DB5D2E8DB512F9
24 changed files with 307 additions and 22 deletions

35
api/check_handler.go Normal file
View file

@ -0,0 +1,35 @@
package api
import (
"context"
"net/http"
"time"
"github.com/baez90/nurse/check"
)
var _ http.Handler = (*CheckHandler)(nil)
type CheckHandler struct {
Timeout time.Duration
Check check.SystemChecker
}
func (c CheckHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
var (
ctx = request.Context()
cancel context.CancelFunc
)
if c.Timeout != 0 {
ctx, cancel = context.WithTimeout(ctx, c.Timeout)
defer cancel()
}
if err := c.Check.Execute(ctx); err != nil {
writer.WriteHeader(http.StatusServiceUnavailable)
_, _ = writer.Write([]byte(err.Error()))
return
}
writer.WriteHeader(200)
return
}

31
api/mux.go Normal file
View file

@ -0,0 +1,31 @@
package api
import (
"net/http"
"go.uber.org/zap"
"github.com/baez90/nurse/check"
"github.com/baez90/nurse/config"
)
func PrepareMux(instance *config.Nurse, modLookup check.ModuleLookup, srvLookup config.ServerLookup) (http.Handler, error) {
mux := http.NewServeMux()
logger := zap.L()
for route, spec := range instance.Endpoints {
logger.Info("Configuring route", zap.String("route", route.String()))
chk, err := check.CheckForScript(spec.Checks, modLookup, srvLookup)
if err != nil {
return nil, err
}
mux.Handle(route.String(), CheckHandler{
Timeout: spec.Timeout(instance.CheckTimeout),
Check: chk,
})
}
return mux, nil
}

View file

@ -26,4 +26,12 @@ type (
CallUnmarshaler interface {
UnmarshalCall(c grammar.Call) error
}
CheckerLookup interface {
Lookup(c grammar.Check, srvLookup config.ServerLookup) (SystemChecker, error)
}
ModuleLookup interface {
Lookup(modName string) (CheckerLookup, error)
}
)

31
check/collection.go Normal file
View file

@ -0,0 +1,31 @@
package check
import (
"context"
"golang.org/x/sync/errgroup"
"github.com/baez90/nurse/config"
"github.com/baez90/nurse/grammar"
)
var _ SystemChecker = (Collection)(nil)
type Collection []SystemChecker
func (Collection) UnmarshalCheck(grammar.Check, config.ServerLookup) error {
panic("unmarshalling is not supported for a collection")
}
func (c Collection) Execute(ctx context.Context) error {
grp, grpCtx := errgroup.WithContext(ctx)
for i := range c {
chk := c[i]
grp.Go(func() error {
return chk.Execute(grpCtx)
})
}
return grp.Wait()
}

27
check/endpoint.go Normal file
View file

@ -0,0 +1,27 @@
package check
import (
"github.com/baez90/nurse/config"
"github.com/baez90/nurse/grammar"
)
func CheckForScript(script []grammar.Check, lkp ModuleLookup, srvLookup config.ServerLookup) (SystemChecker, error) {
compiledChecks := make([]SystemChecker, 0, len(script))
for i := range script {
rawChk := script[i]
mod, err := lkp.Lookup(rawChk.Initiator.Module)
if err != nil {
return nil, err
}
compiledCheck, err := mod.Lookup(rawChk, srvLookup)
if err != nil {
return nil, err
}
compiledChecks = append(compiledChecks, compiledCheck)
}
return Collection(compiledChecks), nil
}

View file

@ -37,8 +37,9 @@ func WithCheck(name string, factory Factory) ModuleOption {
})
}
func NewModule(opts ...ModuleOption) (*Module, error) {
func NewModule(name string, opts ...ModuleOption) (*Module, error) {
m := &Module{
name: name,
knownChecks: make(map[string]Factory),
}
@ -52,10 +53,15 @@ func NewModule(opts ...ModuleOption) (*Module, error) {
}
type Module struct {
name string
lock sync.RWMutex
knownChecks map[string]Factory
}
func (m *Module) Name() string {
return m.name
}
func (m *Module) Lookup(c grammar.Check, srvLookup config.ServerLookup) (SystemChecker, error) {
m.lock.RLock()
defer m.lock.RUnlock()

53
check/registry.go Normal file
View file

@ -0,0 +1,53 @@
package check
import (
"errors"
"fmt"
"strings"
"sync"
)
var (
ErrModuleNameConflict = errors.New("module name conflict")
ErrNoSuchModule = errors.New("no module of given name known")
)
func NewRegistry() *Registry {
return &Registry{
mods: make(map[string]*Module),
}
}
type (
Registry struct {
lock sync.RWMutex
mods map[string]*Module
}
)
func (r *Registry) Register(module *Module) error {
r.lock.Lock()
defer r.lock.Unlock()
modName := strings.ToLower(module.Name())
if _, ok := r.mods[modName]; ok {
return fmt.Errorf("%w: %s", ErrModuleNameConflict, modName)
}
r.mods[modName] = module
return nil
}
func (r *Registry) Lookup(modName string) (CheckerLookup, error) {
r.lock.RLock()
defer r.lock.RUnlock()
modName = strings.ToLower(modName)
if mod, ok := r.mods[modName]; !ok {
return nil, fmt.Errorf("%w: %s", ErrNoSuchModule, modName)
} else {
return mod, nil
}
}

View file

@ -38,6 +38,18 @@ type Nurse struct {
CheckTimeout time.Duration
}
func (n Nurse) ServerLookup() (*ServerRegister, error) {
register := NewServerRegister()
for name, srv := range n.Servers {
if err := register.Register(name, srv); err != nil {
return nil, err
}
}
return register, nil
}
// Merge merges the current Nurse instance with another one
// giving the current instance precedence means no set value is overwritten
func (n Nurse) Merge(other Nurse) Nurse {

View file

@ -1,7 +1,6 @@
package config
import (
"encoding"
"fmt"
"path"
"strings"
@ -10,8 +9,6 @@ import (
"github.com/baez90/nurse/grammar"
)
var _ encoding.TextUnmarshaler = (*EndpointSpec)(nil)
type Route string
func (r Route) String() string {
@ -26,17 +23,25 @@ type EndpointSpec struct {
Checks []grammar.Check
}
func (e *EndpointSpec) UnmarshalText(text []byte) error {
func (s EndpointSpec) Timeout(fallback time.Duration) time.Duration {
if s.CheckTimeout != 0 {
return s.CheckTimeout
}
return fallback
}
func (s *EndpointSpec) Parse(text string) error {
parser, err := grammar.NewParser[grammar.Script]()
if err != nil {
return err
}
script, err := parser.Parse(string(text))
script, err := parser.Parse(text)
if err != nil {
return err
}
e.Checks = script.Checks
s.Checks = script.Checks
return nil
}

View file

@ -50,7 +50,7 @@ func EndpointsFromEnv() (map[Route]EndpointSpec, error) {
endpointRoute := path.Join(Split(ToLower(Trim(Replace(key, EndpointKeyPrefix, "", -1), "_")), "_")...)
spec := EndpointSpec{}
if err := spec.UnmarshalText([]byte(value)); err != nil {
if err := spec.Parse(value); err != nil {
return nil, err
}

View file

@ -95,7 +95,7 @@ func TestEndpointsFromEnv(t *testing.T) {
},
want: td.Map(make(map[config.Route]config.EndpointSpec), td.MapEntries{
config.Route("readiness"): td.Struct(config.EndpointSpec{}, td.StructFields{
"Checks": td.Len(1),
"Script": td.Len(1),
}),
}),
wantErr: false,
@ -107,7 +107,7 @@ func TestEndpointsFromEnv(t *testing.T) {
},
want: td.Map(make(map[config.Route]config.EndpointSpec), td.MapEntries{
config.Route("readiness"): td.Struct(config.EndpointSpec{}, td.StructFields{
"Checks": td.Len(2),
"Script": td.Len(2),
}),
}),
wantErr: false,
@ -119,7 +119,7 @@ func TestEndpointsFromEnv(t *testing.T) {
},
want: td.Map(make(map[config.Route]config.EndpointSpec), td.MapEntries{
config.Route("readiness/redis"): td.Struct(config.EndpointSpec{}, td.StructFields{
"Checks": td.Len(2),
"Script": td.Len(2),
}),
}),
wantErr: false,
@ -132,10 +132,10 @@ func TestEndpointsFromEnv(t *testing.T) {
},
want: td.Map(make(map[config.Route]config.EndpointSpec), td.MapEntries{
config.Route("readiness"): td.Struct(config.EndpointSpec{}, td.StructFields{
"Checks": td.Len(1),
"Script": td.Len(1),
}),
config.Route("liveness"): td.Struct(config.EndpointSpec{}, td.StructFields{
"Checks": td.Len(1),
"Script": td.Len(1),
}),
}),
wantErr: false,

1
go.mod
View file

@ -11,6 +11,7 @@ require (
github.com/testcontainers/testcontainers-go v0.13.0
go.uber.org/zap v1.21.0
golang.org/x/exp v0.0.0-20220428152302-39d4317da171
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
)

6
go.sum
View file

@ -82,6 +82,7 @@ github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kd
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
@ -718,17 +719,15 @@ go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA=
go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
@ -835,6 +834,7 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=

View file

@ -1,5 +1,16 @@
package grammar
import (
"encoding/json"
"gopkg.in/yaml.v3"
)
var (
_ json.Unmarshaler = (*Check)(nil)
_ yaml.Unmarshaler = (*Check)(nil)
)
type Call struct {
Module string `parser:"(@Module'.')?"`
Name string `parser:"@Ident"`
@ -15,6 +26,34 @@ type Check struct {
Validators *Filters `parser:"( '=>' @@)?"`
}
func (c *Check) UnmarshalYAML(value *yaml.Node) error {
parser, err := NewParser[Check]()
if err != nil {
return err
}
chk, err := parser.Parse(value.Value)
if err != nil {
return err
}
*c = *chk
return nil
}
func (c *Check) UnmarshalJSON(bytes []byte) error {
parser, err := NewParser[Check]()
if err != nil {
return err
}
chk, err := parser.ParseBytes(bytes)
if err != nil {
return err
}
*c = *chk
return nil
}
type Script struct {
Checks []Check `parser:"(@@';'?)*"`
}

View file

@ -49,3 +49,12 @@ func (p Parser[T]) Parse(rawRule string) (*T, error) {
return into, nil
}
func (p Parser[T]) ParseBytes(data []byte) (*T, error) {
into := new(T)
if err := p.grammarParser.ParseBytes("", data, into); err != nil {
return nil, err
}
return into, nil
}

View file

@ -10,7 +10,7 @@ import (
)
var wantParsedScript = td.Struct(new(grammar.Script), td.StructFields{
"Checks": td.Bag(
"Script": td.Bag(
grammar.Check{
Initiator: &grammar.Call{
Module: "http",

35
main.go
View file

@ -1,13 +1,18 @@
package main
import (
"errors"
"log"
"net/http"
"os"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"github.com/baez90/nurse/api"
"github.com/baez90/nurse/check"
"github.com/baez90/nurse/config"
"github.com/baez90/nurse/protocols/redis"
)
var (
@ -24,7 +29,7 @@ func main() {
logger := zap.L()
envCfg, err := config.New(
nurseInstance, err := config.New(
config.WithValuesFrom(cfg),
config.WithConfigFile(cfgFile),
config.WithServersFromEnv(),
@ -32,11 +37,33 @@ func main() {
)
if err != nil {
logger.Error("Failed to load config from environment", zap.Error(err))
os.Exit(1)
logger.Fatal("Failed to load config from environment", zap.Error(err))
}
logger.Debug("Loaded config", zap.Any("config", envCfg))
logger.Debug("Loaded config", zap.Any("config", nurseInstance))
chkRegistry := check.NewRegistry()
if err := chkRegistry.Register(redis.Module()); err != nil {
logger.Fatal("Failed to register Redis module", zap.Error(err))
}
srvLookup, err := nurseInstance.ServerLookup()
if err != nil {
logger.Fatal("Failed to prepare server lookup", zap.Error(err))
}
mux, err := api.PrepareMux(nurseInstance, chkRegistry, srvLookup)
if err != nil {
logger.Fatal("Failed to prepare server mux", zap.Error(err))
}
if err := http.ListenAndServe(":8080", mux); err != nil {
if errors.Is(err, http.ErrServerClosed) {
return
}
logger.Fatal("Failed to serve HTTP", zap.Error(err))
}
}
func setupLogging() {

View file

@ -6,6 +6,7 @@ import (
func Module() *check.Module {
m, _ := check.NewModule(
"redis",
check.WithCheck("ping", check.FactoryFunc(func() check.SystemChecker {
return new(PingCheck)
})),

View file

@ -12,7 +12,7 @@ import (
"github.com/baez90/nurse/config"
"github.com/baez90/nurse/grammar"
"github.com/baez90/nurse/redis"
"github.com/baez90/nurse/protocols/redis"
)
func TestChecks_Execute(t *testing.T) {