Add basic Redis checks
This commit is contained in:
parent
1b4f632048
commit
803f52ba46
30 changed files with 2584 additions and 1 deletions
27
.editorconfig
Normal file
27
.editorconfig
Normal file
|
@ -0,0 +1,27 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
tab_width = 4
|
||||
indent_style = space
|
||||
insert_final_newline = false
|
||||
max_line_length = 120
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{go, go2}]
|
||||
indent_style = tab
|
||||
ij_smart_tabs = true
|
||||
ij_go_GROUP_CURRENT_PROJECT_IMPORTS = true
|
||||
ij_go_group_stdlib_imports = true
|
||||
ij_go_import_sorting = goimports
|
||||
ij_go_local_group_mode = project
|
||||
ij_go_move_all_imports_in_one_declaration = true
|
||||
ij_go_move_all_stdlib_imports_in_one_group = true
|
||||
ij_go_remove_redundant_import_aliases = true
|
||||
|
||||
[{*.yaml, *.yml}]
|
||||
indent_size = 2
|
||||
tab_width = 2
|
||||
insert_final_newline = true
|
44
.github/workflows/go.yml
vendored
Normal file
44
.github/workflows/go.yml
vendored
Normal file
|
@ -0,0 +1,44 @@
|
|||
name: Go
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Set up Go 1.18
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '^1.18'
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
lfs: true
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
only-new-issues: true
|
||||
skip-go-installation: true
|
||||
|
||||
- run: go test ./...
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v2
|
||||
with:
|
||||
version: latest
|
||||
args: release --rm-dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -13,3 +13,6 @@
|
|||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
dist/
|
||||
.idea/
|
138
.golangci.yml
Normal file
138
.golangci.yml
Normal file
|
@ -0,0 +1,138 @@
|
|||
linters-settings:
|
||||
dupl:
|
||||
threshold: 100
|
||||
funlen:
|
||||
lines: 100
|
||||
statements: 50
|
||||
gci:
|
||||
local-prefixes: github.com/baez90/nurse
|
||||
goconst:
|
||||
min-len: 2
|
||||
min-occurrences: 2
|
||||
gocritic:
|
||||
enabled-tags:
|
||||
- diagnostic
|
||||
- opinionated
|
||||
- performance
|
||||
disabled-checks:
|
||||
- ifElseChain
|
||||
- octalLiteral
|
||||
- wrapperFunc
|
||||
# see https://github.com/golangci/golangci-lint/issues/2649
|
||||
- hugeParam
|
||||
- rangeValCopy
|
||||
# settings:
|
||||
# hugeParam:
|
||||
# sizeThreshold: 200
|
||||
|
||||
gocyclo:
|
||||
min-complexity: 15
|
||||
goimports:
|
||||
local-prefixes: github.com/baez90/nurse
|
||||
golint:
|
||||
min-confidence: 0
|
||||
gomnd:
|
||||
settings:
|
||||
mnd:
|
||||
# don't include the "operation" and "assign"
|
||||
checks:
|
||||
- argument
|
||||
- case
|
||||
- condition
|
||||
- return
|
||||
gomoddirectives:
|
||||
replace-allow-list: []
|
||||
govet:
|
||||
check-shadowing: true
|
||||
enable-all: true
|
||||
disable:
|
||||
- fieldalignment
|
||||
# see https://github.com/golangci/golangci-lint/issues/2649
|
||||
- nilness
|
||||
- unusedwrite
|
||||
importas:
|
||||
no-unaliased: true
|
||||
lll:
|
||||
line-length: 140
|
||||
misspell:
|
||||
locale: US
|
||||
nolintlint:
|
||||
allow-leading-space: true # don't require machine-readable nolint directives (i.e. with no leading space)
|
||||
allow-unused: false # report any unused nolint directives
|
||||
require-explanation: false # don't require an explanation for nolint directives
|
||||
require-specific: true
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- contextcheck
|
||||
- deadcode
|
||||
- dogsled
|
||||
- dupl
|
||||
- errcheck
|
||||
- errchkjson
|
||||
- errname
|
||||
- errorlint
|
||||
- exhaustive
|
||||
- exportloopref
|
||||
- funlen
|
||||
- gocognit
|
||||
- goconst
|
||||
# - gocritic
|
||||
- gocyclo
|
||||
- godox
|
||||
- gofumpt
|
||||
- goimports
|
||||
- gomoddirectives
|
||||
- gomnd
|
||||
- gosec
|
||||
- gosimple
|
||||
- govet
|
||||
- ifshort
|
||||
- importas
|
||||
- ineffassign
|
||||
# - ireturn - enable later
|
||||
- lll
|
||||
- misspell
|
||||
- nakedret
|
||||
- nestif
|
||||
- nilnil
|
||||
- noctx
|
||||
- nolintlint
|
||||
- paralleltest
|
||||
- prealloc
|
||||
- predeclared
|
||||
- promlinter
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- stylecheck
|
||||
- tenv
|
||||
- testpackage
|
||||
- thelper
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- varcheck
|
||||
- whitespace
|
||||
# - unused
|
||||
- wastedassign
|
||||
|
||||
issues:
|
||||
# Excluding configuration per-path, per-linter, per-text and per-source
|
||||
exclude-rules:
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- dupl
|
||||
- funlen
|
||||
- gocognit
|
||||
- gomnd
|
||||
- govet
|
||||
- path: magefiles/
|
||||
linters:
|
||||
- deadcode
|
||||
|
||||
run:
|
||||
skip-files:
|
||||
- ".*.mock.\\.go$"
|
||||
modules-download-mode: readonly
|
||||
timeout: 5m
|
41
.goreleaser.yaml
Normal file
41
.goreleaser.yaml
Normal file
|
@ -0,0 +1,41 @@
|
|||
before:
|
||||
hooks:
|
||||
- go mod tidy
|
||||
builds:
|
||||
- id: nurse
|
||||
binary: nurse
|
||||
flags:
|
||||
- -trimpath
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
archives:
|
||||
- replacements:
|
||||
darwin: Darwin
|
||||
linux: Linux
|
||||
windows: Windows
|
||||
386: i386
|
||||
amd64: x86_64
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
snapshot:
|
||||
name_template: "{{ incpatch .Version }}-next"
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- '^test:'
|
||||
|
||||
dockers:
|
||||
- ids:
|
||||
- nurse
|
||||
image_templates:
|
||||
- ghcr.io/baez90/nurse:latest
|
||||
- ghcr.io/baez90/nurse:{{ .Tag }}
|
||||
- ghcr.io/baez90/nurse:{{ .Major }}
|
||||
- ghcr.io/baez90/nurse:{{ .ShortCommit}}
|
||||
dockerfile: deployments/Dockerfile
|
20
.pre-commit-config.yaml
Normal file
20
.pre-commit-config.yaml
Normal file
|
@ -0,0 +1,20 @@
|
|||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
repos:
|
||||
- repo: https://github.com/tekwizely/pre-commit-golang
|
||||
rev: v1.0.0-beta.5
|
||||
hooks:
|
||||
- id: go-mod-tidy-repo
|
||||
args:
|
||||
- -go=1.18
|
||||
- id: go-fumpt
|
||||
args:
|
||||
- -w
|
||||
- id: go-imports
|
||||
args:
|
||||
- -local=gitlab.com/inetmock/inetmock
|
||||
- -w
|
||||
- id: golangci-lint-repo-mod
|
||||
args:
|
||||
- --fast
|
||||
- --fix
|
|
@ -1,2 +1,3 @@
|
|||
# nurse
|
||||
# Nurse
|
||||
|
||||
A generic service health sidecar
|
||||
|
|
15
check/api.go
Normal file
15
check/api.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
package check
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/baez90/nurse/grammar"
|
||||
)
|
||||
|
||||
var ErrNoSuchCheck = errors.New("no such check")
|
||||
|
||||
type SystemChecker interface {
|
||||
grammar.CheckUnmarshaler
|
||||
Execute(ctx context.Context) error
|
||||
}
|
48
config/lookup.go
Normal file
48
config/lookup.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrServerNameAlreadyRegistered = errors.New("server name is already registered")
|
||||
ErrNoSuchServer = errors.New("no known server with given name")
|
||||
DefaultLookup = &ServerLookup{
|
||||
servers: make(map[string]Server),
|
||||
}
|
||||
)
|
||||
|
||||
type ServerLookup struct {
|
||||
lock sync.RWMutex
|
||||
servers map[string]Server
|
||||
}
|
||||
|
||||
func (l *ServerLookup) Register(name string, srv Server) error {
|
||||
l.lock.Lock()
|
||||
defer l.lock.Unlock()
|
||||
|
||||
name = strings.ToLower(name)
|
||||
|
||||
if _, ok := l.servers[name]; ok {
|
||||
return fmt.Errorf("%w: %s", ErrServerNameAlreadyRegistered, name)
|
||||
}
|
||||
|
||||
l.servers[name] = srv
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *ServerLookup) Lookup(name string) (*Server, error) {
|
||||
l.lock.RLock()
|
||||
defer l.lock.RUnlock()
|
||||
|
||||
name = strings.ToLower(name)
|
||||
|
||||
match, ok := l.servers[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w: %s", ErrNoSuchServer, name)
|
||||
}
|
||||
return &match, nil
|
||||
}
|
14
config/schemes.go
Normal file
14
config/schemes.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package config
|
||||
|
||||
import "strings"
|
||||
|
||||
var scheme2ServerType = map[string]ServerType{
|
||||
"redis": ServerTypeRedis,
|
||||
}
|
||||
|
||||
func SchemeToServerType(scheme string) ServerType {
|
||||
if match, ok := scheme2ServerType[strings.ToLower(scheme)]; ok {
|
||||
return match
|
||||
}
|
||||
return ServerTypeUnspecified
|
||||
}
|
65
config/server.go
Normal file
65
config/server.go
Normal file
|
@ -0,0 +1,65 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/baez90/nurse/internal/values"
|
||||
)
|
||||
|
||||
type ServerType uint
|
||||
|
||||
const (
|
||||
ServerTypeUnspecified ServerType = iota
|
||||
ServerTypeRedis
|
||||
)
|
||||
|
||||
var hostsRegexp = regexp.MustCompile(`^{(.+:\d{1,5})(,(.+:\d{1,5}))*}|(.+:\d{1,5})$`)
|
||||
|
||||
func ParseFromURL(url *url.URL) (*Server, error) {
|
||||
srv := &Server{
|
||||
Type: SchemeToServerType(url.Scheme),
|
||||
Hosts: hostsRegexp.FindAllString(url.Host, -1),
|
||||
}
|
||||
|
||||
if user := url.User; user != nil {
|
||||
srv.Credentials = &Credentials{
|
||||
Username: user.Username(),
|
||||
}
|
||||
if pw, ok := user.Password(); ok {
|
||||
srv.Credentials.Password = values.StringP(pw)
|
||||
}
|
||||
}
|
||||
|
||||
srv.Path = strings.Split(strings.Trim(url.EscapedPath(), "/"), "/")
|
||||
|
||||
q := url.Query()
|
||||
qm := map[string][]string(q)
|
||||
srv.Args = make(map[string]any, len(qm))
|
||||
|
||||
for k := range qm {
|
||||
var val any
|
||||
if err := json.Unmarshal([]byte(q.Get(k)), &val); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
srv.Args[k] = val
|
||||
}
|
||||
}
|
||||
|
||||
return srv, nil
|
||||
}
|
||||
|
||||
type Credentials struct {
|
||||
Username string
|
||||
Password *string
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
Type ServerType
|
||||
Credentials *Credentials
|
||||
Hosts []string
|
||||
Path []string
|
||||
Args map[string]any
|
||||
}
|
5
deployments/Dockerfile
Normal file
5
deployments/Dockerfile
Normal file
|
@ -0,0 +1,5 @@
|
|||
FROM gcr.io/distroless/static:nonroot
|
||||
|
||||
COPY nurse /usr/local/bin/
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/nurse"]
|
48
go.mod
Normal file
48
go.mod
Normal file
|
@ -0,0 +1,48 @@
|
|||
module github.com/baez90/nurse
|
||||
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/alecthomas/participle/v2 v2.0.0-alpha8
|
||||
github.com/go-redis/redis/v8 v8.11.5
|
||||
github.com/maxatome/go-testdeep v1.11.0
|
||||
github.com/mitchellh/mapstructure v1.4.3
|
||||
github.com/testcontainers/testcontainers-go v0.13.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/Microsoft/go-winio v0.5.2 // indirect
|
||||
github.com/Microsoft/hcsshim v0.9.2 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/containerd/cgroups v1.0.3 // indirect
|
||||
github.com/containerd/containerd v1.6.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/docker/distribution v2.8.1+incompatible // indirect
|
||||
github.com/docker/docker v20.10.14+incompatible // indirect
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/magiconair/properties v1.8.6 // indirect
|
||||
github.com/moby/sys/mount v0.3.2 // indirect
|
||||
github.com/moby/sys/mountinfo v0.6.1 // indirect
|
||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.2 // indirect
|
||||
github.com/opencontainers/runc v1.1.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||
go.opencensus.io v0.23.0 // indirect
|
||||
golang.org/x/net v0.0.0-20220420153159-1850ba15e1be // indirect
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect
|
||||
google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4 // indirect
|
||||
google.golang.org/grpc v1.45.0 // indirect
|
||||
google.golang.org/protobuf v1.28.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
)
|
9
grammar/errors.go
Normal file
9
grammar/errors.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package grammar
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrMissingServer = errors.New("initiator is missing a server")
|
||||
ErrTypeMismatch = errors.New("param has a different type")
|
||||
ErrAmbiguousParamCount = errors.New("the supplied number of arguments does not match the expected one")
|
||||
)
|
24
grammar/grammar.go
Normal file
24
grammar/grammar.go
Normal file
|
@ -0,0 +1,24 @@
|
|||
package grammar
|
||||
|
||||
type CheckUnmarshaler interface {
|
||||
UnmarshalCheck(c Check) error
|
||||
}
|
||||
|
||||
type Call struct {
|
||||
Module string `parser:"(@Module'.')?"`
|
||||
Name string `parser:"@Ident"`
|
||||
Params []Param `parser:"'(' @@? ( ',' @@ )*')'"`
|
||||
}
|
||||
|
||||
type Filters struct {
|
||||
Chain []Call `parser:"@@ ('->' @@)*"`
|
||||
}
|
||||
|
||||
type Check struct {
|
||||
Initiator *Call `parser:"@@"`
|
||||
Validators *Filters `parser:"( '=>' @@)?"`
|
||||
}
|
||||
|
||||
type Script struct {
|
||||
Checks []Check `parser:"@@*"`
|
||||
}
|
39
grammar/param.go
Normal file
39
grammar/param.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package grammar
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func ValidateParameterCount(params []Param, expected int) error {
|
||||
if len(params) < expected {
|
||||
return fmt.Errorf("%w: expected %d got %d", ErrAmbiguousParamCount, expected, len(params))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Param struct {
|
||||
String *string `parser:"@String | @RawString"`
|
||||
Int *int `parser:"| @Int"`
|
||||
Float *float64 `parser:"| @Float"`
|
||||
}
|
||||
|
||||
func (p Param) AsString() (string, error) {
|
||||
if p.String == nil {
|
||||
return "", fmt.Errorf("string is nil %w", ErrTypeMismatch)
|
||||
}
|
||||
return *p.String, nil
|
||||
}
|
||||
|
||||
func (p Param) AsInt() (int, error) {
|
||||
if p.Int == nil {
|
||||
return 0, fmt.Errorf("int is nil %w", ErrTypeMismatch)
|
||||
}
|
||||
return *p.Int, nil
|
||||
}
|
||||
|
||||
func (p Param) AsFloat() (float64, error) {
|
||||
if p.Float == nil {
|
||||
return 0, fmt.Errorf("float is nil %w", ErrTypeMismatch)
|
||||
}
|
||||
return *p.Float, nil
|
||||
}
|
159
grammar/param_test.go
Normal file
159
grammar/param_test.go
Normal file
|
@ -0,0 +1,159 @@
|
|||
package grammar_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/baez90/nurse/grammar"
|
||||
"github.com/baez90/nurse/internal/values"
|
||||
)
|
||||
|
||||
func TestParam_AsString(t *testing.T) {
|
||||
t.Parallel()
|
||||
type fields struct {
|
||||
String *string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Empty string",
|
||||
fields: fields{
|
||||
String: values.StringP(""),
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "Any string",
|
||||
fields: fields{
|
||||
String: values.StringP("Hello, world!"),
|
||||
},
|
||||
want: "Hello, world!",
|
||||
},
|
||||
{
|
||||
name: "nil value",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
tt := tc
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
p := grammar.Param{
|
||||
String: tt.fields.String,
|
||||
}
|
||||
got, err := p.AsString()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("AsString() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("AsString() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParam_AsInt(t *testing.T) {
|
||||
t.Parallel()
|
||||
type fields struct {
|
||||
Int *int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want int
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "zero value",
|
||||
fields: fields{
|
||||
Int: values.IntP(0),
|
||||
},
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "Any int",
|
||||
fields: fields{
|
||||
Int: values.IntP(42),
|
||||
},
|
||||
want: 42,
|
||||
},
|
||||
{
|
||||
name: "nil value",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
tt := tc
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
p := grammar.Param{
|
||||
Int: tt.fields.Int,
|
||||
}
|
||||
got, err := p.AsInt()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("AsInt() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("AsInt() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParam_AsFloat(t *testing.T) {
|
||||
t.Parallel()
|
||||
type fields struct {
|
||||
Float *float64
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want float64
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Zero value",
|
||||
fields: fields{
|
||||
Float: values.FloatP(0),
|
||||
},
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "Any value",
|
||||
fields: fields{
|
||||
Float: values.FloatP(13.37),
|
||||
},
|
||||
want: 13.37,
|
||||
},
|
||||
{
|
||||
name: "nil value",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
tt := tc
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
p := grammar.Param{
|
||||
Float: tt.fields.Float,
|
||||
}
|
||||
got, err := p.AsFloat()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("AsFloat() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("AsFloat() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func params(p ...grammar.Param) []grammar.Param {
|
||||
return p
|
||||
}
|
51
grammar/parser.go
Normal file
51
grammar/parser.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
package grammar
|
||||
|
||||
import (
|
||||
"github.com/alecthomas/participle/v2"
|
||||
"github.com/alecthomas/participle/v2/lexer"
|
||||
)
|
||||
|
||||
func NewParser[T any]() (*Parser[T], error) {
|
||||
def, err := lexer.NewSimple([]lexer.SimpleRule{
|
||||
{Name: "Comment", Pattern: `(?:#|//)[^\n]*\n?`},
|
||||
{Name: `Module`, Pattern: `[a-z]{1}[A-z0-9]+`},
|
||||
{Name: `Ident`, Pattern: `[A-Z][a-zA-Z0-9_]*`},
|
||||
{Name: `CIDR`, Pattern: `(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}/(3[0-2]|[1-2][0-9]|[1-9])`},
|
||||
{Name: `IP`, Pattern: `(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}`},
|
||||
{Name: `Float`, Pattern: `\d+\.\d+`},
|
||||
{Name: `Int`, Pattern: `[-]?\d+`},
|
||||
{Name: `RawString`, Pattern: "`[^`]*`"},
|
||||
{Name: `String`, Pattern: `'[^']*'|"[^"]*"`},
|
||||
{Name: `Arrows`, Pattern: `(->|=>)`},
|
||||
{Name: "whitespace", Pattern: `\s+`},
|
||||
{Name: "Punct", Pattern: `[-[!@#$%^&*()+_={}\|:;\."'<,>?/]|]`},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
grammarParser, err := participle.Build(
|
||||
new(T),
|
||||
participle.Lexer(def),
|
||||
participle.Unquote("String", "RawString"),
|
||||
participle.Elide("Comment"),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Parser[T]{grammarParser: grammarParser}, nil
|
||||
}
|
||||
|
||||
type Parser[T any] struct {
|
||||
grammarParser *participle.Parser
|
||||
}
|
||||
|
||||
func (p Parser[T]) Parse(rawRule string) (*T, error) {
|
||||
into := new(T)
|
||||
if err := p.grammarParser.ParseString("", rawRule, into); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return into, nil
|
||||
}
|
224
grammar/parser_test.go
Normal file
224
grammar/parser_test.go
Normal file
|
@ -0,0 +1,224 @@
|
|||
package grammar_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/maxatome/go-testdeep/td"
|
||||
|
||||
"github.com/baez90/nurse/grammar"
|
||||
"github.com/baez90/nurse/internal/values"
|
||||
)
|
||||
|
||||
var wantParsedScript = td.Struct(new(grammar.Script), td.StructFields{
|
||||
"Checks": td.Bag(
|
||||
grammar.Check{
|
||||
Initiator: &grammar.Call{
|
||||
Module: "http",
|
||||
Name: "Get",
|
||||
Params: params(grammar.Param{String: values.StringP("https://www.gogol.com/")}),
|
||||
},
|
||||
Validators: &grammar.Filters{
|
||||
Chain: []grammar.Call{
|
||||
{
|
||||
Name: "Status",
|
||||
Params: []grammar.Param{
|
||||
{
|
||||
Int: values.IntP(404),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
grammar.Check{
|
||||
Initiator: &grammar.Call{
|
||||
Module: "http",
|
||||
Name: "Get",
|
||||
Params: params(grammar.Param{String: values.StringP("https://www.microsoft.com/")}),
|
||||
},
|
||||
Validators: &grammar.Filters{
|
||||
Chain: []grammar.Call{
|
||||
{
|
||||
Name: "Status",
|
||||
Params: []grammar.Param{
|
||||
{
|
||||
Int: values.IntP(200),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Header",
|
||||
Params: []grammar.Param{
|
||||
{
|
||||
String: values.StringP("Content-Type"),
|
||||
},
|
||||
{
|
||||
String: values.StringP("text/html"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
})
|
||||
|
||||
func TestParser_Parse(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
rawRule string
|
||||
want any
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Check - Initiator only - string argument",
|
||||
rawRule: `http.Get("https://www.microsoft.com/")`,
|
||||
want: &grammar.Script{
|
||||
Checks: []grammar.Check{
|
||||
{
|
||||
Initiator: &grammar.Call{
|
||||
Module: "http",
|
||||
Name: "Get",
|
||||
Params: params(grammar.Param{String: values.StringP("https://www.microsoft.com/")}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Check - Initiator only - raw string argument",
|
||||
rawRule: "http.Post(\"https://www.microsoft.com/\", `{\"Name\":\"Ted.Tester\"}`)",
|
||||
want: &grammar.Script{
|
||||
Checks: []grammar.Check{
|
||||
{
|
||||
Initiator: &grammar.Call{
|
||||
Module: "http",
|
||||
Name: "Post",
|
||||
Params: []grammar.Param{
|
||||
{
|
||||
String: values.StringP("https://www.microsoft.com/"),
|
||||
},
|
||||
{
|
||||
String: values.StringP(`{"Name":"Ted.Tester"}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Check - Initiator and single filter",
|
||||
rawRule: `http.Get("https://www.microsoft.com/") => Status(200)`,
|
||||
want: &grammar.Script{
|
||||
Checks: []grammar.Check{
|
||||
{
|
||||
Initiator: &grammar.Call{
|
||||
Module: "http",
|
||||
Name: "Get",
|
||||
Params: params(grammar.Param{String: values.StringP("https://www.microsoft.com/")}),
|
||||
},
|
||||
Validators: &grammar.Filters{
|
||||
Chain: []grammar.Call{
|
||||
{
|
||||
Name: "Status",
|
||||
Params: []grammar.Param{
|
||||
{
|
||||
Int: values.IntP(200),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Check - Initiator and multiple filters",
|
||||
rawRule: `http.Get("https://www.microsoft.com/") => Status(200) -> Header("Content-Type", "text/html")`,
|
||||
want: &grammar.Script{
|
||||
Checks: []grammar.Check{
|
||||
{
|
||||
Initiator: &grammar.Call{
|
||||
Module: "http",
|
||||
Name: "Get",
|
||||
Params: params(grammar.Param{String: values.StringP("https://www.microsoft.com/")}),
|
||||
},
|
||||
Validators: &grammar.Filters{
|
||||
Chain: []grammar.Call{
|
||||
{
|
||||
Name: "Status",
|
||||
Params: []grammar.Param{
|
||||
{
|
||||
Int: values.IntP(200),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Header",
|
||||
Params: []grammar.Param{
|
||||
{
|
||||
String: values.StringP("Content-Type"),
|
||||
},
|
||||
{
|
||||
String: values.StringP("text/html"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "CheckScript without comments",
|
||||
rawRule: `
|
||||
http.Get("https://www.gogol.com/") => Status(404)
|
||||
http.Get("https://www.microsoft.com/") => Status(200) -> Header("Content-Type", "text/html")
|
||||
`,
|
||||
want: wantParsedScript,
|
||||
},
|
||||
{
|
||||
name: "CheckScript without comments - single line",
|
||||
//nolint:lll // required at this point
|
||||
rawRule: `http.Get("https://www.gogol.com/") => Status(404) http.Get("https://www.microsoft.com/") => Status(200) -> Header("Content-Type", "text/html")`,
|
||||
want: wantParsedScript,
|
||||
},
|
||||
{
|
||||
name: "CheckScript with comments",
|
||||
rawRule: `
|
||||
# GET https://www.gogol.com/ expect a not found response
|
||||
http.Get("https://www.gogol.com/") => Status(404)
|
||||
|
||||
// GET https://www.microsoft.com/ - expect status OK and HTML content
|
||||
http.Get("https://www.microsoft.com/") => Status(200) -> Header("Content-Type", "text/html")
|
||||
`,
|
||||
|
||||
want: wantParsedScript,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
p, err := grammar.NewParser[grammar.Script]()
|
||||
if err != nil {
|
||||
t.Fatalf("NewParser() err = %v", err)
|
||||
}
|
||||
|
||||
got, err := p.Parse(tt.rawRule)
|
||||
if err != nil {
|
||||
if !tt.wantErr {
|
||||
t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
td.Cmp(t, got, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
13
internal/values/refs.go
Normal file
13
internal/values/refs.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package values
|
||||
|
||||
func StringP(value string) *string {
|
||||
return &value
|
||||
}
|
||||
|
||||
func IntP(value int) *int {
|
||||
return &value
|
||||
}
|
||||
|
||||
func FloatP(value float64) *float64 {
|
||||
return &value
|
||||
}
|
28
main.go
Normal file
28
main.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
var checkTimeout time.Duration
|
||||
|
||||
func main() {
|
||||
}
|
||||
|
||||
func lookupEnvOr[T any](envKey string, fallback T, parse func(envVal string) (T, error)) T {
|
||||
envVal := os.Getenv(envKey)
|
||||
if envVal == "" {
|
||||
return fallback
|
||||
}
|
||||
|
||||
if parsed, err := parse(envVal); err != nil {
|
||||
return fallback
|
||||
} else {
|
||||
return parsed
|
||||
}
|
||||
}
|
||||
|
||||
func identity[T any](in T) (T, error) {
|
||||
return in, nil
|
||||
}
|
35
redis/checks.go
Normal file
35
redis/checks.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
package redis
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/baez90/nurse/check"
|
||||
"github.com/baez90/nurse/grammar"
|
||||
)
|
||||
|
||||
var knownChecks = map[string]func() check.SystemChecker{
|
||||
"ping": func() check.SystemChecker {
|
||||
return new(PingCheck)
|
||||
},
|
||||
"get": func() check.SystemChecker {
|
||||
return new(GetCheck)
|
||||
},
|
||||
}
|
||||
|
||||
func LookupCheck(c grammar.Check) (check.SystemChecker, error) {
|
||||
var (
|
||||
provider func() check.SystemChecker
|
||||
ok bool
|
||||
)
|
||||
if provider, ok = knownChecks[strings.ToLower(c.Initiator.Name)]; !ok {
|
||||
return nil, fmt.Errorf("%w: %s", check.ErrNoSuchCheck, c.Initiator.Name)
|
||||
}
|
||||
|
||||
chk := provider()
|
||||
if err := chk.UnmarshalCheck(c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return chk, nil
|
||||
}
|
41
redis/client.go
Normal file
41
redis/client.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
package redis
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
|
||||
"github.com/baez90/nurse/config"
|
||||
"github.com/baez90/nurse/grammar"
|
||||
)
|
||||
|
||||
func clientFromParam(p grammar.Param) (redis.UniversalClient, error) {
|
||||
if srvName, err := p.AsString(); err != nil {
|
||||
return nil, err
|
||||
} else if srv, err := config.DefaultLookup.Lookup(srvName); err != nil {
|
||||
return nil, err
|
||||
} else if redisCli, err := ClientForServer(srv); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return redisCli, nil
|
||||
}
|
||||
}
|
||||
|
||||
func ClientForServer(srv *config.Server) (redis.UniversalClient, error) {
|
||||
opts := &redis.UniversalOptions{
|
||||
Addrs: srv.Hosts,
|
||||
}
|
||||
|
||||
if pathLen := len(srv.Path); pathLen > 0 {
|
||||
if db, err := strconv.Atoi(srv.Path[0]); err == nil {
|
||||
opts.DB = db
|
||||
}
|
||||
}
|
||||
|
||||
if err := mapstructure.Decode(srv.Args, opts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return redis.NewUniversalClient(opts), nil
|
||||
}
|
59
redis/container_test.go
Normal file
59
redis/container_test.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
package redis_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/testcontainers/testcontainers-go"
|
||||
"github.com/testcontainers/testcontainers-go/wait"
|
||||
|
||||
"github.com/baez90/nurse/config"
|
||||
)
|
||||
|
||||
func PrepareRedisContainer(tb testing.TB) *config.Server {
|
||||
tb.Helper()
|
||||
|
||||
const redisPort = "6379/tcp"
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
|
||||
tb.Cleanup(cancel)
|
||||
|
||||
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
|
||||
ContainerRequest: testcontainers.ContainerRequest{
|
||||
Image: "docker.io/redis:alpine",
|
||||
Name: tb.Name(),
|
||||
ExposedPorts: []string{redisPort},
|
||||
WaitingFor: wait.ForListeningPort(redisPort),
|
||||
},
|
||||
Started: true,
|
||||
Logger: testcontainers.TestLogger(tb),
|
||||
})
|
||||
if err != nil {
|
||||
tb.Fatalf("testcontainers.GenericContainer() err = %v", err)
|
||||
}
|
||||
|
||||
tb.Cleanup(func() {
|
||||
if err := container.Terminate(context.Background()); err != nil {
|
||||
tb.Errorf("container.Terminate() err = %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
ep, err := container.PortEndpoint(ctx, redisPort, "redis")
|
||||
if err != nil {
|
||||
tb.Fatalf("container.PortEndpoint() err = %v", err)
|
||||
}
|
||||
|
||||
u, err := url.Parse(fmt.Sprintf("%s/0?MaxRetries=3", ep))
|
||||
if err != nil {
|
||||
tb.Fatalf("url.Parse() err = %v", err)
|
||||
}
|
||||
|
||||
srv, err := config.ParseFromURL(u)
|
||||
if err != nil {
|
||||
tb.Fatalf("config.ParseFromURL() err = %v", err)
|
||||
}
|
||||
return srv
|
||||
}
|
50
redis/get.go
Normal file
50
redis/get.go
Normal file
|
@ -0,0 +1,50 @@
|
|||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
|
||||
"github.com/baez90/nurse/check"
|
||||
"github.com/baez90/nurse/grammar"
|
||||
)
|
||||
|
||||
var (
|
||||
_ check.SystemChecker = (*GetCheck)(nil)
|
||||
_ grammar.CheckUnmarshaler = (*GetCheck)(nil)
|
||||
)
|
||||
|
||||
type GetCheck struct {
|
||||
redis.UniversalClient
|
||||
validators ValidationChain
|
||||
Key string
|
||||
}
|
||||
|
||||
func (g GetCheck) Execute(ctx context.Context) error {
|
||||
cmd := g.Get(ctx, g.Key)
|
||||
|
||||
if err := cmd.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return g.validators.Validate(cmd)
|
||||
}
|
||||
|
||||
func (g *GetCheck) UnmarshalCheck(c grammar.Check) error {
|
||||
const serverAndKeyArgsNumber = 2
|
||||
inst := c.Initiator
|
||||
if err := grammar.ValidateParameterCount(inst.Params, serverAndKeyArgsNumber); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var err error
|
||||
if g.UniversalClient, err = clientFromParam(inst.Params[0]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if g.Key, err = inst.Params[1].AsString(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
70
redis/get_test.go
Normal file
70
redis/get_test.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
package redis_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
redisCli "github.com/go-redis/redis/v8"
|
||||
"github.com/maxatome/go-testdeep/td"
|
||||
|
||||
"github.com/baez90/nurse/config"
|
||||
"github.com/baez90/nurse/grammar"
|
||||
"github.com/baez90/nurse/redis"
|
||||
)
|
||||
|
||||
func TestGetCheck_Execute(t *testing.T) {
|
||||
t.Parallel()
|
||||
srv := PrepareRedisContainer(t)
|
||||
if err := config.DefaultLookup.Register(t.Name(), *srv); err != nil {
|
||||
t.Fatalf("DefaultLookup.Register() err = %v", err)
|
||||
}
|
||||
|
||||
cli, err := redis.ClientForServer(srv)
|
||||
if err != nil {
|
||||
t.Fatalf("redis.ClientForServer() err = %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
check string
|
||||
setup func(tb testing.TB, cli redisCli.UniversalClient)
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Get value",
|
||||
check: fmt.Sprintf(`redis.GET("%s", "some_key")`, t.Name()),
|
||||
setup: func(tb testing.TB, cli redisCli.UniversalClient) {
|
||||
tb.Helper()
|
||||
td.CmpNoError(tb, cli.Set(context.Background(), "some_key", "some_value", 0).Err())
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if tt.setup != nil {
|
||||
tt.setup(t, cli)
|
||||
}
|
||||
|
||||
get := new(redis.GetCheck)
|
||||
|
||||
parser, err := grammar.NewParser[grammar.Check]()
|
||||
td.CmpNoError(t, err, "grammar.NewParser()")
|
||||
check, err := parser.Parse(tt.check)
|
||||
td.CmpNoError(t, err, "parser.Parse()")
|
||||
|
||||
td.CmpNoError(t, get.UnmarshalCheck(*check), "get.UnmarshalCheck()")
|
||||
td.CmpNoError(t, get.Execute(context.Background()))
|
||||
|
||||
if tt.wantErr {
|
||||
td.CmpError(t, get.Execute(context.Background()))
|
||||
} else {
|
||||
td.CmpNoError(t, get.Execute(context.Background()))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
66
redis/ping.go
Normal file
66
redis/ping.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
|
||||
"github.com/baez90/nurse/check"
|
||||
"github.com/baez90/nurse/grammar"
|
||||
)
|
||||
|
||||
var (
|
||||
_ check.SystemChecker = (*PingCheck)(nil)
|
||||
_ grammar.CheckUnmarshaler = (*PingCheck)(nil)
|
||||
)
|
||||
|
||||
type PingCheck struct {
|
||||
redis.UniversalClient
|
||||
validators ValidationChain
|
||||
Message string
|
||||
}
|
||||
|
||||
func (p PingCheck) Execute(ctx context.Context) error {
|
||||
if p.Message == "" {
|
||||
return p.Ping(ctx).Err()
|
||||
}
|
||||
if resp, err := p.Do(ctx, "PING", p.Message).Text(); err != nil {
|
||||
return err
|
||||
} else if resp != p.Message {
|
||||
return fmt.Errorf("expected value %s got %s", p.Message, resp)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PingCheck) UnmarshalCheck(c grammar.Check) error {
|
||||
const (
|
||||
serverOnlyArgCount = 1
|
||||
serverAndMessageArgCount = 2
|
||||
)
|
||||
|
||||
p.validators = append(p.validators, StringCmdValidator("PONG"))
|
||||
|
||||
init := c.Initiator
|
||||
switch len(init.Params) {
|
||||
case 0:
|
||||
return grammar.ErrMissingServer
|
||||
case serverAndMessageArgCount:
|
||||
if msg, err := init.Params[1].AsString(); err != nil {
|
||||
return err
|
||||
} else {
|
||||
p.validators = ValidationChain{StringCmdValidator(msg)}
|
||||
}
|
||||
fallthrough
|
||||
case serverOnlyArgCount:
|
||||
if cli, err := clientFromParam(init.Params[0]); err != nil {
|
||||
return err
|
||||
} else {
|
||||
p.UniversalClient = cli
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return grammar.ErrAmbiguousParamCount
|
||||
}
|
||||
}
|
61
redis/ping_test.go
Normal file
61
redis/ping_test.go
Normal file
|
@ -0,0 +1,61 @@
|
|||
package redis_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/maxatome/go-testdeep/td"
|
||||
|
||||
"github.com/baez90/nurse/config"
|
||||
"github.com/baez90/nurse/grammar"
|
||||
"github.com/baez90/nurse/redis"
|
||||
)
|
||||
|
||||
func TestPingCheck_Execute(t *testing.T) {
|
||||
t.Parallel()
|
||||
srv := PrepareRedisContainer(t)
|
||||
if err := config.DefaultLookup.Register(t.Name(), *srv); err != nil {
|
||||
t.Fatalf("DefaultLookup.Register() err = %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
check string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "PING check",
|
||||
check: fmt.Sprintf(`redis.PING("%s")`, t.Name()),
|
||||
wantErr: false,
|
||||
},
|
||||
// redis.PING("my-redis")
|
||||
// redis.PING("my-redis", "Hello, Redis!")
|
||||
// redis.GET("my-redis", "some-key") -> String("Hello")
|
||||
// redis.PING("my-redis"); redis.GET("my-redis", "some-key") -> String("Hello")
|
||||
{
|
||||
name: "PING check - with custom message",
|
||||
check: fmt.Sprintf(`redis.PING("%s", "Hello, Redis!")`, t.Name()),
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ping := new(redis.PingCheck)
|
||||
|
||||
parser, err := grammar.NewParser[grammar.Check]()
|
||||
td.CmpNoError(t, err, "grammar.NewParser()")
|
||||
check, err := parser.Parse(tt.check)
|
||||
td.CmpNoError(t, err, "parser.Parse()")
|
||||
|
||||
if err := ping.UnmarshalCheck(*check); err != nil {
|
||||
t.Fatalf("UnmarshalCheck() err = %v", err)
|
||||
}
|
||||
if err := ping.Execute(context.Background()); (err != nil) != tt.wantErr {
|
||||
t.Errorf("Execute() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
49
redis/validation.go
Normal file
49
redis/validation.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
package redis
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
)
|
||||
|
||||
var (
|
||||
_ CmdValidator = (ValidationChain)(nil)
|
||||
_ CmdValidator = StringCmdValidator("")
|
||||
)
|
||||
|
||||
type (
|
||||
CmdValidator interface {
|
||||
Validate(cmder redis.Cmder) error
|
||||
}
|
||||
|
||||
ValidationChain []CmdValidator
|
||||
)
|
||||
|
||||
func (v ValidationChain) Validate(cmder redis.Cmder) error {
|
||||
for i := range v {
|
||||
if err := v[i].Validate(cmder); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type StringCmdValidator string
|
||||
|
||||
func (s StringCmdValidator) Validate(cmder redis.Cmder) error {
|
||||
if err := cmder.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if stringCmd, ok := cmder.(*redis.StringCmd); !ok {
|
||||
return errors.New("not a string result")
|
||||
} else if got, err := stringCmd.Result(); err != nil {
|
||||
return err
|
||||
} else if want := string(s); got != want {
|
||||
return fmt.Errorf("want %s but got %s", want, got)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
Loading…
Reference in a new issue