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)
|
# Dependency directories (remove the comment below to include it)
|
||||||
# vendor/
|
# 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
|
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