From e4d47a1b27131c8e4187b80126a5faa6928d585a Mon Sep 17 00:00:00 2001 From: lvlcn-t <75443136+lvlcn-t@users.noreply.github.com> Date: Fri, 15 Nov 2024 21:08:12 +0100 Subject: [PATCH 01/17] test: add e2e tests for sparrow * test: add health check e2e test * test: add latency check e2e test * test: add dns check e2e test * test: add file loader e2e test * fix: add missing version in openapi spec * chore: use go-yaml instead of yaml.v3 * chore: skip go files in bash e2e tests Signed-off-by: lvlcn-t <75443136+lvlcn-t@users.noreply.github.com> --- .github/workflows/e2e_checks.yml | 7 +- .pre-commit-config.yaml | 2 +- .vscode/settings.json | 3 +- cmd/run.go | 4 +- go.mod | 4 +- go.sum | 4 + pkg/config/config.go | 4 + pkg/config/file.go | 4 +- pkg/config/file_test.go | 2 +- pkg/config/http.go | 4 +- pkg/config/http_test.go | 2 +- pkg/sparrow/controller.go | 13 +- pkg/sparrow/controller_test.go | 10 +- pkg/sparrow/handlers.go | 2 +- pkg/sparrow/handlers_test.go | 2 +- pkg/sparrow/run.go | 2 +- scripts/run_e2e_tests.sh | 18 +- test/e2e/.gitignore | 1 + test/e2e/main_test.go | 256 ++++++++++++++++++ {e2e => test/e2e}/traceroute/lab.conf | 0 {e2e => test/e2e}/traceroute/pc1.startup | 0 {e2e => test/e2e}/traceroute/pc2.startup | 0 {e2e => test/e2e}/traceroute/r1.startup | 0 {e2e => test/e2e}/traceroute/r2.startup | 0 .../e2e}/traceroute/shared/config.yaml | 0 .../e2e}/traceroute/shared/get_api.sh | 0 {e2e => test/e2e}/traceroute/test.sh | 0 test/framework/checks.go | 230 ++++++++++++++++ test/framework/framework.go | 69 +++++ test/framework/startup.go | 172 ++++++++++++ 30 files changed, 783 insertions(+), 32 deletions(-) create mode 100644 test/e2e/.gitignore create mode 100644 test/e2e/main_test.go rename {e2e => test/e2e}/traceroute/lab.conf (100%) rename {e2e => test/e2e}/traceroute/pc1.startup (100%) rename {e2e => test/e2e}/traceroute/pc2.startup (100%) rename {e2e => test/e2e}/traceroute/r1.startup (100%) rename {e2e => test/e2e}/traceroute/r2.startup (100%) rename {e2e => test/e2e}/traceroute/shared/config.yaml (100%) rename {e2e => test/e2e}/traceroute/shared/get_api.sh (100%) rename {e2e => test/e2e}/traceroute/test.sh (100%) create mode 100644 test/framework/checks.go create mode 100644 test/framework/framework.go create mode 100644 test/framework/startup.go diff --git a/.github/workflows/e2e_checks.yml b/.github/workflows/e2e_checks.yml index d26f3b56..e3fb772b 100644 --- a/.github/workflows/e2e_checks.yml +++ b/.github/workflows/e2e_checks.yml @@ -1,7 +1,6 @@ -name: E2E - Test checks +name: E2E - Traceroute Check -on: - push: +on: [push] permissions: contents: read @@ -48,7 +47,7 @@ jobs: uses: goreleaser/goreleaser-action@v6 with: version: latest - args: build --single-target --clean --snapshot --config .goreleaser-ci.yaml + args: build --single-target --clean --snapshot --config .goreleaser-ci.yaml - name: Run e2e tests run: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 48197cd6..c3032bb1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: hooks: - id: go-mod-tidy-repo - id: go-test-repo-mod - args: [-race, -count=1, -timeout 30s] + args: [-race, -count=1, -timeout 30s, "-test.short"] - id: go-vet-repo-mod - id: go-fumpt-repo args: [-l, -w] diff --git a/.vscode/settings.json b/.vscode/settings.json index d35d0a6b..bb2287ce 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,7 @@ "go.testFlags": [ "-race", "-cover", - "-count=1" + "-count=1", + "-timeout=120s", ] } \ No newline at end of file diff --git a/cmd/run.go b/cmd/run.go index 1c1bffd0..8fb1d7dc 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -66,8 +66,8 @@ func NewCmdRun() *cobra.Command { // run is the entry point to start the sparrow func run() func(cmd *cobra.Command, args []string) error { - return func(_ *cobra.Command, _ []string) error { - cfg := &config.Config{} + return func(cmd *cobra.Command, _ []string) error { + cfg := &config.Config{Version: cmd.Root().Version} err := viper.Unmarshal(cfg) if err != nil { return fmt.Errorf("failed to parse config: %w", err) diff --git a/go.mod b/go.mod index 100dc801..9916d33a 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.23 require ( github.com/getkin/kin-openapi v0.128.0 github.com/go-chi/chi/v5 v5.1.0 + github.com/goccy/go-yaml v1.13.8 github.com/google/go-cmp v0.6.0 github.com/jarcoal/httpmock v1.3.1 github.com/prometheus/client_golang v1.20.5 @@ -20,7 +21,6 @@ require ( golang.org/x/net v0.31.0 golang.org/x/sys v0.27.0 google.golang.org/grpc v1.68.0 - gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -36,6 +36,7 @@ require ( github.com/go-openapi/swag v0.23.0 // indirect github.com/go-viper/mapstructure/v2 v2.1.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/mux v1.8.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/invopop/yaml v0.3.1 // indirect @@ -65,4 +66,5 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 // indirect google.golang.org/protobuf v1.35.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index fb8e736f..8f83a6c2 100644 --- a/go.sum +++ b/go.sum @@ -29,12 +29,16 @@ github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-viper/mapstructure/v2 v2.1.0 h1:gHnMa2Y/pIxElCH2GlZZ1lZSsn6XMtufpGyP1XxdC/w= github.com/go-viper/mapstructure/v2 v2.1.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-yaml v1.13.8 h1:ftugzaplJyFaFwfyVNeq1XQOBxmlp8zazmuiobaCXbk= +github.com/goccy/go-yaml v1.13.8/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0 h1:ad0vkEBuk23VJzZR9nkLVG0YAoN9coASF1GusYX6AlU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.23.0/go.mod h1:igFoXX2ELCW06bol23DWPB5BEWfZISOzSP5K2sbLea0= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= diff --git a/pkg/config/config.go b/pkg/config/config.go index f1c4b81c..0169e08f 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -29,6 +29,10 @@ import ( ) type Config struct { + // Version is the version of the sparrow. + // This is set at build time by using -ldflags "-X main.version=x.x.x" + // and is not part of the configuration file or flags. + Version string `yaml:"-" mapstructure:"-"` // SparrowName is the DNS name of the sparrow SparrowName string `yaml:"name" mapstructure:"name"` // Loader is the configuration for the loader diff --git a/pkg/config/file.go b/pkg/config/file.go index 7093c844..9857b0fa 100644 --- a/pkg/config/file.go +++ b/pkg/config/file.go @@ -30,7 +30,7 @@ import ( "github.com/caas-team/sparrow/internal/logger" "github.com/caas-team/sparrow/pkg/checks/runtime" - "gopkg.in/yaml.v3" + "github.com/goccy/go-yaml" ) var _ Loader = (*FileLoader)(nil) @@ -120,7 +120,7 @@ func (f *FileLoader) getRuntimeConfig(ctx context.Context) (cfg runtime.Config, return cfg, fmt.Errorf("failed to read config file: %w", err) } - if err := yaml.Unmarshal(b, &cfg); err != nil { + if err := yaml.UnmarshalContext(ctx, b, &cfg); err != nil { log.Error("Failed to parse config file", "error", err) return cfg, fmt.Errorf("failed to parse config file: %w", err) } diff --git a/pkg/config/file_test.go b/pkg/config/file_test.go index 77b78ec1..76fab30e 100644 --- a/pkg/config/file_test.go +++ b/pkg/config/file_test.go @@ -29,7 +29,7 @@ import ( "github.com/caas-team/sparrow/pkg/checks/health" "github.com/caas-team/sparrow/pkg/checks/runtime" "github.com/caas-team/sparrow/pkg/config/test" - "gopkg.in/yaml.v3" + "github.com/goccy/go-yaml" ) func TestNewFileLoader(t *testing.T) { diff --git a/pkg/config/http.go b/pkg/config/http.go index aeaffbd8..2f868e62 100644 --- a/pkg/config/http.go +++ b/pkg/config/http.go @@ -29,7 +29,7 @@ import ( "github.com/caas-team/sparrow/internal/helper" "github.com/caas-team/sparrow/internal/logger" "github.com/caas-team/sparrow/pkg/checks/runtime" - "gopkg.in/yaml.v3" + "github.com/goccy/go-yaml" ) type HttpLoader struct { @@ -138,7 +138,7 @@ func (hl *HttpLoader) getRuntimeConfig(ctx context.Context) (cfg runtime.Config, } log.Debug("Successfully got response") - if err := yaml.Unmarshal(b, &cfg); err != nil { + if err := yaml.UnmarshalContext(ctx, b, &cfg); err != nil { log.Error("Could not unmarshal response", "error", err.Error()) return cfg, err } diff --git a/pkg/config/http_test.go b/pkg/config/http_test.go index 4ab62ce7..e1bb41f1 100644 --- a/pkg/config/http_test.go +++ b/pkg/config/http_test.go @@ -33,9 +33,9 @@ import ( "github.com/caas-team/sparrow/internal/logger" "github.com/caas-team/sparrow/pkg/checks/health" "github.com/caas-team/sparrow/pkg/checks/runtime" + "github.com/goccy/go-yaml" "github.com/jarcoal/httpmock" "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" ) func TestHttpLoader_GetRuntimeConfig(t *testing.T) { diff --git a/pkg/sparrow/controller.go b/pkg/sparrow/controller.go index 461ae605..4fcb33b1 100644 --- a/pkg/sparrow/controller.go +++ b/pkg/sparrow/controller.go @@ -41,10 +41,13 @@ type ChecksController struct { cResult chan checks.ResultDTO cErr chan error done chan struct{} + // version is the version of the sparrow. + // This is set at build time by using -ldflags "-X main.version=x.x.x". + version string } // NewChecksController creates a new ChecksController. -func NewChecksController(dbase db.DB, m metrics.Provider) *ChecksController { +func NewChecksController(dbase db.DB, m metrics.Provider, version string) *ChecksController { return &ChecksController{ db: dbase, metrics: m, @@ -52,6 +55,7 @@ func NewChecksController(dbase db.DB, m metrics.Provider) *ChecksController { cResult: make(chan checks.ResultDTO, 8), //nolint:mnd // Buffered channel to avoid blocking the checks cErr: make(chan error, 1), done: make(chan struct{}, 1), + version: version, } } @@ -170,16 +174,20 @@ func (cc *ChecksController) UnregisterCheck(ctx context.Context, check checks.Ch } var oapiBoilerplate = openapi3.T{ - // this object should probably be user defined OpenAPI: "3.0.0", Info: &openapi3.Info{ Title: "Sparrow Metrics API", + Version: "0.5.0", Description: "Serves metrics collected by sparrows checks", Contact: &openapi3.Contact{ URL: "https://caas.telekom.de", Email: "caas-request@telekom.de", Name: "CaaS Team", }, + License: &openapi3.License{ + Name: "Apache 2.0", + URL: "http://www.apache.org/licenses/LICENSE-2.0", + }, }, Paths: &openapi3.Paths{ Extensions: make(map[string]any), @@ -196,6 +204,7 @@ var oapiBoilerplate = openapi3.T{ func (cc *ChecksController) GenerateCheckSpecs(ctx context.Context) (openapi3.T, error) { log := logger.FromContext(ctx) doc := oapiBoilerplate + doc.Info.Version = cc.version for _, c := range cc.checks.Iter() { name := c.Name() ref, err := c.Schema() diff --git a/pkg/sparrow/controller_test.go b/pkg/sparrow/controller_test.go index f68887db..253ca52a 100644 --- a/pkg/sparrow/controller_test.go +++ b/pkg/sparrow/controller_test.go @@ -42,7 +42,7 @@ func TestRun_CheckRunError(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - cc := NewChecksController(db.NewInMemory(), metrics.New(metrics.Config{})) + cc := NewChecksController(db.NewInMemory(), metrics.New(metrics.Config{}), "") mockCheck := &checks.CheckMock{ NameFunc: func() string { return "mockCheck" }, RunFunc: func(ctx context.Context, cResult chan checks.ResultDTO) error { @@ -82,7 +82,7 @@ func TestRun_CheckRunError(t *testing.T) { func TestRun_ContextCancellation(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) - cc := NewChecksController(db.NewInMemory(), metrics.New(metrics.Config{})) + cc := NewChecksController(db.NewInMemory(), metrics.New(metrics.Config{}), "") done := make(chan struct{}) go func() { @@ -206,7 +206,7 @@ func TestChecksController_Reconcile(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - cc := NewChecksController(db.NewInMemory(), metrics.New(metrics.Config{})) + cc := NewChecksController(db.NewInMemory(), metrics.New(metrics.Config{}), "") for _, c := range tt.checks { cc.checks.Add(c) @@ -244,7 +244,7 @@ func TestChecksController_RegisterCheck(t *testing.T) { { name: "register one check", setup: func() *ChecksController { - return NewChecksController(db.NewInMemory(), metrics.New(metrics.Config{})) + return NewChecksController(db.NewInMemory(), metrics.New(metrics.Config{}), "") }, check: health.NewCheck(), }, @@ -274,7 +274,7 @@ func TestChecksController_UnregisterCheck(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - cc := NewChecksController(db.NewInMemory(), metrics.New(metrics.Config{})) + cc := NewChecksController(db.NewInMemory(), metrics.New(metrics.Config{}), "") cc.UnregisterCheck(context.Background(), tt.check) diff --git a/pkg/sparrow/handlers.go b/pkg/sparrow/handlers.go index 3954470f..9a412b33 100644 --- a/pkg/sparrow/handlers.go +++ b/pkg/sparrow/handlers.go @@ -27,8 +27,8 @@ import ( "github.com/caas-team/sparrow/internal/logger" "github.com/caas-team/sparrow/pkg/api" "github.com/go-chi/chi/v5" + "github.com/goccy/go-yaml" "github.com/prometheus/client_golang/prometheus/promhttp" - "gopkg.in/yaml.v3" ) type encoder interface { diff --git a/pkg/sparrow/handlers_test.go b/pkg/sparrow/handlers_test.go index e1f7a9c0..555b1b08 100644 --- a/pkg/sparrow/handlers_test.go +++ b/pkg/sparrow/handlers_test.go @@ -34,7 +34,7 @@ import ( "github.com/caas-team/sparrow/pkg/db" "github.com/getkin/kin-openapi/openapi3" "github.com/go-chi/chi/v5" - "gopkg.in/yaml.v3" + "github.com/goccy/go-yaml" ) func TestSparrow_handleOpenAPI(t *testing.T) { diff --git a/pkg/sparrow/run.go b/pkg/sparrow/run.go index 742f95da..cbaae5d6 100644 --- a/pkg/sparrow/run.go +++ b/pkg/sparrow/run.go @@ -74,7 +74,7 @@ func New(cfg *config.Config) *Sparrow { db: dbase, api: api.New(cfg.Api), metrics: m, - controller: NewChecksController(dbase, m), + controller: NewChecksController(dbase, m, cfg.Version), cRuntime: make(chan runtime.Config, 1), cErr: make(chan error, 1), cDone: make(chan struct{}, 1), diff --git a/scripts/run_e2e_tests.sh b/scripts/run_e2e_tests.sh index f8387baf..f9f667f1 100755 --- a/scripts/run_e2e_tests.sh +++ b/scripts/run_e2e_tests.sh @@ -5,11 +5,15 @@ EXIT_CODE=0 MAX_RETRY=3 -for i in $(ls e2e); do - for ATTEMPT in $(seq 1 $MAX_RETRY ); do - echo "[$ATTEMPT/$MAX_RETRY] Running test e2e/$i" - cd e2e/$i - ./test.sh +for i in $(ls -d test/e2e/*); do + if [ ! -d $i ] || [ ! -f $i/test.sh ]; then + continue + fi + + for ATTEMPT in $(seq 1 $MAX_RETRY); do + echo "[$ATTEMPT/$MAX_RETRY] Running test $i" + cd $i + ./test.sh TEST_EXIT_CODE=$? cd $root if [ $TEST_EXIT_CODE -eq 0 ]; then @@ -17,7 +21,7 @@ for i in $(ls e2e); do elif [ $ATTEMPT -eq $MAX_RETRY ]; then EXIT_CODE=1 fi - done + done done -exit $EXIT_CODE \ No newline at end of file +exit $EXIT_CODE diff --git a/test/e2e/.gitignore b/test/e2e/.gitignore new file mode 100644 index 00000000..1cc9ae43 --- /dev/null +++ b/test/e2e/.gitignore @@ -0,0 +1 @@ +testdata/* diff --git a/test/e2e/main_test.go b/test/e2e/main_test.go new file mode 100644 index 00000000..12ebbc03 --- /dev/null +++ b/test/e2e/main_test.go @@ -0,0 +1,256 @@ +package e2e + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "testing" + "time" + + "github.com/caas-team/sparrow/test/framework" + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/routers" + "github.com/getkin/kin-openapi/routers/gorillamux" +) + +func TestSparrow_E2E(t *testing.T) { + if testing.Short() { + t.Skip("skipping e2e tests") + } + + type test struct { + name string + startup framework.ConfigBuilder + checks []framework.CheckBuilder + wantEndpoints map[string]int + } + + tests := []test{ + { + name: "no checks", + startup: *framework.NewConfig(), + checks: nil, + wantEndpoints: map[string]int{ + "http://localhost:8080/v1/metrics/health": http.StatusNotFound, + "http://localhost:8080/v1/metrics/latency": http.StatusNotFound, + "http://localhost:8080/v1/metrics/dns": http.StatusNotFound, + "http://localhost:8080/v1/metrics/traceroute": http.StatusNotFound, + }, + }, + { + name: "with health check", + startup: *framework.NewConfig(), + checks: []framework.CheckBuilder{ + framework.NewHealthCheck(). + WithInterval(20 * time.Second). + WithTimeout(15 * time.Second). + WithTargets([]string{"https://www.example.com/", "https://www.google.com/"}), + }, + wantEndpoints: map[string]int{ + "http://localhost:8080/v1/metrics/health": http.StatusOK, + "http://localhost:8080/v1/metrics/latency": http.StatusNotFound, + "http://localhost:8080/v1/metrics/dns": http.StatusNotFound, + "http://localhost:8080/v1/metrics/traceroute": http.StatusNotFound, + }, + }, + { + name: "with health, latency and dns checks", + startup: *framework.NewConfig(), + checks: []framework.CheckBuilder{ + framework.NewHealthCheck(). + WithInterval(20 * time.Second). + WithTimeout(15 * time.Second). + WithTargets([]string{"https://www.example.com/"}), + framework.NewLatencyCheck(). + WithInterval(20 * time.Second). + WithTimeout(15 * time.Second). + WithTargets([]string{"https://www.example.com/"}), + framework.NewDNSCheck(). + WithInterval(20 * time.Second). + WithTimeout(15 * time.Second). + WithTargets([]string{"www.example.com"}), + }, + wantEndpoints: map[string]int{ + "http://localhost:8080/v1/metrics/health": http.StatusOK, + "http://localhost:8080/v1/metrics/latency": http.StatusOK, + "http://localhost:8080/v1/metrics/dns": http.StatusOK, + "http://localhost:8080/v1/metrics/traceroute": http.StatusNotFound, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := framework.New(t) + + e2e := f.E2E(tt.startup.Config(t)) + for _, check := range tt.checks { + e2e = e2e.WithCheck(check) + } + + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) + defer cancel() + + finish := make(chan error, 1) + go func() { + finish <- e2e.Run(ctx) + }() + + // Wait for sparrow to be ready with a readiness probe. + readinessProbe(t, "http://localhost:8080", 15*time.Second) + + // Wait for the checks to be executed. + wait := 5 * time.Second + if len(tt.checks) > 0 { + wait = 35 * time.Second + } + <-time.After(wait) + t.Logf("Waited %s for checks to be executed", wait.String()) + + // Fetch, parse and create a new router from the OpenAPI schema, to be able to validate the responses. + schema, err := fetchOpenAPISchema("http://localhost:8080/openapi") + if err != nil { + t.Fatalf("Failed to fetch OpenAPI schema: %v", err) + } + router, err := gorillamux.NewRouter(schema) + if err != nil { + t.Fatalf("Failed to create router from OpenAPI schema: %v", err) + } + + for url, status := range tt.wantEndpoints { + validateResponse(t, router, url, status) + } + + cancel() + <-finish + }) + } +} + +func fetchOpenAPISchema(url string) (*openapi3.T, error) { + ctx := context.Background() + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to GET OpenAPI schema: %w", err) + } + defer resp.Body.Close() + + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read OpenAPI schema: %w", err) + } + + loader := openapi3.NewLoader() + schema, err := loader.LoadFromData(data) + if err != nil { + return nil, fmt.Errorf("failed to load OpenAPI schema: %w", err) + } + + if err = schema.Validate(ctx); err != nil { + return nil, fmt.Errorf("OpenAPI schema validation error: %w", err) + } + + return schema, nil +} + +func validateResponse(t *testing.T, router routers.Router, url string, wantStatus int) { + t.Helper() + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, http.NoBody) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + return + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Errorf("Failed to get %s: %v", url, err) + return + } + defer resp.Body.Close() + + if resp.StatusCode != wantStatus { + t.Errorf("Want status code %d for %s, got %d", wantStatus, url, resp.StatusCode) + return + } + + if wantStatus == http.StatusOK { + if err = validateResponseSchema(router, req, resp); err != nil { + t.Errorf("Response from %q does not match schema: %v", url, err) + return + } + } + + t.Logf("Got status code %d for %s", resp.StatusCode, url) +} + +func validateResponseSchema(router routers.Router, req *http.Request, resp *http.Response) error { + route, _, err := router.FindRoute(req) + if err != nil { + return fmt.Errorf("failed to find route: %w", err) + } + + data, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed to read response body: %w", err) + } + // Reset the response body for potential further use + resp.Body = io.NopCloser(bytes.NewBuffer(data)) + + responseRef := route.Operation.Responses.Status(resp.StatusCode) + if responseRef == nil || responseRef.Value == nil { + return fmt.Errorf("no response defined in OpenAPI schema for status code %d", resp.StatusCode) + } + + mediaType := responseRef.Value.Content.Get("application/json") + if mediaType == nil { + return errors.New("no media type defined in OpenAPI schema for Content-Type 'application/json'") + } + + var body any + if err = json.Unmarshal(data, &body); err != nil { + return fmt.Errorf("failed to unmarshal response body: %w", err) + } + + // Validate the response body against the schema + err = mediaType.Schema.Value.VisitJSON(body) + if err != nil { + return fmt.Errorf("response body does not match schema: %w", err) + } + + return nil +} + +func readinessProbe(t *testing.T, url string, timeout time.Duration) { + t.Helper() + const retryInterval = 100 * time.Millisecond + deadline := time.Now().Add(timeout) + + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, http.NoBody) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + return + } + + for { + resp, err := http.DefaultClient.Do(req) + if err == nil && resp.StatusCode == http.StatusOK { + t.Log("Sparrow is ready") + resp.Body.Close() + return + } + if time.Now().After(deadline) { + t.Fatalf("Sparrow not ready [%s (%d)] after %v: %v", http.StatusText(resp.StatusCode), resp.StatusCode, timeout, err) + return + } + <-time.After(retryInterval) + } +} diff --git a/e2e/traceroute/lab.conf b/test/e2e/traceroute/lab.conf similarity index 100% rename from e2e/traceroute/lab.conf rename to test/e2e/traceroute/lab.conf diff --git a/e2e/traceroute/pc1.startup b/test/e2e/traceroute/pc1.startup similarity index 100% rename from e2e/traceroute/pc1.startup rename to test/e2e/traceroute/pc1.startup diff --git a/e2e/traceroute/pc2.startup b/test/e2e/traceroute/pc2.startup similarity index 100% rename from e2e/traceroute/pc2.startup rename to test/e2e/traceroute/pc2.startup diff --git a/e2e/traceroute/r1.startup b/test/e2e/traceroute/r1.startup similarity index 100% rename from e2e/traceroute/r1.startup rename to test/e2e/traceroute/r1.startup diff --git a/e2e/traceroute/r2.startup b/test/e2e/traceroute/r2.startup similarity index 100% rename from e2e/traceroute/r2.startup rename to test/e2e/traceroute/r2.startup diff --git a/e2e/traceroute/shared/config.yaml b/test/e2e/traceroute/shared/config.yaml similarity index 100% rename from e2e/traceroute/shared/config.yaml rename to test/e2e/traceroute/shared/config.yaml diff --git a/e2e/traceroute/shared/get_api.sh b/test/e2e/traceroute/shared/get_api.sh similarity index 100% rename from e2e/traceroute/shared/get_api.sh rename to test/e2e/traceroute/shared/get_api.sh diff --git a/e2e/traceroute/test.sh b/test/e2e/traceroute/test.sh similarity index 100% rename from e2e/traceroute/test.sh rename to test/e2e/traceroute/test.sh diff --git a/test/framework/checks.go b/test/framework/checks.go new file mode 100644 index 00000000..545cb3b7 --- /dev/null +++ b/test/framework/checks.go @@ -0,0 +1,230 @@ +package framework + +import ( + "testing" + "time" + + "github.com/caas-team/sparrow/internal/helper" + "github.com/caas-team/sparrow/pkg/checks" + "github.com/caas-team/sparrow/pkg/checks/dns" + "github.com/caas-team/sparrow/pkg/checks/health" + "github.com/caas-team/sparrow/pkg/checks/latency" + "github.com/caas-team/sparrow/pkg/checks/traceroute" + "github.com/goccy/go-yaml" +) + +type CheckBuilder interface { + Check(t *testing.T) checks.Check + YAML(t *testing.T) []byte +} + +// newCheck creates a new check with the given config. +func newCheck(t *testing.T, c checks.Check, config checks.Runtime) checks.Check { + t.Helper() + if err := config.Validate(); err != nil { + t.Fatalf("[%T] is not a valid config: %v", config, err) + } + + if err := c.UpdateConfig(config); err != nil { + t.Fatalf("[%T] failed to update config: %v", c, err) + } + return c +} + +type marshalConfig map[string]any + +func newCheckAsYAML(t *testing.T, cfg marshalConfig) []byte { + t.Helper() + out, err := yaml.Marshal(cfg) + if err != nil { + t.Fatalf("[%T] failed to marshal config: %v", cfg, err) + return []byte{} + } + return out +} + +var _ CheckBuilder = (*healthCheckBuilder)(nil) + +type healthCheckBuilder struct{ cfg health.Config } + +// NewHealthCheck returns a new health check builder. +func NewHealthCheck() *healthCheckBuilder { + return &healthCheckBuilder{cfg: health.Config{Retry: checks.DefaultRetry}} +} + +// WithTargets sets the targets for the health check. +func (b *healthCheckBuilder) WithTargets(targets []string) *healthCheckBuilder { + b.cfg.Targets = targets + return b +} + +// WithInterval sets the interval for the health check. +func (b *healthCheckBuilder) WithInterval(interval time.Duration) *healthCheckBuilder { + b.cfg.Interval = interval + return b +} + +// WithTimeout sets the timeout for the health check. +func (b *healthCheckBuilder) WithTimeout(timeout time.Duration) *healthCheckBuilder { + b.cfg.Timeout = timeout + return b +} + +// WithRetry sets the retry count and delay for the health check. +func (b *healthCheckBuilder) WithRetry(count int, delay time.Duration) *healthCheckBuilder { + b.cfg.Retry = helper.RetryConfig{Count: count, Delay: delay} + return b +} + +// Check returns the health check. +func (b *healthCheckBuilder) Check(t *testing.T) checks.Check { + t.Helper() + return newCheck(t, health.NewCheck(), &b.cfg) +} + +// YAML returns the yaml representation of the health check. +func (b *healthCheckBuilder) YAML(t *testing.T) []byte { + t.Helper() + return newCheckAsYAML(t, marshalConfig{b.cfg.For(): b.cfg}) +} + +var _ CheckBuilder = (*latencyConfigBuilder)(nil) + +type latencyConfigBuilder struct{ cfg latency.Config } + +// NewLatencyCheck returns a new latency check builder. +func NewLatencyCheck() *latencyConfigBuilder { + return &latencyConfigBuilder{cfg: latency.Config{Retry: checks.DefaultRetry}} +} + +// WithTargets sets the targets for the latency check. +func (b *latencyConfigBuilder) WithTargets(targets []string) *latencyConfigBuilder { + b.cfg.Targets = targets + return b +} + +// WithInterval sets the interval for the latency check. +func (b *latencyConfigBuilder) WithInterval(interval time.Duration) *latencyConfigBuilder { + b.cfg.Interval = interval + return b +} + +// WithTimeout sets the timeout for the latency check. +func (b *latencyConfigBuilder) WithTimeout(timeout time.Duration) *latencyConfigBuilder { + b.cfg.Timeout = timeout + return b +} + +// WithRetry sets the retry count and delay for the latency check. +func (b *latencyConfigBuilder) WithRetry(count int, delay time.Duration) *latencyConfigBuilder { + b.cfg.Retry = helper.RetryConfig{Count: count, Delay: delay} + return b +} + +// Check returns the latency check. +func (b *latencyConfigBuilder) Check(t *testing.T) checks.Check { + t.Helper() + return newCheck(t, latency.NewCheck(), &b.cfg) +} + +// YAML returns the yaml representation of the latency check. +func (b *latencyConfigBuilder) YAML(t *testing.T) []byte { + t.Helper() + return newCheckAsYAML(t, marshalConfig{b.cfg.For(): b.cfg}) +} + +var _ CheckBuilder = (*dnsConfigBuilder)(nil) + +type dnsConfigBuilder struct{ cfg dns.Config } + +// NewDNSCheck returns a new dns check builder. +func NewDNSCheck() *dnsConfigBuilder { + return &dnsConfigBuilder{cfg: dns.Config{Retry: checks.DefaultRetry}} +} + +// WithTargets sets the targets for the dns check. +func (b *dnsConfigBuilder) WithTargets(targets []string) *dnsConfigBuilder { + b.cfg.Targets = targets + return b +} + +// WithInterval sets the interval for the dns check. +func (b *dnsConfigBuilder) WithInterval(interval time.Duration) *dnsConfigBuilder { + b.cfg.Interval = interval + return b +} + +// WithTimeout sets the timeout for the dns check. +func (b *dnsConfigBuilder) WithTimeout(timeout time.Duration) *dnsConfigBuilder { + b.cfg.Timeout = timeout + return b +} + +// WithRetry sets the retry count and delay for the dns check. +func (b *dnsConfigBuilder) WithRetry(count int, delay time.Duration) *dnsConfigBuilder { + b.cfg.Retry = helper.RetryConfig{Count: count, Delay: delay} + return b +} + +// Check returns the dns check. +func (b *dnsConfigBuilder) Check(t *testing.T) checks.Check { + t.Helper() + return newCheck(t, dns.NewCheck(), &b.cfg) +} + +// YAML returns the yaml representation of the dns check. +func (b *dnsConfigBuilder) YAML(t *testing.T) []byte { + t.Helper() + return newCheckAsYAML(t, marshalConfig{b.cfg.For(): b.cfg}) +} + +var _ CheckBuilder = (*tracerouteConfigBuilder)(nil) + +type tracerouteConfigBuilder struct{ cfg traceroute.Config } + +// NewTracerouteCheck returns a new traceroute check builder. +func NewTracerouteCheck() *tracerouteConfigBuilder { + return &tracerouteConfigBuilder{cfg: traceroute.Config{Retry: checks.DefaultRetry}} +} + +// WithTargets sets the targets for the traceroute check. +func (b *tracerouteConfigBuilder) WithTargets(targets []traceroute.Target) *tracerouteConfigBuilder { + b.cfg.Targets = targets + return b +} + +// WithMaxHops sets the maximum number of hops for the traceroute check. +func (b *tracerouteConfigBuilder) WithMaxHops(maxHops int) *tracerouteConfigBuilder { + b.cfg.MaxHops = maxHops + return b +} + +// WithInterval sets the interval for the traceroute check. +func (b *tracerouteConfigBuilder) WithInterval(interval time.Duration) *tracerouteConfigBuilder { + b.cfg.Interval = interval + return b +} + +// WithTimeout sets the timeout for the traceroute check. +func (b *tracerouteConfigBuilder) WithTimeout(timeout time.Duration) *tracerouteConfigBuilder { + b.cfg.Timeout = timeout + return b +} + +// WithRetry sets the retry count and delay for the traceroute check. +func (b *tracerouteConfigBuilder) WithRetry(count int, delay time.Duration) *tracerouteConfigBuilder { + b.cfg.Retry = helper.RetryConfig{Count: count, Delay: delay} + return b +} + +// Check returns the traceroute check. +func (b *tracerouteConfigBuilder) Check(t *testing.T) checks.Check { + t.Helper() + return newCheck(t, traceroute.NewCheck(), &b.cfg) +} + +// YAML returns the yaml representation of the traceroute check. +func (b *tracerouteConfigBuilder) YAML(t *testing.T) []byte { + t.Helper() + return newCheckAsYAML(t, marshalConfig{b.cfg.For(): b.cfg}) +} diff --git a/test/framework/framework.go b/test/framework/framework.go new file mode 100644 index 00000000..a1c2047a --- /dev/null +++ b/test/framework/framework.go @@ -0,0 +1,69 @@ +package framework + +import ( + "bytes" + "context" + "os" + "testing" + + "github.com/caas-team/sparrow/pkg/config" + "github.com/caas-team/sparrow/pkg/sparrow" +) + +type Framework struct { + t *testing.T +} + +func New(t *testing.T) *Framework { + return &Framework{t: t} +} + +type E2ETest struct { + t *testing.T + sparrow *sparrow.Sparrow + buf bytes.Buffer + path string +} + +func (f *Framework) E2E(cfg *config.Config) *E2ETest { + if cfg == nil { + cfg = NewConfig().Config(f.t) + } + + return &E2ETest{ + t: f.t, + sparrow: sparrow.New(cfg), + } +} + +func (t *E2ETest) WithConfigFile(path string) *E2ETest { + t.path = path + return t +} + +func (t *E2ETest) WithCheck(builder CheckBuilder) *E2ETest { + t.buf.Write(builder.YAML(t.t)) + return t +} + +// Run runs the test. +// Runs indefinitely until the context is canceled. +func (t *E2ETest) Run(ctx context.Context) error { + if t.path == "" { + t.path = "testdata/checks.yaml" + } + + const fileMode = 0o755 + err := os.MkdirAll("testdata", fileMode) + if err != nil { + t.t.Fatalf("failed to create testdata directory: %v", err) + } + + err = os.WriteFile(t.path, t.buf.Bytes(), fileMode) + if err != nil { + t.t.Fatalf("failed to write testdata/checks.yaml: %v", err) + return err + } + + return t.sparrow.Run(ctx) +} diff --git a/test/framework/startup.go b/test/framework/startup.go new file mode 100644 index 00000000..0b6e36ce --- /dev/null +++ b/test/framework/startup.go @@ -0,0 +1,172 @@ +package framework + +import ( + "context" + "os" + "strconv" + "testing" + "time" + + "github.com/caas-team/sparrow/internal/helper" + "github.com/caas-team/sparrow/pkg/api" + "github.com/caas-team/sparrow/pkg/checks" + "github.com/caas-team/sparrow/pkg/config" + "github.com/caas-team/sparrow/pkg/sparrow/metrics" + "github.com/caas-team/sparrow/pkg/sparrow/targets" + "github.com/caas-team/sparrow/pkg/sparrow/targets/interactor" + "github.com/caas-team/sparrow/pkg/sparrow/targets/remote/gitlab" + "github.com/goccy/go-yaml" +) + +type ConfigBuilder struct{ cfg config.Config } + +func NewConfig() *ConfigBuilder { + return &ConfigBuilder{ + cfg: config.Config{ + SparrowName: "sparrow.de", + Loader: NewLoaderConfig().Build(), + Api: NewAPIConfig("localhost:8080"), + }, + } +} + +func (b *ConfigBuilder) WithName(n string) *ConfigBuilder { + b.cfg.SparrowName = n + return b +} + +func (b *ConfigBuilder) WithLoader(cfg config.LoaderConfig) *ConfigBuilder { //nolint:gocritic // Performance is not a concern here + b.cfg.Loader = cfg + return b +} + +func (b *ConfigBuilder) WithAPI(cfg api.Config) *ConfigBuilder { + b.cfg.Api = cfg + return b +} + +func (b *ConfigBuilder) WithTargetManager(cfg targets.TargetManagerConfig) *ConfigBuilder { //nolint:gocritic // Performance is not a concern here + b.cfg.TargetManager = cfg + return b +} + +func (b *ConfigBuilder) WithTelemetry(cfg metrics.Config) *ConfigBuilder { //nolint:gocritic // Performance is not a concern here + b.cfg.Telemetry = cfg + return b +} + +func (b *ConfigBuilder) Config(t *testing.T) *config.Config { + t.Helper() + if err := b.cfg.Validate(context.Background()); err != nil { + t.Fatalf("config is not valid: %v", err) + } + return &b.cfg +} + +func (b *ConfigBuilder) YAML(t *testing.T) []byte { + t.Helper() + out, err := yaml.Marshal(b.cfg) + if err != nil { + t.Fatalf("[%T] failed to marshal config: %v", b.cfg, err) + return []byte{} + } + return out +} + +type LoaderConfigBuilder struct{ cfg config.LoaderConfig } + +func NewLoaderConfig() *LoaderConfigBuilder { + return &LoaderConfigBuilder{ + cfg: config.LoaderConfig{ + Type: "file", + Interval: 0, + File: config.FileLoaderConfig{ + Path: "testdata/checks.yaml", + }, + }, + } +} + +func (b *LoaderConfigBuilder) WithInterval(i time.Duration) *LoaderConfigBuilder { + b.cfg.Interval = i + return b +} + +func (b *LoaderConfigBuilder) FromFile(path string) *LoaderConfigBuilder { + b.cfg.Type = "file" + b.cfg.File.Path = path + return b +} + +func (b *LoaderConfigBuilder) FromHTTP(cfg config.HttpLoaderConfig) *LoaderConfigBuilder { + if cfg.RetryCfg == (helper.RetryConfig{}) { + cfg.RetryCfg = checks.DefaultRetry + } + + b.cfg.Type = "http" + b.cfg.Http = cfg + return b +} + +func (b *LoaderConfigBuilder) Build() config.LoaderConfig { + return b.cfg +} + +func NewAPIConfig(address string) api.Config { + return api.Config{ListeningAddress: address} +} + +type TargetManagerConfigBuilder struct{ cfg targets.TargetManagerConfig } + +func NewTargetManagerConfig() *TargetManagerConfigBuilder { + id, _ := strconv.Atoi(os.Getenv("SPARROW_TARGETMANAGER_GITLAB_PROJECTID")) + return &TargetManagerConfigBuilder{ + cfg: targets.TargetManagerConfig{ + Enabled: true, + Type: interactor.Gitlab, + General: targets.General{ + CheckInterval: 60 * time.Second, + RegistrationInterval: 0, + UpdateInterval: 0, + UnhealthyThreshold: 0, + Scheme: "http", + }, + Config: interactor.Config{ + Gitlab: gitlab.Config{ + BaseURL: os.Getenv("SPARROW_TARGETMANAGER_GITLAB_BASEURL"), + Token: os.Getenv("SPARROW_TARGETMANAGER_GITLAB_TOKEN"), + ProjectID: id, + }, + }, + }, + } +} + +func (b *TargetManagerConfigBuilder) WithScheme(s string) *TargetManagerConfigBuilder { + b.cfg.Scheme = s + return b +} + +func (b *TargetManagerConfigBuilder) WithCheckInterval(i time.Duration) *TargetManagerConfigBuilder { + b.cfg.CheckInterval = i + return b +} + +func (b *TargetManagerConfigBuilder) WithRegistrationInterval(i time.Duration) *TargetManagerConfigBuilder { + b.cfg.RegistrationInterval = i + return b +} + +func (b *TargetManagerConfigBuilder) WithUpdateInterval(i time.Duration) *TargetManagerConfigBuilder { + b.cfg.UpdateInterval = i + return b +} + +func (b *TargetManagerConfigBuilder) WithUnhealthyThreshold(t time.Duration) *TargetManagerConfigBuilder { + b.cfg.UnhealthyThreshold = t + return b +} + +func (b *TargetManagerConfigBuilder) Build() targets.TargetManagerConfig { + return b.cfg +} From 5e956588d45e9e43b219626670a2d9c0940e28f4 Mon Sep 17 00:00:00 2001 From: lvlcn-t <75443136+lvlcn-t@users.noreply.github.com> Date: Sat, 16 Nov 2024 12:00:56 +0100 Subject: [PATCH 02/17] fix: race conditions in check shutdown & oapi version * fix: race conditions in check shutdown * fix: set openapi version on build time * refactor: check base naming Signed-off-by: lvlcn-t <75443136+lvlcn-t@users.noreply.github.com> --- .../workflows/{test_unit.yml => test_go.yml} | 6 +-- cmd/run.go | 4 +- main.go | 5 +++ pkg/checks/base.go | 16 ++++++- pkg/checks/base_test.go | 44 +++++++++++++++++++ pkg/checks/dns/dns.go | 9 +--- pkg/checks/dns/dns_test.go | 19 +------- pkg/checks/health/health.go | 10 +---- pkg/checks/health/health_test.go | 30 ------------- pkg/checks/latency/latency.go | 9 +--- pkg/checks/latency/latency_test.go | 15 ------- pkg/checks/traceroute/check.go | 10 +---- pkg/checks/traceroute/check_test.go | 2 +- pkg/config/config.go | 4 -- pkg/sparrow/controller.go | 24 +++++----- pkg/sparrow/controller_test.go | 10 ++--- pkg/sparrow/run.go | 2 +- pkg/version.go | 24 ++++++++++ test/e2e/main_test.go | 41 +++++++++-------- test/framework/checks.go | 8 ++-- 20 files changed, 149 insertions(+), 143 deletions(-) rename .github/workflows/{test_unit.yml => test_go.yml} (74%) create mode 100644 pkg/checks/base_test.go create mode 100644 pkg/version.go diff --git a/.github/workflows/test_unit.yml b/.github/workflows/test_go.yml similarity index 74% rename from .github/workflows/test_unit.yml rename to .github/workflows/test_go.yml index 5a77885f..99e53f5d 100644 --- a/.github/workflows/test_unit.yml +++ b/.github/workflows/test_go.yml @@ -1,4 +1,4 @@ -name: Test - Unit +name: Test Go on: push: @@ -19,7 +19,7 @@ jobs: with: go-version-file: go.mod - - name: Test + - name: Run all go tests run: | go mod download - go test --race --count=1 --coverprofile cover.out -v ./... + go test -race -count=1 -coverprofile cover.out -v ./... diff --git a/cmd/run.go b/cmd/run.go index 8fb1d7dc..1c1bffd0 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -66,8 +66,8 @@ func NewCmdRun() *cobra.Command { // run is the entry point to start the sparrow func run() func(cmd *cobra.Command, args []string) error { - return func(cmd *cobra.Command, _ []string) error { - cfg := &config.Config{Version: cmd.Root().Version} + return func(_ *cobra.Command, _ []string) error { + cfg := &config.Config{} err := viper.Unmarshal(cfg) if err != nil { return fmt.Errorf("failed to parse config: %w", err) diff --git a/main.go b/main.go index ba7422e4..29d8596f 100644 --- a/main.go +++ b/main.go @@ -20,12 +20,17 @@ package main import ( "github.com/caas-team/sparrow/cmd" + "github.com/caas-team/sparrow/pkg" ) // Version is the current version of sparrow // It is set at build time by using -ldflags "-X main.version=x.x.x" var version string +func init() { //nolint:gochecknoinits // Required for version to be set on build + pkg.Version = version +} + func main() { cmd.Execute(version) } diff --git a/pkg/checks/base.go b/pkg/checks/base.go index c9f45604..b3e4e61a 100644 --- a/pkg/checks/base.go +++ b/pkg/checks/base.go @@ -61,13 +61,25 @@ type Check interface { RemoveLabelledMetrics(target string) error } -// CheckBase is a struct providing common fields used by implementations of the Check interface. +// Base is a struct providing common fields and methods used by implementations of the [Check] interface. // It serves as a foundational structure that should be embedded in specific check implementations. -type CheckBase struct { +type Base struct { // Mutex for thread-safe access to shared resources within the check implementation Mu sync.Mutex // Signal channel used to notify about shutdown of a check DoneChan chan struct{} + // closed is a flag indicating if the check has been shut down. + closed bool +} + +// Shutdown closes the DoneChan to signal the check to stop running. +func (b *Base) Shutdown() { + b.Mu.Lock() + defer b.Mu.Unlock() + if !b.closed { + close(b.DoneChan) + b.closed = true + } } // Runtime is the interface that all check configurations must implement diff --git a/pkg/checks/base_test.go b/pkg/checks/base_test.go new file mode 100644 index 00000000..6f2308bb --- /dev/null +++ b/pkg/checks/base_test.go @@ -0,0 +1,44 @@ +package checks + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBase_Shutdown(t *testing.T) { + tests := []struct { + name string + b *Base + }{ + { + name: "shutdown", + b: &Base{ + DoneChan: make(chan struct{}, 1), + }, + }, + { + name: "already shutdown", + b: &Base{ + DoneChan: make(chan struct{}, 1), + closed: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.b.closed { + close(tt.b.DoneChan) + } + tt.b.Shutdown() + + if !tt.b.closed { + t.Error("Base.Shutdown() should close DoneChan") + } + + assert.Panics(t, func() { + tt.b.DoneChan <- struct{}{} + }, "Base.DoneChan should be closed") + }) + } +} diff --git a/pkg/checks/dns/dns.go b/pkg/checks/dns/dns.go index ad33ba23..d11109f7 100644 --- a/pkg/checks/dns/dns.go +++ b/pkg/checks/dns/dns.go @@ -41,7 +41,7 @@ const CheckName = "dns" // DNS is a check that resolves the names and addresses type DNS struct { - checks.CheckBase + checks.Base config Config metrics metrics client Resolver @@ -60,7 +60,7 @@ func (d *DNS) Name() string { // NewCheck creates a new instance of the dns check func NewCheck() checks.Check { return &DNS{ - CheckBase: checks.CheckBase{ + Base: checks.Base{ Mu: sync.Mutex{}, DoneChan: make(chan struct{}, 1), }, @@ -108,11 +108,6 @@ func (d *DNS) Run(ctx context.Context, cResult chan checks.ResultDTO) error { } } -func (d *DNS) Shutdown() { - d.DoneChan <- struct{}{} - close(d.DoneChan) -} - func (d *DNS) UpdateConfig(cfg checks.Runtime) error { if c, ok := cfg.(*Config); ok { d.Mu.Lock() diff --git a/pkg/checks/dns/dns_test.go b/pkg/checks/dns/dns_test.go index bcf7cc81..6be26d8b 100644 --- a/pkg/checks/dns/dns_test.go +++ b/pkg/checks/dns/dns_test.go @@ -51,7 +51,7 @@ func TestDNS_Run(t *testing.T) { name: "success with no targets", mockSetup: func() *DNS { return &DNS{ - CheckBase: checks.CheckBase{ + Base: checks.Base{ Mu: sync.Mutex{}, DoneChan: make(chan struct{}, 1), }, @@ -241,21 +241,6 @@ func TestDNS_Run_Context_Done(t *testing.T) { time.Sleep(time.Millisecond * 30) } -func TestDNS_Shutdown(t *testing.T) { - cDone := make(chan struct{}, 1) - c := DNS{ - CheckBase: checks.CheckBase{ - DoneChan: cDone, - }, - } - c.Shutdown() - - _, ok := <-cDone - if !ok { - t.Error("Shutdown() should be ok") - } -} - func TestDNS_UpdateConfig(t *testing.T) { tests := []struct { name string @@ -322,7 +307,7 @@ func stringPointer(s string) *string { func newCommonDNS() *DNS { return &DNS{ - CheckBase: checks.CheckBase{ + Base: checks.Base{ Mu: sync.Mutex{}, DoneChan: make(chan struct{}, 1), }, diff --git a/pkg/checks/health/health.go b/pkg/checks/health/health.go index 08bcdc6a..55d7a058 100644 --- a/pkg/checks/health/health.go +++ b/pkg/checks/health/health.go @@ -48,7 +48,7 @@ const CheckName = "health" // Health is a check that measures the availability of an endpoint type Health struct { - checks.CheckBase + checks.Base config Config metrics metrics } @@ -56,7 +56,7 @@ type Health struct { // NewCheck creates a new instance of the health check func NewCheck() checks.Check { return &Health{ - CheckBase: checks.CheckBase{ + Base: checks.Base{ Mu: sync.Mutex{}, DoneChan: make(chan struct{}, 1), }, @@ -97,12 +97,6 @@ func (h *Health) Run(ctx context.Context, cResult chan checks.ResultDTO) error { } } -// Shutdown is called once when the check is unregistered or sparrow shuts down -func (h *Health) Shutdown() { - h.DoneChan <- struct{}{} - close(h.DoneChan) -} - // UpdateConfig sets the configuration for the health check func (h *Health) UpdateConfig(cfg checks.Runtime) error { if c, ok := cfg.(*Config); ok { diff --git a/pkg/checks/health/health_test.go b/pkg/checks/health/health_test.go index 77c91c97..ee83bb9c 100644 --- a/pkg/checks/health/health_test.go +++ b/pkg/checks/health/health_test.go @@ -247,33 +247,3 @@ func TestHealth_Check(t *testing.T) { }) } } - -func TestHealth_Shutdown(t *testing.T) { - cDone := make(chan struct{}, 1) - c := Health{ - CheckBase: checks.CheckBase{ - DoneChan: cDone, - }, - } - c.Shutdown() - - if _, ok := <-cDone; !ok { - t.Error("Channel should be done") - } - - assert.Panics(t, func() { - cDone <- struct{}{} - }, "Channel is closed, should panic") - - hc := NewCheck() - hc.Shutdown() - - _, ok := <-hc.(*Health).DoneChan - if !ok { - t.Error("Channel should be done") - } - - assert.Panics(t, func() { - hc.(*Health).DoneChan <- struct{}{} - }, "Channel is closed, should panic") -} diff --git a/pkg/checks/latency/latency.go b/pkg/checks/latency/latency.go index da21b435..d9c0b63c 100644 --- a/pkg/checks/latency/latency.go +++ b/pkg/checks/latency/latency.go @@ -43,7 +43,7 @@ const CheckName = "latency" // Latency is a check that measures the latency to an endpoint type Latency struct { - checks.CheckBase + checks.Base config Config metrics metrics } @@ -51,7 +51,7 @@ type Latency struct { // NewCheck creates a new instance of the latency check func NewCheck() checks.Check { return &Latency{ - CheckBase: checks.CheckBase{ + Base: checks.Base{ Mu: sync.Mutex{}, DoneChan: make(chan struct{}, 1), }, @@ -98,11 +98,6 @@ func (l *Latency) Run(ctx context.Context, cResult chan checks.ResultDTO) error } } -func (l *Latency) Shutdown() { - l.DoneChan <- struct{}{} - close(l.DoneChan) -} - // UpdateConfig sets the configuration for the latency check func (l *Latency) UpdateConfig(cfg checks.Runtime) error { if c, ok := cfg.(*Config); ok { diff --git a/pkg/checks/latency/latency_test.go b/pkg/checks/latency/latency_test.go index 78a7a58c..d4155cbf 100644 --- a/pkg/checks/latency/latency_test.go +++ b/pkg/checks/latency/latency_test.go @@ -305,21 +305,6 @@ func TestLatency_check(t *testing.T) { } } -func TestLatency_Shutdown(t *testing.T) { - cDone := make(chan struct{}, 1) - c := Latency{ - CheckBase: checks.CheckBase{ - DoneChan: cDone, - }, - } - c.Shutdown() - - _, ok := <-cDone - if !ok { - t.Error("Shutdown() should be ok") - } -} - func TestLatency_UpdateConfig(t *testing.T) { c := Latency{} wantCfg := Config{ diff --git a/pkg/checks/traceroute/check.go b/pkg/checks/traceroute/check.go index b6c00155..5ea3c820 100644 --- a/pkg/checks/traceroute/check.go +++ b/pkg/checks/traceroute/check.go @@ -53,7 +53,7 @@ func (t Target) String() string { func NewCheck() checks.Check { c := &Traceroute{ - CheckBase: checks.CheckBase{ + Base: checks.Base{ Mu: sync.Mutex{}, DoneChan: make(chan struct{}, 1), }, @@ -66,7 +66,7 @@ func NewCheck() checks.Check { } type Traceroute struct { - checks.CheckBase + checks.Base config Config traceroute tracerouteFactory metrics metrics @@ -212,12 +212,6 @@ func (tr *Traceroute) check(ctx context.Context) map[string]result { return res } -// Shutdown is called once when the check is unregistered or sparrow shuts down -func (tr *Traceroute) Shutdown() { - tr.DoneChan <- struct{}{} - close(tr.DoneChan) -} - // UpdateConfig is called once when the check is registered // This is also called while the check is running, if the remote config is updated // This should return an error if the config is invalid diff --git a/pkg/checks/traceroute/check_test.go b/pkg/checks/traceroute/check_test.go index 89ff9911..37bea923 100644 --- a/pkg/checks/traceroute/check_test.go +++ b/pkg/checks/traceroute/check_test.go @@ -79,7 +79,7 @@ func newForTest(f tracerouteFactory, maxHops int, targets []string) *Traceroute t[i] = Target{Addr: target} } return &Traceroute{ - CheckBase: checks.CheckBase{Mu: sync.Mutex{}, DoneChan: make(chan struct{})}, + Base: checks.Base{Mu: sync.Mutex{}, DoneChan: make(chan struct{})}, config: Config{Targets: t, MaxHops: maxHops}, traceroute: f, metrics: newMetrics(), diff --git a/pkg/config/config.go b/pkg/config/config.go index 0169e08f..f1c4b81c 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -29,10 +29,6 @@ import ( ) type Config struct { - // Version is the version of the sparrow. - // This is set at build time by using -ldflags "-X main.version=x.x.x" - // and is not part of the configuration file or flags. - Version string `yaml:"-" mapstructure:"-"` // SparrowName is the DNS name of the sparrow SparrowName string `yaml:"name" mapstructure:"name"` // Loader is the configuration for the loader diff --git a/pkg/sparrow/controller.go b/pkg/sparrow/controller.go index 4fcb33b1..de14339a 100644 --- a/pkg/sparrow/controller.go +++ b/pkg/sparrow/controller.go @@ -23,8 +23,10 @@ import ( "errors" "fmt" "net/http" + stdruntime "runtime" "github.com/caas-team/sparrow/internal/logger" + "github.com/caas-team/sparrow/pkg" "github.com/caas-team/sparrow/pkg/checks" "github.com/caas-team/sparrow/pkg/checks/runtime" "github.com/caas-team/sparrow/pkg/db" @@ -41,13 +43,10 @@ type ChecksController struct { cResult chan checks.ResultDTO cErr chan error done chan struct{} - // version is the version of the sparrow. - // This is set at build time by using -ldflags "-X main.version=x.x.x". - version string } // NewChecksController creates a new ChecksController. -func NewChecksController(dbase db.DB, m metrics.Provider, version string) *ChecksController { +func NewChecksController(dbase db.DB, m metrics.Provider) *ChecksController { return &ChecksController{ db: dbase, metrics: m, @@ -55,7 +54,6 @@ func NewChecksController(dbase db.DB, m metrics.Provider, version string) *Check cResult: make(chan checks.ResultDTO, 8), //nolint:mnd // Buffered channel to avoid blocking the checks cErr: make(chan error, 1), done: make(chan struct{}, 1), - version: version, } } @@ -76,7 +74,6 @@ func (cc *ChecksController) Run(ctx context.Context) error { case <-ctx.Done(): return ctx.Err() case <-cc.done: - cc.cErr <- nil return nil } } @@ -90,7 +87,6 @@ func (cc *ChecksController) Shutdown(ctx context.Context) { for _, c := range cc.checks.Iter() { cc.UnregisterCheck(ctx, c) } - cc.done <- struct{}{} close(cc.done) close(cc.cResult) } @@ -176,8 +172,17 @@ func (cc *ChecksController) UnregisterCheck(ctx context.Context, check checks.Ch var oapiBoilerplate = openapi3.T{ OpenAPI: "3.0.0", Info: &openapi3.Info{ - Title: "Sparrow Metrics API", - Version: "0.5.0", + Title: "Sparrow Metrics API", + Version: func() string { + version := pkg.Version + if version == "" { + return fmt.Sprintf("0.0.0-dev-%s", stdruntime.Version()) + } + if version[0] == 'v' { + return version[1:] + } + return version + }(), Description: "Serves metrics collected by sparrows checks", Contact: &openapi3.Contact{ URL: "https://caas.telekom.de", @@ -204,7 +209,6 @@ var oapiBoilerplate = openapi3.T{ func (cc *ChecksController) GenerateCheckSpecs(ctx context.Context) (openapi3.T, error) { log := logger.FromContext(ctx) doc := oapiBoilerplate - doc.Info.Version = cc.version for _, c := range cc.checks.Iter() { name := c.Name() ref, err := c.Schema() diff --git a/pkg/sparrow/controller_test.go b/pkg/sparrow/controller_test.go index 253ca52a..f68887db 100644 --- a/pkg/sparrow/controller_test.go +++ b/pkg/sparrow/controller_test.go @@ -42,7 +42,7 @@ func TestRun_CheckRunError(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - cc := NewChecksController(db.NewInMemory(), metrics.New(metrics.Config{}), "") + cc := NewChecksController(db.NewInMemory(), metrics.New(metrics.Config{})) mockCheck := &checks.CheckMock{ NameFunc: func() string { return "mockCheck" }, RunFunc: func(ctx context.Context, cResult chan checks.ResultDTO) error { @@ -82,7 +82,7 @@ func TestRun_CheckRunError(t *testing.T) { func TestRun_ContextCancellation(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) - cc := NewChecksController(db.NewInMemory(), metrics.New(metrics.Config{}), "") + cc := NewChecksController(db.NewInMemory(), metrics.New(metrics.Config{})) done := make(chan struct{}) go func() { @@ -206,7 +206,7 @@ func TestChecksController_Reconcile(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - cc := NewChecksController(db.NewInMemory(), metrics.New(metrics.Config{}), "") + cc := NewChecksController(db.NewInMemory(), metrics.New(metrics.Config{})) for _, c := range tt.checks { cc.checks.Add(c) @@ -244,7 +244,7 @@ func TestChecksController_RegisterCheck(t *testing.T) { { name: "register one check", setup: func() *ChecksController { - return NewChecksController(db.NewInMemory(), metrics.New(metrics.Config{}), "") + return NewChecksController(db.NewInMemory(), metrics.New(metrics.Config{})) }, check: health.NewCheck(), }, @@ -274,7 +274,7 @@ func TestChecksController_UnregisterCheck(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - cc := NewChecksController(db.NewInMemory(), metrics.New(metrics.Config{}), "") + cc := NewChecksController(db.NewInMemory(), metrics.New(metrics.Config{})) cc.UnregisterCheck(context.Background(), tt.check) diff --git a/pkg/sparrow/run.go b/pkg/sparrow/run.go index cbaae5d6..742f95da 100644 --- a/pkg/sparrow/run.go +++ b/pkg/sparrow/run.go @@ -74,7 +74,7 @@ func New(cfg *config.Config) *Sparrow { db: dbase, api: api.New(cfg.Api), metrics: m, - controller: NewChecksController(dbase, m, cfg.Version), + controller: NewChecksController(dbase, m), cRuntime: make(chan runtime.Config, 1), cErr: make(chan error, 1), cDone: make(chan struct{}, 1), diff --git a/pkg/version.go b/pkg/version.go new file mode 100644 index 00000000..0134fe93 --- /dev/null +++ b/pkg/version.go @@ -0,0 +1,24 @@ +// sparrow +// (C) 2024, Deutsche Telekom IT GmbH +// +// Deutsche Telekom IT GmbH and all other contributors / +// copyright owners license this file to you under the Apache +// License, Version 2.0 (the "License"); you may not use this +// file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// Package pkg contains metadata about the sparrow. +package pkg + +// Version is the current version of sparrow. +// It is set at build time by using -ldflags "-X main.version=x.x.x". +var Version string diff --git a/test/e2e/main_test.go b/test/e2e/main_test.go index 12ebbc03..75f84042 100644 --- a/test/e2e/main_test.go +++ b/test/e2e/main_test.go @@ -17,19 +17,22 @@ import ( "github.com/getkin/kin-openapi/routers/gorillamux" ) +const ( + checkInterval = 20 * time.Second + checkTimeout = 15 * time.Second +) + func TestSparrow_E2E(t *testing.T) { if testing.Short() { t.Skip("skipping e2e tests") } - type test struct { + tests := []struct { name string startup framework.ConfigBuilder checks []framework.CheckBuilder wantEndpoints map[string]int - } - - tests := []test{ + }{ { name: "no checks", startup: *framework.NewConfig(), @@ -46,9 +49,9 @@ func TestSparrow_E2E(t *testing.T) { startup: *framework.NewConfig(), checks: []framework.CheckBuilder{ framework.NewHealthCheck(). - WithInterval(20 * time.Second). - WithTimeout(15 * time.Second). - WithTargets([]string{"https://www.example.com/", "https://www.google.com/"}), + WithInterval(checkInterval). + WithTimeout(checkTimeout). + WithTargets("https://www.example.com/", "https://www.google.com/"), }, wantEndpoints: map[string]int{ "http://localhost:8080/v1/metrics/health": http.StatusOK, @@ -62,17 +65,17 @@ func TestSparrow_E2E(t *testing.T) { startup: *framework.NewConfig(), checks: []framework.CheckBuilder{ framework.NewHealthCheck(). - WithInterval(20 * time.Second). - WithTimeout(15 * time.Second). - WithTargets([]string{"https://www.example.com/"}), + WithInterval(checkInterval). + WithTimeout(checkTimeout). + WithTargets("https://www.example.com/"), framework.NewLatencyCheck(). - WithInterval(20 * time.Second). - WithTimeout(15 * time.Second). - WithTargets([]string{"https://www.example.com/"}), + WithInterval(checkInterval). + WithTimeout(checkTimeout). + WithTargets("https://www.example.com/"), framework.NewDNSCheck(). - WithInterval(20 * time.Second). - WithTimeout(15 * time.Second). - WithTargets([]string{"www.example.com"}), + WithInterval(checkInterval). + WithTimeout(checkTimeout). + WithTargets("www.example.com"), }, wantEndpoints: map[string]int{ "http://localhost:8080/v1/metrics/health": http.StatusOK, @@ -101,15 +104,15 @@ func TestSparrow_E2E(t *testing.T) { }() // Wait for sparrow to be ready with a readiness probe. - readinessProbe(t, "http://localhost:8080", 15*time.Second) + readinessProbe(t, "http://localhost:8080", checkTimeout) // Wait for the checks to be executed. wait := 5 * time.Second if len(tt.checks) > 0 { - wait = 35 * time.Second + wait = checkInterval + checkTimeout + 5*time.Second } + t.Logf("Waiting %s for checks to be executed", wait.String()) <-time.After(wait) - t.Logf("Waited %s for checks to be executed", wait.String()) // Fetch, parse and create a new router from the OpenAPI schema, to be able to validate the responses. schema, err := fetchOpenAPISchema("http://localhost:8080/openapi") diff --git a/test/framework/checks.go b/test/framework/checks.go index 545cb3b7..e8ccca02 100644 --- a/test/framework/checks.go +++ b/test/framework/checks.go @@ -53,7 +53,7 @@ func NewHealthCheck() *healthCheckBuilder { } // WithTargets sets the targets for the health check. -func (b *healthCheckBuilder) WithTargets(targets []string) *healthCheckBuilder { +func (b *healthCheckBuilder) WithTargets(targets ...string) *healthCheckBuilder { b.cfg.Targets = targets return b } @@ -98,7 +98,7 @@ func NewLatencyCheck() *latencyConfigBuilder { } // WithTargets sets the targets for the latency check. -func (b *latencyConfigBuilder) WithTargets(targets []string) *latencyConfigBuilder { +func (b *latencyConfigBuilder) WithTargets(targets ...string) *latencyConfigBuilder { b.cfg.Targets = targets return b } @@ -143,7 +143,7 @@ func NewDNSCheck() *dnsConfigBuilder { } // WithTargets sets the targets for the dns check. -func (b *dnsConfigBuilder) WithTargets(targets []string) *dnsConfigBuilder { +func (b *dnsConfigBuilder) WithTargets(targets ...string) *dnsConfigBuilder { b.cfg.Targets = targets return b } @@ -188,7 +188,7 @@ func NewTracerouteCheck() *tracerouteConfigBuilder { } // WithTargets sets the targets for the traceroute check. -func (b *tracerouteConfigBuilder) WithTargets(targets []traceroute.Target) *tracerouteConfigBuilder { +func (b *tracerouteConfigBuilder) WithTargets(targets ...traceroute.Target) *tracerouteConfigBuilder { b.cfg.Targets = targets return b } From f26de9eca52913628fb57219037cb613886bc855 Mon Sep 17 00:00:00 2001 From: lvlcn-t <75443136+lvlcn-t@users.noreply.github.com> Date: Sat, 16 Nov 2024 13:35:45 +0100 Subject: [PATCH 03/17] fix: gosec SAST scan ci job * fix: gosec SAST scan ci job * feat: upload gosec report to github security page Signed-off-by: lvlcn-t <75443136+lvlcn-t@users.noreply.github.com> --- .github/workflows/test_sast.yml | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test_sast.yml b/.github/workflows/test_sast.yml index 811828f3..0049b59f 100644 --- a/.github/workflows/test_sast.yml +++ b/.github/workflows/test_sast.yml @@ -2,6 +2,9 @@ name: Test - SAST on: push: + schedule: + # Schedule the workflow to run at 00:00 on Sunday UTC time. + - cron: "0 0 * * 0" permissions: contents: read @@ -9,15 +12,16 @@ permissions: jobs: tests: runs-on: ubuntu-latest - env: GO111MODULE: on - + GOFLAGS: "-buildvcs=false" steps: - - name: Checkout repository - uses: actions/checkout@v4 - + - uses: actions/checkout@v4 - name: Run Gosec Security Scanner uses: securego/gosec@master with: - args: ./... + args: "-no-fail -fmt sarif -out results.sarif ./..." + - name: Upload SARIF file + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: results.sarif From fbf85c359073b21a4ba6c1bf29f4a1e9803e599b Mon Sep 17 00:00:00 2001 From: lvlcn-t <75443136+lvlcn-t@users.noreply.github.com> Date: Sat, 16 Nov 2024 13:44:32 +0100 Subject: [PATCH 04/17] ci: rename workflows to kebab-case & introduce naming pattern * ci: rename workflows to kebab-case * chore: introduce naming pattern for test workflows * chore: bump e2e k3s v1.31.2-k3s1 Signed-off-by: lvlcn-t <75443136+lvlcn-t@users.noreply.github.com> --- .github/workflows/end2end.yml | 87 ------------------- .github/workflows/test-e2e-k8s.yml | 87 +++++++++++++++++++ ...e2e_checks.yml => test-e2e-traceroute.yml} | 2 +- .../workflows/{test_go.yml => test-go.yml} | 2 +- .../{test_sast.yml => test-sast.yml} | 0 5 files changed, 89 insertions(+), 89 deletions(-) delete mode 100644 .github/workflows/end2end.yml create mode 100644 .github/workflows/test-e2e-k8s.yml rename .github/workflows/{e2e_checks.yml => test-e2e-traceroute.yml} (97%) rename .github/workflows/{test_go.yml => test-go.yml} (96%) rename .github/workflows/{test_sast.yml => test-sast.yml} (100%) diff --git a/.github/workflows/end2end.yml b/.github/workflows/end2end.yml deleted file mode 100644 index 38ecbf12..00000000 --- a/.github/workflows/end2end.yml +++ /dev/null @@ -1,87 +0,0 @@ -# This workflow installs 1 instance of sparrow and -# verify the API output - -name: End2End Testing -on: - push: - -jobs: - end2end: - runs-on: ubuntu-latest - steps: - - name: Checkout Repo - uses: actions/checkout@v4 - - name: Set up K3S - uses: debianmaster/actions-k3s@master - id: k3s - with: - version: 'v1.26.9-k3s1' - - name: Check Cluster - run: | - kubectl get nodes - - name: Check Coredns Deployment - run: | - kubectl -n kube-system rollout status deployment/coredns --timeout=60s - STATUS=$(kubectl -n kube-system get deployment coredns -o jsonpath={.status.readyReplicas}) - if [[ $STATUS -ne 1 ]] - then - echo "Deployment coredns not ready" - kubectl -n kube-system get events - exit 1 - else - echo "Deployment coredns OK" - fi - - name: Check Metricsserver Deployment - run: | - kubectl -n kube-system rollout status deployment/metrics-server --timeout=60s - STATUS=$(kubectl -n kube-system get deployment metrics-server -o jsonpath={.status.readyReplicas}) - if [[ $STATUS -ne 1 ]] - then - echo "Deployment metrics-server not ready" - kubectl -n kube-system get events - exit 1 - else - echo "Deployment metrics-server OK" - fi - - name: Setup Helm - run: | - curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash - helm version - - name: Get Image Tag - id: version - run: echo "value=commit-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT - - name: Install Sparrow - run: | - helm upgrade -i sparrow \ - --atomic \ - --timeout 300s \ - --set image.tag=${{ steps.version.outputs.value }} \ - --set sparrowConfig.name=the-sparrow.com \ - --set sparrowConfig.loader.type=file \ - --set sparrowConfig.loader.interval=5s \ - --set sparrowConfig.loader.file.path=/config/.sparrow.yaml \ - --set checksConfig.health.interval=1s \ - --set checksConfig.health.timeout=1s \ - ./chart - - - name: Check Pods - run: | - kubectl get pods - - name: Wait for Sparrow - run: | - sleep 60 - - name: Healthcheck - run: | - kubectl create job curl --image=quay.io/curl/curl:latest -- curl -f -v -H 'Content-Type: application/json' http://sparrow:8080/v1/metrics/health - kubectl wait --for=condition=complete job/curl - STATUS=$(kubectl get job curl -o jsonpath={.status.succeeded}) - if [[ $STATUS -ne 1 ]] - then - echo "Job failed" - kubectl logs -ljob-name=curl - kubectl delete job curl - exit 1 - else - echo "Job OK" - kubectl delete job curl - fi diff --git a/.github/workflows/test-e2e-k8s.yml b/.github/workflows/test-e2e-k8s.yml new file mode 100644 index 00000000..25949713 --- /dev/null +++ b/.github/workflows/test-e2e-k8s.yml @@ -0,0 +1,87 @@ +# This workflow installs 1 instance of sparrow and +# verify the API output + +name: Test - E2E - Kubernetes +on: + push: + +jobs: + end2end: + runs-on: ubuntu-latest + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + - name: Set up K3S + uses: debianmaster/actions-k3s@master + id: k3s + with: + version: "v1.31.2-k3s1" + - name: Check Cluster + run: | + kubectl get nodes + - name: Check Coredns Deployment + run: | + kubectl -n kube-system rollout status deployment/coredns --timeout=60s + STATUS=$(kubectl -n kube-system get deployment coredns -o jsonpath={.status.readyReplicas}) + if [[ $STATUS -ne 1 ]] + then + echo "Deployment coredns not ready" + kubectl -n kube-system get events + exit 1 + else + echo "Deployment coredns OK" + fi + - name: Check Metricsserver Deployment + run: | + kubectl -n kube-system rollout status deployment/metrics-server --timeout=60s + STATUS=$(kubectl -n kube-system get deployment metrics-server -o jsonpath={.status.readyReplicas}) + if [[ $STATUS -ne 1 ]] + then + echo "Deployment metrics-server not ready" + kubectl -n kube-system get events + exit 1 + else + echo "Deployment metrics-server OK" + fi + - name: Setup Helm + run: | + curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash + helm version + - name: Get Image Tag + id: version + run: echo "value=commit-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + - name: Install Sparrow + run: | + helm upgrade -i sparrow \ + --atomic \ + --timeout 300s \ + --set image.tag=${{ steps.version.outputs.value }} \ + --set sparrowConfig.name=the-sparrow.com \ + --set sparrowConfig.loader.type=file \ + --set sparrowConfig.loader.interval=5s \ + --set sparrowConfig.loader.file.path=/config/.sparrow.yaml \ + --set checksConfig.health.interval=1s \ + --set checksConfig.health.timeout=1s \ + ./chart + + - name: Check Pods + run: | + kubectl get pods + - name: Wait for Sparrow + run: | + sleep 60 + - name: Healthcheck + run: | + kubectl create job curl --image=quay.io/curl/curl:latest -- curl -f -v -H 'Content-Type: application/json' http://sparrow:8080/v1/metrics/health + kubectl wait --for=condition=complete job/curl + STATUS=$(kubectl get job curl -o jsonpath={.status.succeeded}) + if [[ $STATUS -ne 1 ]] + then + echo "Job failed" + kubectl logs -ljob-name=curl + kubectl delete job curl + exit 1 + else + echo "Job OK" + kubectl delete job curl + fi diff --git a/.github/workflows/e2e_checks.yml b/.github/workflows/test-e2e-traceroute.yml similarity index 97% rename from .github/workflows/e2e_checks.yml rename to .github/workflows/test-e2e-traceroute.yml index e3fb772b..643c4213 100644 --- a/.github/workflows/e2e_checks.yml +++ b/.github/workflows/test-e2e-traceroute.yml @@ -1,4 +1,4 @@ -name: E2E - Traceroute Check +name: Test - E2E - Traceroute Check on: [push] diff --git a/.github/workflows/test_go.yml b/.github/workflows/test-go.yml similarity index 96% rename from .github/workflows/test_go.yml rename to .github/workflows/test-go.yml index 99e53f5d..cab2fe35 100644 --- a/.github/workflows/test_go.yml +++ b/.github/workflows/test-go.yml @@ -1,4 +1,4 @@ -name: Test Go +name: Test - Go on: push: diff --git a/.github/workflows/test_sast.yml b/.github/workflows/test-sast.yml similarity index 100% rename from .github/workflows/test_sast.yml rename to .github/workflows/test-sast.yml From cb36ca05be92535ae1b775d19a0f1bcaf065fd89 Mon Sep 17 00:00:00 2001 From: lvlcn-t <75443136+lvlcn-t@users.noreply.github.com> Date: Sat, 16 Nov 2024 13:46:54 +0100 Subject: [PATCH 05/17] fix: update permissions for SAST workflow to allow security events Signed-off-by: lvlcn-t <75443136+lvlcn-t@users.noreply.github.com> --- .github/workflows/test-sast.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test-sast.yml b/.github/workflows/test-sast.yml index 0049b59f..dea65d03 100644 --- a/.github/workflows/test-sast.yml +++ b/.github/workflows/test-sast.yml @@ -8,6 +8,7 @@ on: permissions: contents: read + security-events: write jobs: tests: From 41c7b1f1cf1a4b8f5898bf57fa13d712f4ea78dd Mon Sep 17 00:00:00 2001 From: lvlcn-t <75443136+lvlcn-t@users.noreply.github.com> Date: Sat, 16 Nov 2024 14:15:10 +0100 Subject: [PATCH 06/17] chore: more CI naming changes Signed-off-by: lvlcn-t <75443136+lvlcn-t@users.noreply.github.com> --- .github/workflows/ci.yml | 15 +++++++-------- .github/workflows/pre-commit.yml | 1 + .github/workflows/prune.yml | 5 ++--- .github/workflows/release.yml | 7 +++---- .github/workflows/test-e2e-k8s.yml | 10 ++++------ .github/workflows/test-e2e-traceroute.yml | 3 ++- .github/workflows/test-go.yml | 7 +++---- .github/workflows/test-sast.yml | 2 +- 8 files changed, 23 insertions(+), 27 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 740a5b56..df859c2e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,7 +1,6 @@ name: Continuous Integration -on: - push: +on: [push] permissions: contents: write @@ -9,8 +8,8 @@ permissions: security-events: write jobs: - rel: - name: Build, scan & push Snapshot + snapshot: + name: Build Snapshot runs-on: ubuntu-latest steps: - name: Checkout repository @@ -64,17 +63,17 @@ jobs: docker push ghcr.io/${{ github.repository }}:${{ steps.version.outputs.value }} docker push mtr.devops.telekom.de/sparrow/sparrow:${{ steps.version.outputs.value }} - helm: + name: Build Helm Chart runs-on: ubuntu-latest steps: - name: Checkout Repo uses: actions/checkout@v4 - with: - fetch-tags: true + # We don't use checkout/fetch-tags: true because it's broken + # For more information see: https://github.com/actions/checkout/issues/1471 - name: Fetch tags explicitly - run: git fetch --tags + run: git fetch --prune --unshallow --tags - name: Get App Version id: appVersion diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index bc19f374..2693e2b9 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -4,6 +4,7 @@ on: [pull_request] jobs: pre-commit: + name: Run pre-commit hooks runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/prune.yml b/.github/workflows/prune.yml index 98eb770c..0b6892d4 100644 --- a/.github/workflows/prune.yml +++ b/.github/workflows/prune.yml @@ -10,10 +10,9 @@ permissions: security-events: write jobs: - prune_images: - name: Prune old sparrow images + prune: + name: Images and Charts runs-on: ubuntu-latest - steps: - name: Prune Images uses: vlaurin/action-ghcr-prune@v0.6.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ca399963..ccad7786 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,11 +11,10 @@ permissions: packages: write jobs: - main: + release: name: Release Sparrow runs-on: ubuntu-latest steps: - - name: Checkout repository uses: actions/checkout@v4 @@ -45,8 +44,8 @@ jobs: args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - helm: + name: Release Helm Chart runs-on: ubuntu-latest steps: - name: Checkout Repo @@ -66,4 +65,4 @@ jobs: - name: Push helm package run: | helm push $(ls ./chart/*.tgz| head -1) oci://ghcr.io/${{ github.repository_owner }}/charts - helm push $(ls ./chart/*.tgz| head -1) oci://mtr.devops.telekom.de/sparrow/charts \ No newline at end of file + helm push $(ls ./chart/*.tgz| head -1) oci://mtr.devops.telekom.de/sparrow/charts diff --git a/.github/workflows/test-e2e-k8s.yml b/.github/workflows/test-e2e-k8s.yml index 25949713..c22f445a 100644 --- a/.github/workflows/test-e2e-k8s.yml +++ b/.github/workflows/test-e2e-k8s.yml @@ -1,12 +1,10 @@ -# This workflow installs 1 instance of sparrow and -# verify the API output - name: Test - E2E - Kubernetes -on: - push: + +on: [push] jobs: - end2end: + test: + name: Test runs-on: ubuntu-latest steps: - name: Checkout Repo diff --git a/.github/workflows/test-e2e-traceroute.yml b/.github/workflows/test-e2e-traceroute.yml index 643c4213..08fc6cfe 100644 --- a/.github/workflows/test-e2e-traceroute.yml +++ b/.github/workflows/test-e2e-traceroute.yml @@ -6,7 +6,8 @@ permissions: contents: read jobs: - test_e2e: + test: + name: Test runs-on: ubuntu-latest steps: - name: Checkout repository diff --git a/.github/workflows/test-go.yml b/.github/workflows/test-go.yml index cab2fe35..4d55fdec 100644 --- a/.github/workflows/test-go.yml +++ b/.github/workflows/test-go.yml @@ -1,15 +1,14 @@ name: Test - Go -on: - push: +on: [push] permissions: contents: read jobs: - test_go: + test: + name: Test runs-on: ubuntu-latest - steps: - name: Checkout repository uses: actions/checkout@v4 diff --git a/.github/workflows/test-sast.yml b/.github/workflows/test-sast.yml index dea65d03..d3d8adbe 100644 --- a/.github/workflows/test-sast.yml +++ b/.github/workflows/test-sast.yml @@ -11,7 +11,7 @@ permissions: security-events: write jobs: - tests: + test: runs-on: ubuntu-latest env: GO111MODULE: on From 5937af4fce4b90f5724b8d9a0dad4ab26e4b482b Mon Sep 17 00:00:00 2001 From: lvlcn-t <75443136+lvlcn-t@users.noreply.github.com> Date: Sat, 16 Nov 2024 14:22:46 +0100 Subject: [PATCH 07/17] refactor: merge test workflow files into one Signed-off-by: lvlcn-t <75443136+lvlcn-t@users.noreply.github.com> --- .github/workflows/test-e2e-traceroute.yml | 55 ----------- .github/workflows/test-go.yml | 24 ----- .github/workflows/test-sast.yml | 6 +- .../workflows/{test-e2e-k8s.yml => test.yml} | 93 ++++++++++++++++--- 4 files changed, 85 insertions(+), 93 deletions(-) delete mode 100644 .github/workflows/test-e2e-traceroute.yml delete mode 100644 .github/workflows/test-go.yml rename .github/workflows/{test-e2e-k8s.yml => test.yml} (57%) diff --git a/.github/workflows/test-e2e-traceroute.yml b/.github/workflows/test-e2e-traceroute.yml deleted file mode 100644 index 08fc6cfe..00000000 --- a/.github/workflows/test-e2e-traceroute.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: Test - E2E - Traceroute Check - -on: [push] - -permissions: - contents: read - -jobs: - test: - name: Test - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - - name: Install dependencies - run: | - sudo add-apt-repository ppa:katharaframework/kathara - sudo apt-get update - sudo apt-get install -y jq kathara - - name: Setup kathara - run: | - echo '{ - "image": "kathara/base", - "manager_type": "docker", - "terminal": "/usr/bin/xterm", - "open_terminals": false, - "device_shell": "/bin/bash", - "net_prefix": "kathara", - "device_prefix": "kathara", - "debug_level": "INFO", - "print_startup_log": true, - "enable_ipv6": false, - "last_checked": 1721834897.2415252, - "hosthome_mount": false, - "shared_mount": true, - "image_update_policy": "Prompt", - "shared_cds": 1, - "remote_url": null, - "cert_path": null, - "network_plugin": "kathara/katharanp_vde" - }' > ~/.config/kathara.conf - - - name: Build binary for e2e - uses: goreleaser/goreleaser-action@v6 - with: - version: latest - args: build --single-target --clean --snapshot --config .goreleaser-ci.yaml - - - name: Run e2e tests - run: | - ./scripts/run_e2e_tests.sh diff --git a/.github/workflows/test-go.yml b/.github/workflows/test-go.yml deleted file mode 100644 index 4d55fdec..00000000 --- a/.github/workflows/test-go.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Test - Go - -on: [push] - -permissions: - contents: read - -jobs: - test: - name: Test - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version-file: go.mod - - - name: Run all go tests - run: | - go mod download - go test -race -count=1 -coverprofile cover.out -v ./... diff --git a/.github/workflows/test-sast.yml b/.github/workflows/test-sast.yml index d3d8adbe..f66bc03b 100644 --- a/.github/workflows/test-sast.yml +++ b/.github/workflows/test-sast.yml @@ -1,9 +1,8 @@ -name: Test - SAST +name: SAST on: push: schedule: - # Schedule the workflow to run at 00:00 on Sunday UTC time. - cron: "0 0 * * 0" permissions: @@ -11,7 +10,8 @@ permissions: security-events: write jobs: - test: + go: + name: Go - Tests runs-on: ubuntu-latest env: GO111MODULE: on diff --git a/.github/workflows/test-e2e-k8s.yml b/.github/workflows/test.yml similarity index 57% rename from .github/workflows/test-e2e-k8s.yml rename to .github/workflows/test.yml index c22f445a..451ac702 100644 --- a/.github/workflows/test-e2e-k8s.yml +++ b/.github/workflows/test.yml @@ -1,22 +1,89 @@ -name: Test - E2E - Kubernetes +name: Tests on: [push] +permissions: + contents: read + jobs: - test: - name: Test + go: + # TODO: Split go tests into multiple jobs to reduce the time + # e.g. go-unit, go-e2e + name: Go - Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Run all go tests + run: | + go mod download + go test -race -count=1 -coverprofile cover.out -v ./... + + traceroute: + name: E2E - Traceroute + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install dependencies + run: | + sudo add-apt-repository ppa:katharaframework/kathara + sudo apt-get update + sudo apt-get install -y jq kathara + + - name: Setup kathara + run: | + echo '{ + "image": "kathara/base", + "manager_type": "docker", + "terminal": "/usr/bin/xterm", + "open_terminals": false, + "device_shell": "/bin/bash", + "net_prefix": "kathara", + "device_prefix": "kathara", + "debug_level": "INFO", + "print_startup_log": true, + "enable_ipv6": false, + "last_checked": 1721834897.2415252, + "hosthome_mount": false, + "shared_mount": true, + "image_update_policy": "Prompt", + "shared_cds": 1, + "remote_url": null, + "cert_path": null, + "network_plugin": "kathara/katharanp_vde" + }' > ~/.config/kathara.conf + + - name: Build binary for e2e + uses: goreleaser/goreleaser-action@v6 + with: + version: latest + args: build --single-target --clean --snapshot --config .goreleaser-ci.yaml + + - name: Run e2e tests + run: | + ./scripts/run_e2e_tests.sh + + k8s: + name: E2E - Kubernetes runs-on: ubuntu-latest steps: - - name: Checkout Repo - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - name: Set up K3S uses: debianmaster/actions-k3s@master id: k3s with: version: "v1.31.2-k3s1" + - name: Check Cluster - run: | - kubectl get nodes + run: kubectl get nodes + - name: Check Coredns Deployment run: | kubectl -n kube-system rollout status deployment/coredns --timeout=60s @@ -29,6 +96,7 @@ jobs: else echo "Deployment coredns OK" fi + - name: Check Metricsserver Deployment run: | kubectl -n kube-system rollout status deployment/metrics-server --timeout=60s @@ -41,13 +109,16 @@ jobs: else echo "Deployment metrics-server OK" fi + - name: Setup Helm run: | curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash helm version + - name: Get Image Tag id: version run: echo "value=commit-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + - name: Install Sparrow run: | helm upgrade -i sparrow \ @@ -63,11 +134,11 @@ jobs: ./chart - name: Check Pods - run: | - kubectl get pods + run: kubectl get pods + - name: Wait for Sparrow - run: | - sleep 60 + run: sleep 45 + - name: Healthcheck run: | kubectl create job curl --image=quay.io/curl/curl:latest -- curl -f -v -H 'Content-Type: application/json' http://sparrow:8080/v1/metrics/health From 9e4526f5acb32476e759acc59f75bfba57c0380c Mon Sep 17 00:00:00 2001 From: lvlcn-t <75443136+lvlcn-t@users.noreply.github.com> Date: Sun, 17 Nov 2024 01:12:07 +0100 Subject: [PATCH 08/17] test: add additional e2e tests for check reconfiguration * test: add additional e2e tests for check reconfiguration * fix: race conditions in all checks when reconfiguring while running checks * refactor: add check base constructor * fix: add missing json tags for dns check result struct * chore: use time ticker instead of time.After for check intervals * fix: only update check config if it has changed * chore: improve naming in checks * chore: unexpose private functions & structs * refactor: simplify check config & e2e test builder Signed-off-by: lvlcn-t <75443136+lvlcn-t@users.noreply.github.com> --- .vscode/settings.json | 3 +- pkg/checks/base.go | 45 ++- pkg/checks/base_test.go | 10 +- pkg/checks/dns/dns.go | 107 ++++--- pkg/checks/dns/dns_test.go | 33 +- pkg/checks/dns/resolver.go | 2 +- pkg/checks/health/health.go | 109 ++++--- pkg/checks/health/health_test.go | 4 +- pkg/checks/latency/latency.go | 112 ++++--- pkg/checks/latency/latency_test.go | 4 +- pkg/checks/traceroute/check.go | 125 ++++---- pkg/checks/traceroute/check_test.go | 9 +- test/{framework => }/checks.go | 61 +++- test/e2e.go | 461 ++++++++++++++++++++++++++++ test/e2e/main_test.go | 441 ++++++++++++++++---------- test/framework.go | 58 ++++ test/framework/framework.go | 69 ----- test/{framework => }/startup.go | 6 +- test/unit.go | 19 ++ 19 files changed, 1176 insertions(+), 502 deletions(-) rename test/{framework => }/checks.go (76%) create mode 100644 test/e2e.go create mode 100644 test/framework.go delete mode 100644 test/framework/framework.go rename test/{framework => }/startup.go (97%) create mode 100644 test/unit.go diff --git a/.vscode/settings.json b/.vscode/settings.json index bb2287ce..cefc2e80 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,6 +3,7 @@ "-race", "-cover", "-count=1", - "-timeout=120s", + "-timeout=240s", + "-v" ] } \ No newline at end of file diff --git a/pkg/checks/base.go b/pkg/checks/base.go index b3e4e61a..8f98da6a 100644 --- a/pkg/checks/base.go +++ b/pkg/checks/base.go @@ -42,42 +42,55 @@ type Check interface { // run until the context is canceled and handle problems itself. // Returning a non-nil error will cause the shutdown of the check. Run(ctx context.Context, cResult chan ResultDTO) error - // Shutdown is called once when the check is unregistered or sparrow shuts down + // Shutdown is called once when the check is unregistered or sparrow shuts down. Shutdown() - // UpdateConfig is called once when the check is registered - // This is also called while the check is running, if the remote config is updated - // This should return an error if the config is invalid + // UpdateConfig updates the configuration of the check. + // It is called when the runtime configuration is updated. + // The check should handle the update itself. + // Returns an error if the configuration is invalid. UpdateConfig(config Runtime) error - // GetConfig returns the current configuration of the check + // GetConfig returns the current configuration of the check. GetConfig() Runtime - // Name returns the name of the check + // Name returns the name of the check. Name() string - // Schema returns an openapi3.SchemaRef of the result type returned by the check + // Schema returns an openapi3.SchemaRef of the result type returned by the check. Schema() (*openapi3.SchemaRef, error) - // GetMetricCollectors allows the check to provide prometheus metric collectors + // GetMetricCollectors allows the check to provide prometheus metric collectors. GetMetricCollectors() []prometheus.Collector // RemoveLabelledMetrics allows the check to remove the prometheus metrics - // of the check whose `target` label matches the passed value + // of the check whose `target` label matches the passed value. RemoveLabelledMetrics(target string) error } // Base is a struct providing common fields and methods used by implementations of the [Check] interface. // It serves as a foundational structure that should be embedded in specific check implementations. type Base struct { - // Mutex for thread-safe access to shared resources within the check implementation - Mu sync.Mutex - // Signal channel used to notify about shutdown of a check - DoneChan chan struct{} + // Mutex for thread-safe access to shared resources within the check implementation. + Mutex sync.Mutex + // Done channel is used to notify about shutdown of a check. + Done chan struct{} + // Update is a channel used to notify about configuration updates. + Update chan struct{} // closed is a flag indicating if the check has been shut down. closed bool } +// NewBase creates a new instance of the [Base] struct. +func NewBase() Base { + return Base{ + Mutex: sync.Mutex{}, + Done: make(chan struct{}, 1), + Update: make(chan struct{}, 3), + closed: false, + } +} + // Shutdown closes the DoneChan to signal the check to stop running. func (b *Base) Shutdown() { - b.Mu.Lock() - defer b.Mu.Unlock() + b.Mutex.Lock() + defer b.Mutex.Unlock() if !b.closed { - close(b.DoneChan) + close(b.Done) b.closed = true } } diff --git a/pkg/checks/base_test.go b/pkg/checks/base_test.go index 6f2308bb..afd99d48 100644 --- a/pkg/checks/base_test.go +++ b/pkg/checks/base_test.go @@ -14,21 +14,21 @@ func TestBase_Shutdown(t *testing.T) { { name: "shutdown", b: &Base{ - DoneChan: make(chan struct{}, 1), + Done: make(chan struct{}, 1), }, }, { name: "already shutdown", b: &Base{ - DoneChan: make(chan struct{}, 1), - closed: true, + Done: make(chan struct{}, 1), + closed: true, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.b.closed { - close(tt.b.DoneChan) + close(tt.b.Done) } tt.b.Shutdown() @@ -37,7 +37,7 @@ func TestBase_Shutdown(t *testing.T) { } assert.Panics(t, func() { - tt.b.DoneChan <- struct{}{} + tt.b.Done <- struct{}{} }, "Base.DoneChan should be closed") }) } diff --git a/pkg/checks/dns/dns.go b/pkg/checks/dns/dns.go index d11109f7..87b60b8e 100644 --- a/pkg/checks/dns/dns.go +++ b/pkg/checks/dns/dns.go @@ -21,6 +21,7 @@ package dns import ( "context" "net" + "reflect" "slices" "sync" "time" @@ -33,96 +34,104 @@ import ( ) var ( - _ checks.Check = (*DNS)(nil) + _ checks.Check = (*check)(nil) _ checks.Runtime = (*Config)(nil) ) const CheckName = "dns" -// DNS is a check that resolves the names and addresses -type DNS struct { +// check is the implementation of the dns check. +// It resolves DNS names and IP addresses for a list of targets. +type check struct { checks.Base config Config metrics metrics client Resolver } -func (d *DNS) GetConfig() checks.Runtime { - d.Mu.Lock() - defer d.Mu.Unlock() - return &d.config +func (ch *check) GetConfig() checks.Runtime { + ch.Mutex.Lock() + defer ch.Mutex.Unlock() + return &ch.config } -func (d *DNS) Name() string { +func (*check) Name() string { return CheckName } // NewCheck creates a new instance of the dns check func NewCheck() checks.Check { - return &DNS{ - Base: checks.Base{ - Mu: sync.Mutex{}, - DoneChan: make(chan struct{}, 1), - }, + return &check{ + Base: checks.NewBase(), config: Config{ Retry: checks.DefaultRetry, }, metrics: newMetrics(), - client: NewResolver(), + client: newResolver(), } } // result represents the result of a single DNS check for a specific target type result struct { - Resolved []string - Error *string - Total float64 + Resolved []string `json:"resolved"` + Error *string `json:"error"` + Total float64 `json:"total"` } // Run starts the dns check -func (d *DNS) Run(ctx context.Context, cResult chan checks.ResultDTO) error { +func (ch *check) Run(ctx context.Context, cResult chan checks.ResultDTO) error { ctx, cancel := logger.NewContextWithLogger(ctx) defer cancel() log := logger.FromContext(ctx) - log.Info("Starting dns check", "interval", d.config.Interval.String()) + ticker := time.NewTicker(ch.config.Interval) + log.InfoContext(ctx, "Starting dns check", "interval", ch.config.Interval.String()) for { select { case <-ctx.Done(): - log.Error("Context canceled", "err", ctx.Err()) + log.ErrorContext(ctx, "Context canceled", "err", ctx.Err()) return ctx.Err() - case <-d.DoneChan: + case <-ch.Done: return nil - case <-time.After(d.config.Interval): - res := d.check(ctx) - + case <-ch.Update: + ch.Mutex.Lock() + ticker.Stop() + ticker = time.NewTicker(ch.config.Interval) + log.DebugContext(ctx, "Interval of dns check updated", "interval", ch.config.Interval.String()) + ch.Mutex.Unlock() + case <-ticker.C: + res := ch.check(ctx) cResult <- checks.ResultDTO{ - Name: d.Name(), + Name: ch.Name(), Result: &checks.Result{ Data: res, Timestamp: time.Now(), }, } - log.Debug("Successfully finished dns check run") + log.DebugContext(ctx, "Successfully finished dns check run") } } } -func (d *DNS) UpdateConfig(cfg checks.Runtime) error { +func (ch *check) UpdateConfig(cfg checks.Runtime) error { if c, ok := cfg.(*Config); ok { - d.Mu.Lock() - defer d.Mu.Unlock() + ch.Mutex.Lock() + defer ch.Mutex.Unlock() + if reflect.DeepEqual(ch.config, *c) { + return nil + } - for _, target := range d.config.Targets { + for _, target := range ch.config.Targets { if !slices.Contains(c.Targets, target) { - err := d.metrics.Remove(target) + err := ch.metrics.Remove(target) if err != nil { return err } } } - d.config = *c + ch.config = *c + ch.Update <- struct{}{} return nil } @@ -134,27 +143,31 @@ func (d *DNS) UpdateConfig(cfg checks.Runtime) error { // Schema provides the schema of the data that will be provided // by the dns check -func (d *DNS) Schema() (*openapi3.SchemaRef, error) { - return checks.OpenapiFromPerfData(make(map[string]result)) +func (ch *check) Schema() (*openapi3.SchemaRef, error) { + return checks.OpenapiFromPerfData(map[string]result{}) } // GetMetricCollectors returns all metric collectors of check -func (d *DNS) GetMetricCollectors() []prometheus.Collector { - return d.metrics.GetCollectors() +func (ch *check) GetMetricCollectors() []prometheus.Collector { + return ch.metrics.GetCollectors() } // RemoveLabelledMetrics removes the metrics which have the passed // target as a label -func (d *DNS) RemoveLabelledMetrics(target string) error { - return d.metrics.Remove(target) +func (ch *check) RemoveLabelledMetrics(target string) error { + return ch.metrics.Remove(target) } // check performs DNS checks for all configured targets using a custom net.Resolver. // Returns a map where each target is associated with its DNS check result. -func (d *DNS) check(ctx context.Context) map[string]result { +func (ch *check) check(ctx context.Context) map[string]result { log := logger.FromContext(ctx) log.Debug("Checking dns") - if len(d.config.Targets) == 0 { + ch.Mutex.Lock() + cfg := ch.config + ch.Mutex.Unlock() + + if len(cfg.Targets) == 0 { log.Debug("No targets defined") return map[string]result{} } @@ -163,18 +176,18 @@ func (d *DNS) check(ctx context.Context) map[string]result { var wg sync.WaitGroup results := map[string]result{} - d.client.SetDialer(&net.Dialer{ - Timeout: d.config.Timeout, + ch.client.SetDialer(&net.Dialer{ + Timeout: cfg.Timeout, }) - log.Debug("Getting dns status for each target in separate routine", "amount", len(d.config.Targets)) - for _, t := range d.config.Targets { + log.Debug("Getting dns status for each target in separate routine", "amount", len(cfg.Targets)) + for _, t := range cfg.Targets { target := t wg.Add(1) lo := log.With("target", target) getDNSRetry := helper.Retry(func(ctx context.Context) error { - res, err := getDNS(ctx, d.client, target) + res, err := getDNS(ctx, ch.client, target) mu.Lock() defer mu.Unlock() results[target] = res @@ -182,7 +195,7 @@ func (d *DNS) check(ctx context.Context) map[string]result { return err } return nil - }, d.config.Retry) + }, cfg.Retry) go func() { defer wg.Done() @@ -197,7 +210,7 @@ func (d *DNS) check(ctx context.Context) map[string]result { mu.Lock() defer mu.Unlock() - d.metrics.Set(target, results, float64(status)) + ch.metrics.Set(target, results, float64(status)) }() } wg.Wait() diff --git a/pkg/checks/dns/dns_test.go b/pkg/checks/dns/dns_test.go index 6be26d8b..02cad0b2 100644 --- a/pkg/checks/dns/dns_test.go +++ b/pkg/checks/dns/dns_test.go @@ -23,7 +23,6 @@ import ( "fmt" "net" "reflect" - "sync" "testing" "time" @@ -43,18 +42,15 @@ const ( func TestDNS_Run(t *testing.T) { tests := []struct { name string - mockSetup func() *DNS + mockSetup func() *check targets []string want checks.Result }{ { name: "success with no targets", - mockSetup: func() *DNS { - return &DNS{ - Base: checks.Base{ - Mu: sync.Mutex{}, - DoneChan: make(chan struct{}, 1), - }, + mockSetup: func() *check { + return &check{ + Base: checks.NewBase(), } }, targets: []string{}, @@ -64,7 +60,7 @@ func TestDNS_Run(t *testing.T) { }, { name: "success with one target lookup", - mockSetup: func() *DNS { + mockSetup: func() *check { c := newCommonDNS() c.client = &ResolverMock{ LookupHostFunc: func(ctx context.Context, addr string) ([]string, error) { @@ -83,7 +79,7 @@ func TestDNS_Run(t *testing.T) { }, { //nolint:dupl // normal lookup name: "success with multiple target lookups", - mockSetup: func() *DNS { + mockSetup: func() *check { c := newCommonDNS() c.client = &ResolverMock{ LookupHostFunc: func(ctx context.Context, addr string) ([]string, error) { @@ -103,7 +99,7 @@ func TestDNS_Run(t *testing.T) { }, { //nolint:dupl // reverse lookup name: "success with multiple target reverse lookups", - mockSetup: func() *DNS { + mockSetup: func() *check { c := newCommonDNS() c.client = &ResolverMock{ LookupAddrFunc: func(ctx context.Context, addr string) ([]string, error) { @@ -123,7 +119,7 @@ func TestDNS_Run(t *testing.T) { }, { name: "error - lookup failure for a target", - mockSetup: func() *DNS { + mockSetup: func() *check { c := newCommonDNS() c.client = &ResolverMock{ LookupHostFunc: func(ctx context.Context, addr string) ([]string, error) { @@ -142,7 +138,7 @@ func TestDNS_Run(t *testing.T) { }, { name: "error - timeout scenario for a target", - mockSetup: func() *DNS { + mockSetup: func() *check { c := newCommonDNS() c.client = &ResolverMock{ LookupHostFunc: func(ctx context.Context, addr string) ([]string, error) { @@ -284,7 +280,7 @@ func TestDNS_UpdateConfig(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - c := &DNS{} + c := &check{} if err := c.UpdateConfig(tt.input); (err != nil) != tt.wantErr { t.Errorf("DNS.UpdateConfig() error = %v, wantErr %v", err, tt.wantErr) @@ -305,12 +301,9 @@ func stringPointer(s string) *string { return &s } -func newCommonDNS() *DNS { - return &DNS{ - Base: checks.Base{ - Mu: sync.Mutex{}, - DoneChan: make(chan struct{}, 1), - }, +func newCommonDNS() *check { + return &check{ + Base: checks.NewBase(), metrics: newMetrics(), } } diff --git a/pkg/checks/dns/resolver.go b/pkg/checks/dns/resolver.go index 63cd1ea9..c4c47213 100644 --- a/pkg/checks/dns/resolver.go +++ b/pkg/checks/dns/resolver.go @@ -34,7 +34,7 @@ type resolver struct { *net.Resolver } -func NewResolver() Resolver { +func newResolver() Resolver { return &resolver{ Resolver: &net.Resolver{ // We need to set this so the custom dialer is used diff --git a/pkg/checks/health/health.go b/pkg/checks/health/health.go index 55d7a058..be1443bc 100644 --- a/pkg/checks/health/health.go +++ b/pkg/checks/health/health.go @@ -21,8 +21,8 @@ package health import ( "context" "fmt" - "io" "net/http" + "reflect" "slices" "sync" "time" @@ -36,7 +36,7 @@ import ( ) var ( - _ checks.Check = (*Health)(nil) + _ checks.Check = (*check)(nil) _ checks.Runtime = (*Config)(nil) stateMapping = map[int]string{ 0: "unhealthy", @@ -46,8 +46,9 @@ var ( const CheckName = "health" -// Health is a check that measures the availability of an endpoint -type Health struct { +// check is the implementation of the health check. +// It measures the availability of a list of targets. +type check struct { checks.Base config Config metrics metrics @@ -55,11 +56,8 @@ type Health struct { // NewCheck creates a new instance of the health check func NewCheck() checks.Check { - return &Health{ - Base: checks.Base{ - Mu: sync.Mutex{}, - DoneChan: make(chan struct{}, 1), - }, + return &check{ + Base: checks.NewBase(), config: Config{ Retry: checks.DefaultRetry, }, @@ -68,51 +66,61 @@ func NewCheck() checks.Check { } // Run starts the health check -func (h *Health) Run(ctx context.Context, cResult chan checks.ResultDTO) error { +func (ch *check) Run(ctx context.Context, cResult chan checks.ResultDTO) error { ctx, cancel := logger.NewContextWithLogger(ctx) defer cancel() log := logger.FromContext(ctx) - log.Info("Starting healthcheck", "interval", h.config.Interval.String()) + ticker := time.NewTicker(ch.config.Interval) + defer ticker.Stop() + log.InfoContext(ctx, "Starting health check", "interval", ch.config.Interval.String()) for { select { case <-ctx.Done(): - log.Error("Context canceled", "err", ctx.Err()) + log.ErrorContext(ctx, "Context canceled", "err", ctx.Err()) return ctx.Err() - case <-h.DoneChan: - log.Debug("Soft shut down") + case <-ch.Done: return nil - case <-time.After(h.config.Interval): - res := h.check(ctx) - + case <-ch.Update: + ch.Mutex.Lock() + ticker.Stop() + ticker = time.NewTicker(ch.config.Interval) + log.DebugContext(ctx, "Interval of health check updated", "interval", ch.config.Interval.String()) + ch.Mutex.Unlock() + case <-ticker.C: + res := ch.check(ctx) cResult <- checks.ResultDTO{ - Name: h.Name(), + Name: ch.Name(), Result: &checks.Result{ Data: res, Timestamp: time.Now(), }, } - log.Debug("Successfully finished health check run") + log.DebugContext(ctx, "Successfully finished health check run") } } } // UpdateConfig sets the configuration for the health check -func (h *Health) UpdateConfig(cfg checks.Runtime) error { +func (ch *check) UpdateConfig(cfg checks.Runtime) error { if c, ok := cfg.(*Config); ok { - h.Mu.Lock() - defer h.Mu.Unlock() + ch.Mutex.Lock() + defer ch.Mutex.Unlock() + if reflect.DeepEqual(ch.config, *c) { + return nil + } - for _, target := range h.config.Targets { + for _, target := range ch.config.Targets { if !slices.Contains(c.Targets, target) { - err := h.metrics.Remove(target) + err := ch.metrics.Remove(target) if err != nil { return err } } } - h.config = *c + ch.config = *c + ch.Update <- struct{}{} return nil } @@ -123,62 +131,66 @@ func (h *Health) UpdateConfig(cfg checks.Runtime) error { } // GetConfig returns the current configuration of the check -func (h *Health) GetConfig() checks.Runtime { - h.Mu.Lock() - defer h.Mu.Unlock() - return &h.config +func (ch *check) GetConfig() checks.Runtime { + ch.Mutex.Lock() + defer ch.Mutex.Unlock() + return &ch.config } // Name returns the name of the check -func (h *Health) Name() string { +func (*check) Name() string { return CheckName } // Schema provides the schema of the data that will be provided // by the health check -func (h *Health) Schema() (*openapi3.SchemaRef, error) { - return checks.OpenapiFromPerfData[map[string]string](map[string]string{}) +func (ch *check) Schema() (*openapi3.SchemaRef, error) { + return checks.OpenapiFromPerfData(map[string]string{}) } // GetMetricCollectors returns all metric collectors of check -func (h *Health) GetMetricCollectors() []prometheus.Collector { +func (ch *check) GetMetricCollectors() []prometheus.Collector { return []prometheus.Collector{ - h.metrics, + ch.metrics, } } // RemoveLabelledMetrics removes the metrics which have the passed // target as a label -func (h *Health) RemoveLabelledMetrics(target string) error { - return h.metrics.Remove(target) +func (ch *check) RemoveLabelledMetrics(target string) error { + return ch.metrics.Remove(target) } // check performs a health check using a retry function // to get the health status for all targets -func (h *Health) check(ctx context.Context) map[string]string { +func (ch *check) check(ctx context.Context) map[string]string { log := logger.FromContext(ctx) log.Debug("Checking health") - if len(h.config.Targets) == 0 { + ch.Mutex.Lock() + cfg := ch.config + ch.Mutex.Unlock() + + if len(cfg.Targets) == 0 { log.Debug("No targets defined") return map[string]string{} } - log.Debug("Getting health status for each target in separate routine", "amount", len(h.config.Targets)) + log.Debug("Getting health status for each target in separate routine", "amount", len(cfg.Targets)) var wg sync.WaitGroup var mu sync.Mutex results := map[string]string{} client := &http.Client{ - Timeout: h.config.Timeout, + Timeout: cfg.Timeout, } - for _, t := range h.config.Targets { + for _, t := range cfg.Targets { target := t wg.Add(1) l := log.With("target", target) getHealthRetry := helper.Retry(func(ctx context.Context) error { return getHealth(ctx, client, target) - }, h.config.Retry) + }, cfg.Retry) go func() { defer wg.Done() @@ -187,7 +199,7 @@ func (h *Health) check(ctx context.Context) map[string]string { l.Debug("Starting retry routine to get health status") if err := getHealthRetry(ctx); err != nil { state = 0 - l.Warn(fmt.Sprintf("Health check failed after %d retries", h.config.Retry.Count), "error", err) + l.Warn(fmt.Sprintf("Health check failed after %d retries", cfg.Retry.Count), "error", err) } l.Debug("Successfully got health status of target", "status", stateMapping[state]) @@ -195,7 +207,7 @@ func (h *Health) check(ctx context.Context) map[string]string { defer mu.Unlock() results[target] = stateMapping[state] - h.metrics.WithLabelValues(target).Set(float64(state)) + ch.metrics.WithLabelValues(target).Set(float64(state)) }() } @@ -216,17 +228,12 @@ func getHealth(ctx context.Context, client *http.Client, url string) error { return err } - resp, err := client.Do(req) //nolint:bodyclose // Closed in defer below + resp, err := client.Do(req) if err != nil { log.Error("Error while requesting health", "error", err) return err } - defer func(Body io.ReadCloser) { - err := Body.Close() - if err != nil { - log.Error("Failed to close response body", "error", err.Error()) - } - }(resp.Body) + defer resp.Body.Close() if resp.StatusCode != http.StatusOK { log.Warn("Health request was not ok (HTTP Status 200)", "status", resp.Status) diff --git a/pkg/checks/health/health_test.go b/pkg/checks/health/health_test.go index ee83bb9c..7c53b538 100644 --- a/pkg/checks/health/health_test.go +++ b/pkg/checks/health/health_test.go @@ -69,7 +69,7 @@ func TestHealth_UpdateConfig(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := &Health{ + h := &check{ metrics: newMetrics(), } @@ -227,7 +227,7 @@ func TestHealth_Check(t *testing.T) { ) } - h := &Health{ + h := &check{ config: Config{ Targets: tt.targets, Timeout: 30, diff --git a/pkg/checks/latency/latency.go b/pkg/checks/latency/latency.go index d9c0b63c..aab44e76 100644 --- a/pkg/checks/latency/latency.go +++ b/pkg/checks/latency/latency.go @@ -20,8 +20,8 @@ package latency import ( "context" - "io" "net/http" + "reflect" "slices" "sync" "time" @@ -35,14 +35,15 @@ import ( ) var ( - _ checks.Check = (*Latency)(nil) + _ checks.Check = (*check)(nil) _ checks.Runtime = (*Config)(nil) ) const CheckName = "latency" -// Latency is a check that measures the latency to an endpoint -type Latency struct { +// check is the implementation of the latency check. +// It measures the latency to a list of targets. +type check struct { checks.Base config Config metrics metrics @@ -50,11 +51,8 @@ type Latency struct { // NewCheck creates a new instance of the latency check func NewCheck() checks.Check { - return &Latency{ - Base: checks.Base{ - Mu: sync.Mutex{}, - DoneChan: make(chan struct{}, 1), - }, + return &check{ + Base: checks.NewBase(), config: Config{ Retry: checks.DefaultRetry, }, @@ -70,50 +68,61 @@ type result struct { } // Run starts the latency check -func (l *Latency) Run(ctx context.Context, cResult chan checks.ResultDTO) error { +func (ch *check) Run(ctx context.Context, cResult chan checks.ResultDTO) error { ctx, cancel := logger.NewContextWithLogger(ctx) defer cancel() log := logger.FromContext(ctx) - log.Info("Starting latency check", "interval", l.config.Interval.String()) + ticker := time.NewTicker(ch.config.Interval) + defer ticker.Stop() + log.InfoContext(ctx, "Starting latency check", "interval", ch.config.Interval.String()) for { select { case <-ctx.Done(): - log.Error("Context canceled", "err", ctx.Err()) + log.ErrorContext(ctx, "Context canceled", "err", ctx.Err()) return ctx.Err() - case <-l.DoneChan: + case <-ch.Done: return nil - case <-time.After(l.config.Interval): - res := l.check(ctx) - + case <-ch.Update: + ch.Mutex.Lock() + ticker.Stop() + ticker = time.NewTicker(ch.config.Interval) + log.DebugContext(ctx, "Interval of latency check updated", "interval", ch.config.Interval.String()) + ch.Mutex.Unlock() + case <-ticker.C: + res := ch.check(ctx) cResult <- checks.ResultDTO{ - Name: l.Name(), + Name: ch.Name(), Result: &checks.Result{ Data: res, Timestamp: time.Now(), }, } - log.Debug("Successfully finished latency check run") + log.DebugContext(ctx, "Successfully finished latency check run") } } } // UpdateConfig sets the configuration for the latency check -func (l *Latency) UpdateConfig(cfg checks.Runtime) error { +func (ch *check) UpdateConfig(cfg checks.Runtime) error { if c, ok := cfg.(*Config); ok { - l.Mu.Lock() - defer l.Mu.Unlock() + ch.Mutex.Lock() + defer ch.Mutex.Unlock() + if reflect.DeepEqual(ch.config, *c) { + return nil + } - for _, target := range l.config.Targets { + for _, target := range ch.config.Targets { if !slices.Contains(c.Targets, target) { - err := l.metrics.Remove(target) + err := ch.metrics.Remove(target) if err != nil { return err } } } - l.config = *c + ch.config = *c + ch.Update <- struct{}{} return nil } @@ -124,56 +133,60 @@ func (l *Latency) UpdateConfig(cfg checks.Runtime) error { } // GetConfig returns the current configuration of the latency Check -func (l *Latency) GetConfig() checks.Runtime { - l.Mu.Lock() - defer l.Mu.Unlock() - return &l.config +func (ch *check) GetConfig() checks.Runtime { + ch.Mutex.Lock() + defer ch.Mutex.Unlock() + return &ch.config } // Name returns the name of the check -func (l *Latency) Name() string { +func (*check) Name() string { return CheckName } // Schema provides the schema of the data that will be provided // by the latency check -func (l *Latency) Schema() (*openapi3.SchemaRef, error) { - return checks.OpenapiFromPerfData[map[string]result](make(map[string]result)) +func (ch *check) Schema() (*openapi3.SchemaRef, error) { + return checks.OpenapiFromPerfData(map[string]result{}) } // GetMetricCollectors returns all metric collectors of check -func (l *Latency) GetMetricCollectors() []prometheus.Collector { +func (ch *check) GetMetricCollectors() []prometheus.Collector { return []prometheus.Collector{ - l.metrics.totalDuration, - l.metrics.count, - l.metrics.histogram, + ch.metrics.totalDuration, + ch.metrics.count, + ch.metrics.histogram, } } // RemoveLabelledMetrics removes the metrics which have the passed target as a label -func (l *Latency) RemoveLabelledMetrics(target string) error { - return l.metrics.Remove(target) +func (ch *check) RemoveLabelledMetrics(target string) error { + return ch.metrics.Remove(target) } // check performs a latency check using a retry function // to get the latency to all targets -func (l *Latency) check(ctx context.Context) map[string]result { +func (ch *check) check(ctx context.Context) map[string]result { log := logger.FromContext(ctx) log.Debug("Checking latency") - if len(l.config.Targets) == 0 { + ch.Mutex.Lock() + cfg := ch.config + ch.Mutex.Unlock() + + if len(cfg.Targets) == 0 { log.Debug("No targets defined") return map[string]result{} } - log.Debug("Getting latency status for each target in separate routine", "amount", len(l.config.Targets)) + log.Debug("Getting latency status for each target in separate routine", "amount", len(cfg.Targets)) var mu sync.Mutex var wg sync.WaitGroup results := map[string]result{} client := &http.Client{ - Timeout: l.config.Timeout, + Timeout: cfg.Timeout, } - for _, t := range l.config.Targets { + for _, t := range cfg.Targets { target := t wg.Add(1) lo := log.With("target", target) @@ -187,7 +200,7 @@ func (l *Latency) check(ctx context.Context) map[string]result { return err } return nil - }, l.config.Retry) + }, cfg.Retry) go func() { defer wg.Done() @@ -201,9 +214,9 @@ func (l *Latency) check(ctx context.Context) map[string]result { mu.Lock() defer mu.Unlock() - l.metrics.totalDuration.WithLabelValues(target).Set(results[target].Total) - l.metrics.count.WithLabelValues(target).Inc() - l.metrics.histogram.WithLabelValues(target).Observe(results[target].Total) + ch.metrics.totalDuration.WithLabelValues(target).Set(results[target].Total) + ch.metrics.count.WithLabelValues(target).Inc() + ch.metrics.histogram.WithLabelValues(target).Observe(results[target].Total) }() } @@ -228,7 +241,7 @@ func getLatency(ctx context.Context, c *http.Client, url string) (result, error) } start := time.Now() - resp, err := c.Do(req) //nolint:bodyclose // Closed in defer below + resp, err := c.Do(req) if err != nil { log.Error("Error while checking latency", "error", err) errval := err.Error() @@ -236,12 +249,9 @@ func getLatency(ctx context.Context, c *http.Client, url string) (result, error) return res, err } end := time.Now() + defer resp.Body.Close() res.Code = resp.StatusCode - defer func(Body io.ReadCloser) { - _ = Body.Close() - }(resp.Body) - res.Total = end.Sub(start).Seconds() return res, nil } diff --git a/pkg/checks/latency/latency_test.go b/pkg/checks/latency/latency_test.go index d4155cbf..f08543ae 100644 --- a/pkg/checks/latency/latency_test.go +++ b/pkg/checks/latency/latency_test.go @@ -274,7 +274,7 @@ func TestLatency_check(t *testing.T) { } } - l := &Latency{ + l := &check{ config: Config{Targets: tt.targets, Interval: time.Second * 120, Timeout: time.Second * 1}, metrics: newMetrics(), } @@ -306,7 +306,7 @@ func TestLatency_check(t *testing.T) { } func TestLatency_UpdateConfig(t *testing.T) { - c := Latency{} + c := check{} wantCfg := Config{ Targets: []string{"http://localhost:9090"}, } diff --git a/pkg/checks/traceroute/check.go b/pkg/checks/traceroute/check.go index 5ea3c820..0b022be6 100644 --- a/pkg/checks/traceroute/check.go +++ b/pkg/checks/traceroute/check.go @@ -21,6 +21,7 @@ package traceroute import ( "context" "fmt" + "reflect" "slices" "sync" "time" @@ -36,7 +37,7 @@ import ( "go.opentelemetry.io/otel/trace" ) -var _ checks.Check = (*Traceroute)(nil) +var _ checks.Check = (*check)(nil) const CheckName = "traceroute" @@ -51,12 +52,19 @@ func (t Target) String() string { return fmt.Sprintf("%s:%d", t.Addr, t.Port) } +// check is the implementation of the traceroute check. +// It traces the path to a list of targets. +type check struct { + checks.Base + config Config + traceroute tracerouteFactory + metrics metrics + tracer trace.Tracer +} + func NewCheck() checks.Check { - c := &Traceroute{ - Base: checks.Base{ - Mu: sync.Mutex{}, - DoneChan: make(chan struct{}, 1), - }, + c := &check{ + Base: checks.NewBase(), config: Config{}, traceroute: TraceRoute, metrics: newMetrics(), @@ -65,14 +73,6 @@ func NewCheck() checks.Check { return c } -type Traceroute struct { - checks.Base - config Config - traceroute tracerouteFactory - metrics metrics - tracer trace.Tracer -} - type tracerouteConfig struct { Dest string Port int @@ -91,24 +91,32 @@ type result struct { } // Run runs the check in a loop sending results to the provided channel -func (tr *Traceroute) Run(ctx context.Context, cResult chan checks.ResultDTO) error { +func (ch *check) Run(ctx context.Context, cResult chan checks.ResultDTO) error { ctx, cancel := logger.NewContextWithLogger(ctx) defer cancel() log := logger.FromContext(ctx) - log.InfoContext(ctx, "Starting traceroute check", "interval", tr.config.Interval.String()) + ticker := time.NewTicker(ch.config.Interval) + defer ticker.Stop() + log.InfoContext(ctx, "Starting traceroute check", "interval", ch.config.Interval.String()) for { select { case <-ctx.Done(): log.ErrorContext(ctx, "Context canceled", "error", ctx.Err()) return ctx.Err() - case <-tr.DoneChan: + case <-ch.Done: return nil - case <-time.After(tr.config.Interval): - res := tr.check(ctx) - tr.metrics.MinHops(res) + case <-ch.Update: + ch.Mutex.Lock() + ticker.Stop() + ticker = time.NewTicker(ch.config.Interval) + log.DebugContext(ctx, "Interval of traceroute check updated", "interval", ch.config.Interval.String()) + ch.Mutex.Unlock() + case <-ticker.C: + res := ch.check(ctx) + ch.metrics.MinHops(res) cResult <- checks.ResultDTO{ - Name: tr.Name(), + Name: ch.Name(), Result: &checks.Result{ Data: res, Timestamp: time.Now(), @@ -120,50 +128,53 @@ func (tr *Traceroute) Run(ctx context.Context, cResult chan checks.ResultDTO) er } // GetConfig returns the current configuration of the check -func (tr *Traceroute) GetConfig() checks.Runtime { - tr.Mu.Lock() - defer tr.Mu.Unlock() - return &tr.config +func (ch *check) GetConfig() checks.Runtime { + ch.Mutex.Lock() + defer ch.Mutex.Unlock() + return &ch.config } -func (tr *Traceroute) check(ctx context.Context) map[string]result { +func (ch *check) check(ctx context.Context) map[string]result { res := make(map[string]result) log := logger.FromContext(ctx) + ch.Mutex.Lock() + cfg := ch.config + ch.Mutex.Unlock() type internalResult struct { addr string res result } - cResult := make(chan internalResult, len(tr.config.Targets)) + cResult := make(chan internalResult, len(cfg.Targets)) var wg sync.WaitGroup start := time.Now() - wg.Add(len(tr.config.Targets)) + wg.Add(len(cfg.Targets)) - for _, t := range tr.config.Targets { + for _, t := range cfg.Targets { go func(t Target) { defer wg.Done() l := log.With("target", t.String()) l.DebugContext(ctx, "Running traceroute") - c, span := tr.tracer.Start(ctx, t.String(), trace.WithAttributes( + c, span := ch.tracer.Start(ctx, t.String(), trace.WithAttributes( attribute.String("target.addr", t.Addr), attribute.Int("target.port", t.Port), - attribute.Stringer("config.interval", tr.config.Interval), - attribute.Stringer("config.timeout", tr.config.Timeout), - attribute.Int("config.max_hops", tr.config.MaxHops), - attribute.Int("config.retry.count", tr.config.Retry.Count), - attribute.Stringer("config.retry.delay", tr.config.Retry.Delay), + attribute.Stringer("config.interval", cfg.Interval), + attribute.Stringer("config.timeout", cfg.Timeout), + attribute.Int("config.max_hops", cfg.MaxHops), + attribute.Int("config.retry.count", cfg.Retry.Count), + attribute.Stringer("config.retry.delay", cfg.Retry.Delay), )) defer span.End() s := time.Now() - hops, err := tr.traceroute(c, tracerouteConfig{ + hops, err := ch.traceroute(c, tracerouteConfig{ Dest: t.Addr, Port: t.Port, - Timeout: tr.config.Timeout, - MaxHops: tr.config.MaxHops, - Rc: tr.config.Retry, + Timeout: cfg.Timeout, + MaxHops: cfg.MaxHops, + Rc: cfg.Retry, }) elapsed := time.Since(s) @@ -175,12 +186,12 @@ func (tr *Traceroute) check(ctx context.Context) map[string]result { span.SetStatus(codes.Ok, "success") } - tr.metrics.CheckDuration(t.Addr, elapsed) + ch.metrics.CheckDuration(t.Addr, elapsed) l.DebugContext(ctx, "Ran traceroute", "result", hops, "duration", elapsed) res := result{ Hops: hops, - MinHops: tr.config.MaxHops, + MinHops: cfg.MaxHops, } for ttl, hop := range hops { for _, attempt := range hop { @@ -212,24 +223,26 @@ func (tr *Traceroute) check(ctx context.Context) map[string]result { return res } -// UpdateConfig is called once when the check is registered -// This is also called while the check is running, if the remote config is updated -// This should return an error if the config is invalid -func (tr *Traceroute) UpdateConfig(cfg checks.Runtime) error { +// UpdateConfig updates the configuration of the check. +func (ch *check) UpdateConfig(cfg checks.Runtime) error { if c, ok := cfg.(*Config); ok { - tr.Mu.Lock() - defer tr.Mu.Unlock() + ch.Mutex.Lock() + defer ch.Mutex.Unlock() + if reflect.DeepEqual(ch.config, *c) { + return nil + } - for _, target := range tr.config.Targets { + for _, target := range ch.config.Targets { if !slices.Contains(c.Targets, target) { - err := tr.metrics.Remove(target.Addr) + err := ch.metrics.Remove(target.Addr) if err != nil { return err } } } - tr.config = *c + ch.config = *c + ch.Update <- struct{}{} return nil } @@ -240,22 +253,22 @@ func (tr *Traceroute) UpdateConfig(cfg checks.Runtime) error { } // Schema returns an openapi3.SchemaRef of the result type returned by the check -func (tr *Traceroute) Schema() (*openapi3.SchemaRef, error) { +func (ch *check) Schema() (*openapi3.SchemaRef, error) { return checks.OpenapiFromPerfData(map[string]result{}) } // GetMetricCollectors allows the check to provide prometheus metric collectors -func (tr *Traceroute) GetMetricCollectors() []prometheus.Collector { - return tr.metrics.List() +func (ch *check) GetMetricCollectors() []prometheus.Collector { + return ch.metrics.List() } // Name returns the name of the check -func (tr *Traceroute) Name() string { +func (*check) Name() string { return CheckName } // RemoveLabelledMetrics removes the metrics which have the passed // target as a label -func (tr *Traceroute) RemoveLabelledMetrics(target string) error { - return tr.metrics.Remove(target) +func (ch *check) RemoveLabelledMetrics(target string) error { + return ch.metrics.Remove(target) } diff --git a/pkg/checks/traceroute/check_test.go b/pkg/checks/traceroute/check_test.go index 37bea923..e597b472 100644 --- a/pkg/checks/traceroute/check_test.go +++ b/pkg/checks/traceroute/check_test.go @@ -21,7 +21,6 @@ package traceroute import ( "context" "net" - "sync" "testing" "time" @@ -33,7 +32,7 @@ import ( func TestCheck(t *testing.T) { cases := []struct { name string - c *Traceroute + c *check want map[string]result }{ { @@ -73,13 +72,13 @@ func TestCheck(t *testing.T) { } } -func newForTest(f tracerouteFactory, maxHops int, targets []string) *Traceroute { +func newForTest(f tracerouteFactory, maxHops int, targets []string) *check { t := make([]Target, len(targets)) for i, target := range targets { t[i] = Target{Addr: target} } - return &Traceroute{ - Base: checks.Base{Mu: sync.Mutex{}, DoneChan: make(chan struct{})}, + return &check{ + Base: checks.NewBase(), config: Config{Targets: t, MaxHops: maxHops}, traceroute: f, metrics: newMetrics(), diff --git a/test/framework/checks.go b/test/checks.go similarity index 76% rename from test/framework/checks.go rename to test/checks.go index e8ccca02..3bc44071 100644 --- a/test/framework/checks.go +++ b/test/checks.go @@ -1,4 +1,4 @@ -package framework +package test import ( "testing" @@ -14,8 +14,14 @@ import ( ) type CheckBuilder interface { + // For returns the name of the check. + For() string + // Check returns the check. Check(t *testing.T) checks.Check + // YAML returns the yaml representation of the check. YAML(t *testing.T) []byte + // ExpectedWaitTime returns the expected wait time for the check. + ExpectedWaitTime() time.Duration } // newCheck creates a new check with the given config. @@ -31,9 +37,10 @@ func newCheck(t *testing.T, c checks.Check, config checks.Runtime) checks.Check return c } -type marshalConfig map[string]any +// checkConfig is a map of check names to their configuration. +type checkConfig map[string]checks.Runtime -func newCheckAsYAML(t *testing.T, cfg marshalConfig) []byte { +func newCheckAsYAML(t *testing.T, cfg checkConfig) []byte { t.Helper() out, err := yaml.Marshal(cfg) if err != nil { @@ -85,7 +92,17 @@ func (b *healthCheckBuilder) Check(t *testing.T) checks.Check { // YAML returns the yaml representation of the health check. func (b *healthCheckBuilder) YAML(t *testing.T) []byte { t.Helper() - return newCheckAsYAML(t, marshalConfig{b.cfg.For(): b.cfg}) + return newCheckAsYAML(t, checkConfig{b.cfg.For(): &b.cfg}) +} + +// ExpectedWaitTime returns the expected wait time for the health check. +func (b *healthCheckBuilder) ExpectedWaitTime() time.Duration { + return b.cfg.Interval + b.cfg.Timeout + time.Duration(b.cfg.Retry.Count)*b.cfg.Retry.Delay +} + +// For returns the name of the check. +func (b *healthCheckBuilder) For() string { + return b.cfg.For() } var _ CheckBuilder = (*latencyConfigBuilder)(nil) @@ -130,7 +147,17 @@ func (b *latencyConfigBuilder) Check(t *testing.T) checks.Check { // YAML returns the yaml representation of the latency check. func (b *latencyConfigBuilder) YAML(t *testing.T) []byte { t.Helper() - return newCheckAsYAML(t, marshalConfig{b.cfg.For(): b.cfg}) + return newCheckAsYAML(t, checkConfig{b.cfg.For(): &b.cfg}) +} + +// For returns the name of the check. +func (b *latencyConfigBuilder) For() string { + return b.cfg.For() +} + +// ExpectedWaitTime returns the expected wait time for the health check. +func (b *latencyConfigBuilder) ExpectedWaitTime() time.Duration { + return b.cfg.Interval + b.cfg.Timeout + time.Duration(b.cfg.Retry.Count)*b.cfg.Retry.Delay } var _ CheckBuilder = (*dnsConfigBuilder)(nil) @@ -175,7 +202,17 @@ func (b *dnsConfigBuilder) Check(t *testing.T) checks.Check { // YAML returns the yaml representation of the dns check. func (b *dnsConfigBuilder) YAML(t *testing.T) []byte { t.Helper() - return newCheckAsYAML(t, marshalConfig{b.cfg.For(): b.cfg}) + return newCheckAsYAML(t, checkConfig{b.cfg.For(): &b.cfg}) +} + +// ExpectedWaitTime returns the expected wait time for the health check. +func (b *dnsConfigBuilder) ExpectedWaitTime() time.Duration { + return b.cfg.Interval + b.cfg.Timeout + time.Duration(b.cfg.Retry.Count)*b.cfg.Retry.Delay +} + +// For returns the name of the check. +func (b *dnsConfigBuilder) For() string { + return b.cfg.For() } var _ CheckBuilder = (*tracerouteConfigBuilder)(nil) @@ -226,5 +263,15 @@ func (b *tracerouteConfigBuilder) Check(t *testing.T) checks.Check { // YAML returns the yaml representation of the traceroute check. func (b *tracerouteConfigBuilder) YAML(t *testing.T) []byte { t.Helper() - return newCheckAsYAML(t, marshalConfig{b.cfg.For(): b.cfg}) + return newCheckAsYAML(t, checkConfig{b.cfg.For(): &b.cfg}) +} + +// ExpectedWaitTime returns the expected wait time for the health check. +func (b *tracerouteConfigBuilder) ExpectedWaitTime() time.Duration { + return b.cfg.Interval + b.cfg.Timeout + time.Duration(b.cfg.Retry.Count)*b.cfg.Retry.Delay +} + +// For returns the name of the check. +func (b *tracerouteConfigBuilder) For() string { + return b.cfg.For() } diff --git a/test/e2e.go b/test/e2e.go new file mode 100644 index 00000000..09754e26 --- /dev/null +++ b/test/e2e.go @@ -0,0 +1,461 @@ +package test + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/url" + "os" + "path/filepath" + "reflect" + "sync" + "testing" + "time" + + "github.com/caas-team/sparrow/pkg/checks" + "github.com/caas-team/sparrow/pkg/config" + "github.com/caas-team/sparrow/pkg/sparrow" + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/routers" + "github.com/getkin/kin-openapi/routers/gorillamux" +) + +var _ Runner = (*E2E)(nil) + +// E2E is an end-to-end test. +type E2E struct { + t *testing.T + config config.Config + sparrow *sparrow.Sparrow + checks map[string]CheckBuilder + buf bytes.Buffer + path string + mu sync.Mutex + running bool +} + +// WithConfigFile sets the path to the config file. +func (t *E2E) WithConfigFile(path string) *E2E { + t.path = path + return t +} + +// WithChecks sets the checks in the test. +func (t *E2E) WithChecks(builders ...CheckBuilder) *E2E { + for _, b := range builders { + t.checks[b.For()] = b + t.buf.Write(b.YAML(t.t)) + } + return t +} + +// UpdateChecks updates the checks of the test. +func (t *E2E) UpdateChecks(builders ...CheckBuilder) *E2E { + t.checks = map[string]CheckBuilder{} + t.buf.Reset() + for _, b := range builders { + t.checks[b.For()] = b + t.buf.Write(b.YAML(t.t)) + } + + err := t.writeCheckConfig() + if err != nil { + t.t.Fatalf("Failed to write check config: %v", err) + } + + return t +} + +// Run runs the test. +// Runs indefinitely until the context is canceled. +func (t *E2E) Run(ctx context.Context) error { + if t.isRunning() { + t.t.Fatal("E2E.Run must be called once") + } + + if t.path == "" { + t.path = "testdata/checks.yaml" + } + + err := t.writeCheckConfig() + if err != nil { + t.t.Fatalf("Failed to write check config: %v", err) + } + + t.mu.Lock() + t.running = true + t.mu.Unlock() + return t.sparrow.Run(ctx) +} + +// AwaitStartup waits for the provided URL to be ready. +// +// Must be called after the e2e test started with [E2E.Run]. +func (t *E2E) AwaitStartup(u string, failureTimeout time.Duration) *E2E { + t.t.Helper() + // To ensure the goroutine is started before we are checking if the test is running. + const initialDelay = 100 * time.Millisecond + <-time.After(initialDelay) + if !t.isRunning() { + t.t.Fatal("E2E.AwaitStartup must be called after E2E.Run") + } + + const retryInterval = 100 * time.Millisecond + start := time.Now() + deadline := start.Add(failureTimeout) + + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, u, http.NoBody) + if err != nil { + t.t.Fatalf("Failed to create request: %v", err) + return t + } + + for { + resp, err := http.DefaultClient.Do(req) + if err == nil && resp.StatusCode == http.StatusOK { + t.t.Logf("%s is ready after %v", u, time.Since(start)) + resp.Body.Close() + return t + } + if time.Now().After(deadline) { + t.t.Errorf("%s is not ready [%s (%d)] after %v: %v", u, http.StatusText(resp.StatusCode), resp.StatusCode, failureTimeout, err) + return t + } + <-time.After(retryInterval) + } +} + +// AwaitLoader waits for the loader to reload the configuration. +// +// Must be called after the e2e test started with [E2E.Run]. +func (t *E2E) AwaitLoader() *E2E { + t.t.Helper() + if !t.isRunning() { + t.t.Fatal("E2E.AwaitLoader must be called after E2E.Run") + } + + t.t.Logf("Waiting %s for loader to reload configuration", t.config.Loader.Interval.String()) + <-time.After(t.config.Loader.Interval) + return t +} + +// AwaitChecks waits for all checks to be executed before proceeding. +// +// Must be called after the e2e test started with [E2E.Run]. +func (t *E2E) AwaitChecks() *E2E { + t.t.Helper() + if !t.isRunning() { + t.t.Fatal("E2E.AwaitReadiness must be called after E2E.Run") + } + + wait := 5 * time.Second + for _, check := range t.checks { + wait = max(wait, check.ExpectedWaitTime()) + } + t.t.Logf("Waiting %s for checks to be executed", wait.String()) + <-time.After(wait) + return t +} + +// writeCheckConfig writes the check config to a file at the provided path. +func (t *E2E) writeCheckConfig() error { + const fileMode = 0o755 + err := os.MkdirAll(filepath.Dir(t.path), fileMode) + if err != nil { + return fmt.Errorf("failed to create %q: %w", filepath.Dir(t.path), err) + } + + err = os.WriteFile(t.path, t.buf.Bytes(), fileMode) + if err != nil { + return fmt.Errorf("failed to write %q: %w", t.path, err) + } + return nil +} + +// isRunning returns true if the test is running. +func (t *E2E) isRunning() bool { + t.mu.Lock() + defer t.mu.Unlock() + return t.running +} + +// e2eHttpAsserter is an HTTP asserter for end-to-end tests. +type e2eHttpAsserter struct { + e2e *E2E + url string + response *e2eResponseAsserter + schema *openapi3.T + router routers.Router +} + +type e2eResponseAsserter struct { + want any + asserter func(r *http.Response) error +} + +// HttpAssertion creates a new HTTP assertion for the given URL. +func (t *E2E) HttpAssertion(u string) *e2eHttpAsserter { + return &e2eHttpAsserter{e2e: t, url: u} +} + +// Assert asserts the status code and optional validations against the response. +// Optional validations must be set before calling this method. +// +// Must be called after the e2e test started with [E2E.Run]. +func (a *e2eHttpAsserter) Assert(status int) { + a.e2e.t.Helper() + if !a.e2e.isRunning() { + a.e2e.t.Fatal("e2eHttpAsserter.Assert must be called after E2E.Run") + } + + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, a.url, http.NoBody) + if err != nil { + a.e2e.t.Fatalf("Failed to create request: %v", err) + return + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + a.e2e.t.Errorf("Failed to get %s: %v", a.url, err) + return + } + defer resp.Body.Close() + + if resp.StatusCode != status { + a.e2e.t.Errorf("Want status code %d for %s, got %d", status, a.url, resp.StatusCode) + return + } + a.e2e.t.Logf("Got status code %d for %s", resp.StatusCode, a.url) + + if status == http.StatusOK { + if a.schema != nil && a.router != nil { + if err = a.assertSchema(req, resp); err != nil { + a.e2e.t.Errorf("Response from %q does not match schema: %v", a.url, err) + return + } + } + + if a.response != nil { + err := a.response.asserter(resp) + if err != nil { + a.e2e.t.Errorf("Failed to assert response: %v", err) + } + } + } +} + +// WithSchema fetches the OpenAPI schema and validates the response against it. +func (a *e2eHttpAsserter) WithSchema() *e2eHttpAsserter { + a.e2e.t.Helper() + schema, err := a.fetchSchema() + if err != nil { + a.e2e.t.Fatalf("Failed to fetch OpenAPI schema: %v", err) + } + + router, err := gorillamux.NewRouter(schema) + if err != nil { + a.e2e.t.Fatalf("Failed to create router from OpenAPI schema: %v", err) + } + + a.schema = schema + a.router = router + return a +} + +// WithResult sets the expected result for the response. +// The result is validated against the response body. +func (a *e2eHttpAsserter) WithCheckResult(r checks.Result) *e2eHttpAsserter { + a.e2e.t.Helper() + a.response = &e2eResponseAsserter{ + want: r, + asserter: a.assertCheckResponse, + } + return a +} + +// fetchSchema fetches the OpenAPI schema from the server. +func (a *e2eHttpAsserter) fetchSchema() (*openapi3.T, error) { + ctx := context.Background() + u, err := url.Parse(a.url) + if err != nil { + return nil, fmt.Errorf("failed to parse URL: %w", err) + } + u.Path = "/openapi" + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), http.NoBody) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to GET OpenAPI schema: %w", err) + } + defer resp.Body.Close() + + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read OpenAPI schema: %w", err) + } + + loader := openapi3.NewLoader() + schema, err := loader.LoadFromData(data) + if err != nil { + return nil, fmt.Errorf("failed to load OpenAPI schema: %w", err) + } + + if err = schema.Validate(ctx); err != nil { + return nil, fmt.Errorf("OpenAPI schema validation error: %w", err) + } + + return schema, nil +} + +// assertSchema asserts the response body against the OpenAPI schema. +func (a *e2eHttpAsserter) assertSchema(req *http.Request, resp *http.Response) error { + route, _, err := a.router.FindRoute(req) + if err != nil { + return fmt.Errorf("failed to find route: %w", err) + } + + data, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed to read response body: %w", err) + } + resp.Body.Close() + resp.Body = io.NopCloser(bytes.NewReader(data)) + + responseRef := route.Operation.Responses.Status(resp.StatusCode) + if responseRef == nil || responseRef.Value == nil { + return fmt.Errorf("no response defined in OpenAPI schema for status code %d", resp.StatusCode) + } + + mediaType := responseRef.Value.Content.Get("application/json") + if mediaType == nil { + return errors.New("no media type defined in OpenAPI schema for Content-Type 'application/json'") + } + + var body any + if err = json.Unmarshal(data, &body); err != nil { + return fmt.Errorf("failed to unmarshal response body: %w", err) + } + + // Validate the response body against the schema + err = mediaType.Schema.Value.VisitJSON(body) + if err != nil { + return fmt.Errorf("response body does not match schema: %w", err) + } + + return nil +} + +// assertCheckResponse asserts the response body against the expected check result. +func (a *e2eHttpAsserter) assertCheckResponse(resp *http.Response) error { + want, ok := a.response.want.(checks.Result) + if !ok { + a.e2e.t.Fatalf("Invalid response type: %T", a.response.want) + } + + var res checks.Result + if err := json.NewDecoder(resp.Body).Decode(&res); err != nil { + a.e2e.t.Errorf("Failed to decode response body: %v", err) + } + + wantData := want.Data.(map[string]any) + gotData := res.Data.(map[string]any) + assertMapEqual(a.e2e.t, wantData, gotData) + + const deltaTimeThreshold = 5 * time.Minute + if time.Since(res.Timestamp) > deltaTimeThreshold { + a.e2e.t.Errorf("Response timestamp is not recent: %v", res.Timestamp) + } + + return nil +} + +// assertMapEqual asserts the equality of the want and got maps. +// Fails the test if the maps are not equal. +func assertMapEqual(t *testing.T, want, got map[string]any) { + t.Helper() + if len(want) != len(got) { + t.Errorf("Want %d keys (%v), got %d keys (%v)", len(want), want, len(got), got) + } + + for k, w := range want { + g, ok := got[k] + if !ok { + t.Errorf("Missing key %q", k) + } + + if err := assertValueEqual(t, w, g); err != nil { + t.Errorf("got[%q]: %v", k, err) + } + } +} + +// assertValueEqual asserts the equality of the want and got values. +// For values that cannot be compared directly, it uses a type-specific comparison. +// e.g. IP addresses, timestamps, etc. +func assertValueEqual(t *testing.T, want, got any) error { + switch w := want.(type) { + case map[string]any: + gm, ok := got.(map[string]any) + if !ok { + return fmt.Errorf("%v (%T), want %v (%T)", got, got, w, w) + } + assertMapEqual(t, w, gm) + return nil + case time.Time, float32, float64: + // Timestamps and floating-point numbers are time-sensitive and are never equal. + return nil + case int: + // Unmarshaling JSON numbers as int will convert them to float64. + // We need to compare them as float64 to avoid type mismatch errors. + want = float64(w) + case []string: + // Unmarshaling JSON arrays as []string will convert them to []interface{}. + // We need to compare them as []interface{} and cast the elements to string + // to avoid type mismatch errors. + gs, ok := got.([]any) + if !ok { + return fmt.Errorf("%v (%T), want %v (%T)", got, got, w, w) + } + gss := make([]string, len(gs)) + for i, g := range gs { + gss[i] = g.(string) + } + for _, ipStr := range w { + wIP := net.ParseIP(ipStr) + if wIP == nil { + // This is a special case for string slices that might contain IP addresses. + // If the `want` value is not a valid IP address, we skip the IP validation + // and proceed to the default case for a generic equality check. + // + // Using `goto` here avoids introducing an additional boolean flag or + // nesting the logic further, which would make the code harder to read. + // In this case it simplifies the control flow by explicitly directing the + // execution to the default case. + goto defaultCase + } + + for _, gipStr := range gss { + gIP := net.ParseIP(gipStr) + if gIP == nil { + return fmt.Errorf("%q, want an IP address (%s)", gipStr, wIP) + } + } + } + return nil + } + +defaultCase: + if !reflect.DeepEqual(want, got) { + return fmt.Errorf("%v (%T), want %v (%T)", got, got, want, want) + } + return nil +} diff --git a/test/e2e/main_test.go b/test/e2e/main_test.go index 75f84042..9f016659 100644 --- a/test/e2e/main_test.go +++ b/test/e2e/main_test.go @@ -1,41 +1,31 @@ package e2e import ( - "bytes" "context" - "encoding/json" - "errors" - "fmt" - "io" "net/http" "testing" "time" - "github.com/caas-team/sparrow/test/framework" - "github.com/getkin/kin-openapi/openapi3" - "github.com/getkin/kin-openapi/routers" - "github.com/getkin/kin-openapi/routers/gorillamux" + "github.com/caas-team/sparrow/pkg/checks" + "github.com/caas-team/sparrow/test" ) const ( - checkInterval = 20 * time.Second - checkTimeout = 15 * time.Second + checkInterval = 10 * time.Second + checkTimeout = 10 * time.Second ) -func TestSparrow_E2E(t *testing.T) { - if testing.Short() { - t.Skip("skipping e2e tests") - } - +func TestE2E_Sparrow_WithChecks_ConfigureOnce(t *testing.T) { + framework := test.NewFramework(t) tests := []struct { name string - startup framework.ConfigBuilder - checks []framework.CheckBuilder + startup test.ConfigBuilder + checks []test.CheckBuilder wantEndpoints map[string]int }{ { name: "no checks", - startup: *framework.NewConfig(), + startup: *test.NewSparrowConfig(), checks: nil, wantEndpoints: map[string]int{ "http://localhost:8080/v1/metrics/health": http.StatusNotFound, @@ -46,9 +36,9 @@ func TestSparrow_E2E(t *testing.T) { }, { name: "with health check", - startup: *framework.NewConfig(), - checks: []framework.CheckBuilder{ - framework.NewHealthCheck(). + startup: *test.NewSparrowConfig(), + checks: []test.CheckBuilder{ + test.NewHealthCheck(). WithInterval(checkInterval). WithTimeout(checkTimeout). WithTargets("https://www.example.com/", "https://www.google.com/"), @@ -62,17 +52,17 @@ func TestSparrow_E2E(t *testing.T) { }, { name: "with health, latency and dns checks", - startup: *framework.NewConfig(), - checks: []framework.CheckBuilder{ - framework.NewHealthCheck(). + startup: *test.NewSparrowConfig(), + checks: []test.CheckBuilder{ + test.NewHealthCheck(). WithInterval(checkInterval). WithTimeout(checkTimeout). WithTargets("https://www.example.com/"), - framework.NewLatencyCheck(). + test.NewLatencyCheck(). WithInterval(checkInterval). WithTimeout(checkTimeout). WithTargets("https://www.example.com/"), - framework.NewDNSCheck(). + test.NewDNSCheck(). WithInterval(checkInterval). WithTimeout(checkTimeout). WithTargets("www.example.com"), @@ -88,13 +78,7 @@ func TestSparrow_E2E(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - f := framework.New(t) - - e2e := f.E2E(tt.startup.Config(t)) - for _, check := range tt.checks { - e2e = e2e.WithCheck(check) - } - + e2e := framework.E2E(t, tt.startup.Config(t)).WithChecks(tt.checks...) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) defer cancel() @@ -102,30 +86,10 @@ func TestSparrow_E2E(t *testing.T) { go func() { finish <- e2e.Run(ctx) }() - - // Wait for sparrow to be ready with a readiness probe. - readinessProbe(t, "http://localhost:8080", checkTimeout) - - // Wait for the checks to be executed. - wait := 5 * time.Second - if len(tt.checks) > 0 { - wait = checkInterval + checkTimeout + 5*time.Second - } - t.Logf("Waiting %s for checks to be executed", wait.String()) - <-time.After(wait) - - // Fetch, parse and create a new router from the OpenAPI schema, to be able to validate the responses. - schema, err := fetchOpenAPISchema("http://localhost:8080/openapi") - if err != nil { - t.Fatalf("Failed to fetch OpenAPI schema: %v", err) - } - router, err := gorillamux.NewRouter(schema) - if err != nil { - t.Fatalf("Failed to create router from OpenAPI schema: %v", err) - } + e2e.AwaitStartup("http://localhost:8080", checkTimeout).AwaitChecks() for url, status := range tt.wantEndpoints { - validateResponse(t, router, url, status) + e2e.HttpAssertion(url).WithSchema().Assert(status) } cancel() @@ -134,126 +98,271 @@ func TestSparrow_E2E(t *testing.T) { } } -func fetchOpenAPISchema(url string) (*openapi3.T, error) { - ctx := context.Background() - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody) - if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) - } +const loaderInterval = 5 * time.Second - resp, err := http.DefaultClient.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to GET OpenAPI schema: %w", err) - } - defer resp.Body.Close() - - data, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read OpenAPI schema: %w", err) - } - - loader := openapi3.NewLoader() - schema, err := loader.LoadFromData(data) - if err != nil { - return nil, fmt.Errorf("failed to load OpenAPI schema: %w", err) - } - - if err = schema.Validate(ctx); err != nil { - return nil, fmt.Errorf("OpenAPI schema validation error: %w", err) - } - - return schema, nil -} - -func validateResponse(t *testing.T, router routers.Router, url string, wantStatus int) { - t.Helper() - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, http.NoBody) - if err != nil { - t.Fatalf("Failed to create request: %v", err) - return - } - - resp, err := http.DefaultClient.Do(req) - if err != nil { - t.Errorf("Failed to get %s: %v", url, err) - return - } - defer resp.Body.Close() +func TestE2E_Sparrow_WithChecks_Reconfigure(t *testing.T) { + framework := test.NewFramework(t) - if resp.StatusCode != wantStatus { - t.Errorf("Want status code %d for %s, got %d", wantStatus, url, resp.StatusCode) - return + type result struct { + status int + response checks.Result } - - if wantStatus == http.StatusOK { - if err = validateResponseSchema(router, req, resp); err != nil { - t.Errorf("Response from %q does not match schema: %v", url, err) - return - } - } - - t.Logf("Got status code %d for %s", resp.StatusCode, url) -} - -func validateResponseSchema(router routers.Router, req *http.Request, resp *http.Response) error { - route, _, err := router.FindRoute(req) - if err != nil { - return fmt.Errorf("failed to find route: %w", err) + tests := []struct { + name string + startup test.ConfigBuilder + initialChecks []test.CheckBuilder + wantInitial map[string]result + secondChecks []test.CheckBuilder + wantSecond map[string]result + }{ + { + name: "with health check then latency check", + startup: *test.NewSparrowConfig().WithLoader( + test.NewLoaderConfig(). + WithInterval(loaderInterval). + Build(), + ), + initialChecks: []test.CheckBuilder{ + test.NewHealthCheck(). + WithInterval(checkInterval). + WithTimeout(checkTimeout). + WithTargets("https://www.example.com/", "https://www.google.com/"), + }, + wantInitial: map[string]result{ + "http://localhost:8080/v1/metrics/health": { + status: http.StatusOK, + response: checks.Result{ + Data: map[string]any{ + "https://www.example.com/": "healthy", + "https://www.google.com/": "healthy", + }, + Timestamp: time.Now(), + }, + }, + "http://localhost:8080/v1/metrics/latency": {status: http.StatusNotFound}, + "http://localhost:8080/v1/metrics/dns": {status: http.StatusNotFound}, + "http://localhost:8080/v1/metrics/traceroute": {status: http.StatusNotFound}, + }, + secondChecks: []test.CheckBuilder{ + test.NewLatencyCheck(). + WithInterval(checkInterval). + WithTimeout(checkTimeout). + WithTargets("https://www.example.com/"), + }, + wantSecond: map[string]result{ + "http://localhost:8080/v1/metrics/health": { + status: http.StatusOK, + response: checks.Result{ + Data: map[string]any{ + "https://www.example.com/": "healthy", + "https://www.google.com/": "healthy", + }, + Timestamp: time.Now(), + }, + }, + "http://localhost:8080/v1/metrics/latency": { + status: http.StatusOK, + response: checks.Result{ + Data: map[string]any{ + "https://www.example.com/": map[string]any{ + "code": http.StatusOK, + "error": nil, + "total": time.Since(time.Now().Add(-100 * time.Millisecond)).Seconds(), + }, + }, + Timestamp: time.Now(), + }, + }, + "http://localhost:8080/v1/metrics/dns": {status: http.StatusNotFound}, + "http://localhost:8080/v1/metrics/traceroute": {status: http.StatusNotFound}, + }, + }, + { + name: "with health check then dns check", + startup: *test.NewSparrowConfig().WithLoader( + test.NewLoaderConfig(). + WithInterval(loaderInterval). + Build(), + ), + initialChecks: []test.CheckBuilder{ + test.NewHealthCheck(). + WithInterval(checkInterval). + WithTimeout(checkTimeout). + WithTargets("https://www.example.com/"), + }, + wantInitial: map[string]result{ + "http://localhost:8080/v1/metrics/health": { + status: http.StatusOK, + response: checks.Result{ + Data: map[string]any{ + "https://www.example.com/": "healthy", + }, + Timestamp: time.Now(), + }, + }, + "http://localhost:8080/v1/metrics/latency": {status: http.StatusNotFound}, + "http://localhost:8080/v1/metrics/dns": {status: http.StatusNotFound}, + "http://localhost:8080/v1/metrics/traceroute": {status: http.StatusNotFound}, + }, + secondChecks: []test.CheckBuilder{ + test.NewDNSCheck(). + WithInterval(checkInterval). + WithTimeout(checkTimeout). + WithTargets("www.example.com"), + }, + wantSecond: map[string]result{ + "http://localhost:8080/v1/metrics/health": { + status: http.StatusOK, + response: checks.Result{ + Data: map[string]any{ + "https://www.example.com/": "healthy", + }, + Timestamp: time.Now(), + }, + }, + "http://localhost:8080/v1/metrics/latency": {status: http.StatusNotFound}, + "http://localhost:8080/v1/metrics/dns": { + status: http.StatusOK, + response: checks.Result{ + Data: map[string]any{ + "www.example.com": map[string]any{ + "resolved": []string{"1.2.3.4"}, + "error": nil, + "total": time.Since(time.Now().Add(-100 * time.Millisecond)).Seconds(), + }, + }, + Timestamp: time.Now(), + }, + }, + "http://localhost:8080/v1/metrics/traceroute": {status: http.StatusNotFound}, + }, + }, + { + name: "with health check then updated health check", + startup: *test.NewSparrowConfig().WithLoader( + test.NewLoaderConfig(). + WithInterval(loaderInterval). + Build(), + ), + initialChecks: []test.CheckBuilder{ + test.NewHealthCheck(). + WithInterval(checkInterval). + WithTimeout(checkTimeout). + WithTargets("https://www.example.com/"), + }, + wantInitial: map[string]result{ + "http://localhost:8080/v1/metrics/health": { + status: http.StatusOK, + response: checks.Result{ + Data: map[string]any{ + "https://www.example.com/": "healthy", + }, + Timestamp: time.Now(), + }, + }, + "http://localhost:8080/v1/metrics/latency": {status: http.StatusNotFound}, + "http://localhost:8080/v1/metrics/dns": {status: http.StatusNotFound}, + "http://localhost:8080/v1/metrics/traceroute": {status: http.StatusNotFound}, + }, + secondChecks: []test.CheckBuilder{ + test.NewHealthCheck(). + WithInterval(checkInterval). + WithTimeout(checkTimeout). + WithTargets("https://www.google.com/"), + }, + wantSecond: map[string]result{ + "http://localhost:8080/v1/metrics/health": { + status: http.StatusOK, + response: checks.Result{ + Data: map[string]any{ + "https://www.google.com/": "healthy", + }, + Timestamp: time.Now(), + }, + }, + "http://localhost:8080/v1/metrics/latency": {status: http.StatusNotFound}, + "http://localhost:8080/v1/metrics/dns": {status: http.StatusNotFound}, + "http://localhost:8080/v1/metrics/traceroute": {status: http.StatusNotFound}, + }, + }, + { + name: "with health check then no checks", + startup: *test.NewSparrowConfig().WithLoader( + test.NewLoaderConfig(). + WithInterval(loaderInterval). + Build(), + ), + initialChecks: []test.CheckBuilder{ + test.NewHealthCheck(). + WithInterval(checkInterval). + WithTimeout(checkTimeout). + WithTargets("https://www.example.com/"), + }, + wantInitial: map[string]result{ + "http://localhost:8080/v1/metrics/health": { + status: http.StatusOK, + response: checks.Result{ + Data: map[string]any{ + "https://www.example.com/": "healthy", + }, + Timestamp: time.Now(), + }, + }, + "http://localhost:8080/v1/metrics/latency": {status: http.StatusNotFound}, + "http://localhost:8080/v1/metrics/dns": {status: http.StatusNotFound}, + "http://localhost:8080/v1/metrics/traceroute": {status: http.StatusNotFound}, + }, + secondChecks: nil, + wantSecond: map[string]result{ + "http://localhost:8080/v1/metrics/health": { + status: http.StatusOK, + response: checks.Result{ + Data: map[string]any{ + "https://www.example.com/": "healthy", + }, + Timestamp: time.Now(), + }, + }, + "http://localhost:8080/v1/metrics/latency": {status: http.StatusNotFound}, + "http://localhost:8080/v1/metrics/dns": {status: http.StatusNotFound}, + "http://localhost:8080/v1/metrics/traceroute": {status: http.StatusNotFound}, + }, + }, } - data, err := io.ReadAll(resp.Body) - if err != nil { - return fmt.Errorf("failed to read response body: %w", err) - } - // Reset the response body for potential further use - resp.Body = io.NopCloser(bytes.NewBuffer(data)) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e2e := framework.E2E(t, tt.startup.Config(t)).WithChecks(tt.initialChecks...) + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() - responseRef := route.Operation.Responses.Status(resp.StatusCode) - if responseRef == nil || responseRef.Value == nil { - return fmt.Errorf("no response defined in OpenAPI schema for status code %d", resp.StatusCode) - } + finish := make(chan error, 1) + go func() { + finish <- e2e.Run(ctx) + }() + e2e.AwaitStartup("http://localhost:8080", checkTimeout).AwaitChecks() - mediaType := responseRef.Value.Content.Get("application/json") - if mediaType == nil { - return errors.New("no media type defined in OpenAPI schema for Content-Type 'application/json'") - } + for url, result := range tt.wantInitial { + e2e.HttpAssertion(url). + WithSchema(). + WithCheckResult(result.response). + Assert(result.status) + } - var body any - if err = json.Unmarshal(data, &body); err != nil { - return fmt.Errorf("failed to unmarshal response body: %w", err) - } + e2e.UpdateChecks(tt.secondChecks...).AwaitLoader().AwaitChecks() + for url, result := range tt.wantSecond { + e2e.HttpAssertion(url). + WithSchema(). + WithCheckResult(result.response). + Assert(result.status) + } - // Validate the response body against the schema - err = mediaType.Schema.Value.VisitJSON(body) - if err != nil { - return fmt.Errorf("response body does not match schema: %w", err) + cancel() + <-finish + }) } - - return nil } -func readinessProbe(t *testing.T, url string, timeout time.Duration) { - t.Helper() - const retryInterval = 100 * time.Millisecond - deadline := time.Now().Add(timeout) - - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, http.NoBody) - if err != nil { - t.Fatalf("Failed to create request: %v", err) - return - } +func TestE2E_Sparrow_WithRemoteConfig(t *testing.T) {} - for { - resp, err := http.DefaultClient.Do(req) - if err == nil && resp.StatusCode == http.StatusOK { - t.Log("Sparrow is ready") - resp.Body.Close() - return - } - if time.Now().After(deadline) { - t.Fatalf("Sparrow not ready [%s (%d)] after %v: %v", http.StatusText(resp.StatusCode), resp.StatusCode, timeout, err) - return - } - <-time.After(retryInterval) - } -} +func TestE2E_Sparrow_WithTargetManager(t *testing.T) {} diff --git a/test/framework.go b/test/framework.go new file mode 100644 index 00000000..2de6982f --- /dev/null +++ b/test/framework.go @@ -0,0 +1,58 @@ +package test + +import ( + "context" + "testing" + + "github.com/caas-team/sparrow/pkg/config" + "github.com/caas-team/sparrow/pkg/sparrow" +) + +// Runner is a test runner. +type Runner interface { + // Run runs the test. + Run(ctx context.Context) error +} + +// Framework is a test framework. +// It provides a way to run various tests. +type Framework struct { + t *testing.T +} + +// NewFramework creates a new test framework. +func NewFramework(t *testing.T) *Framework { + t.Helper() + return &Framework{t: t} +} + +// Unit creates a new unit test. +// If the test is not run in short mode, it will be skipped. +func (f *Framework) Unit(t *testing.T, run func(context.Context) error) *Unit { + if !testing.Short() { + f.t.Skip("skipping unit tests") + return nil + } + + return &Unit{t: t, run: run} +} + +// E2E creates a new end-to-end test. +// If the test is run in short mode, it will be skipped. +func (f *Framework) E2E(t *testing.T, cfg *config.Config) *E2E { + if testing.Short() { + f.t.Skip("skipping e2e tests") + return nil + } + + if cfg == nil { + cfg = NewSparrowConfig().Config(f.t) + } + + return &E2E{ + t: t, + config: *cfg, + sparrow: sparrow.New(cfg), + checks: map[string]CheckBuilder{}, + } +} diff --git a/test/framework/framework.go b/test/framework/framework.go deleted file mode 100644 index a1c2047a..00000000 --- a/test/framework/framework.go +++ /dev/null @@ -1,69 +0,0 @@ -package framework - -import ( - "bytes" - "context" - "os" - "testing" - - "github.com/caas-team/sparrow/pkg/config" - "github.com/caas-team/sparrow/pkg/sparrow" -) - -type Framework struct { - t *testing.T -} - -func New(t *testing.T) *Framework { - return &Framework{t: t} -} - -type E2ETest struct { - t *testing.T - sparrow *sparrow.Sparrow - buf bytes.Buffer - path string -} - -func (f *Framework) E2E(cfg *config.Config) *E2ETest { - if cfg == nil { - cfg = NewConfig().Config(f.t) - } - - return &E2ETest{ - t: f.t, - sparrow: sparrow.New(cfg), - } -} - -func (t *E2ETest) WithConfigFile(path string) *E2ETest { - t.path = path - return t -} - -func (t *E2ETest) WithCheck(builder CheckBuilder) *E2ETest { - t.buf.Write(builder.YAML(t.t)) - return t -} - -// Run runs the test. -// Runs indefinitely until the context is canceled. -func (t *E2ETest) Run(ctx context.Context) error { - if t.path == "" { - t.path = "testdata/checks.yaml" - } - - const fileMode = 0o755 - err := os.MkdirAll("testdata", fileMode) - if err != nil { - t.t.Fatalf("failed to create testdata directory: %v", err) - } - - err = os.WriteFile(t.path, t.buf.Bytes(), fileMode) - if err != nil { - t.t.Fatalf("failed to write testdata/checks.yaml: %v", err) - return err - } - - return t.sparrow.Run(ctx) -} diff --git a/test/framework/startup.go b/test/startup.go similarity index 97% rename from test/framework/startup.go rename to test/startup.go index 0b6e36ce..c7712402 100644 --- a/test/framework/startup.go +++ b/test/startup.go @@ -1,4 +1,4 @@ -package framework +package test import ( "context" @@ -20,10 +20,10 @@ import ( type ConfigBuilder struct{ cfg config.Config } -func NewConfig() *ConfigBuilder { +func NewSparrowConfig() *ConfigBuilder { return &ConfigBuilder{ cfg: config.Config{ - SparrowName: "sparrow.de", + SparrowName: "sparrow.telekom.com", Loader: NewLoaderConfig().Build(), Api: NewAPIConfig("localhost:8080"), }, diff --git a/test/unit.go b/test/unit.go new file mode 100644 index 00000000..14c2c43b --- /dev/null +++ b/test/unit.go @@ -0,0 +1,19 @@ +package test + +import ( + "context" + "testing" +) + +var _ Runner = (*Unit)(nil) + +// Unit is a unit test. +type Unit struct { + t *testing.T + run func(context.Context) error +} + +// Run runs the test. +func (t *Unit) Run(ctx context.Context) error { + return t.run(ctx) +} From 653d4b12aa765e5ce18e3a534b74567ac301debe Mon Sep 17 00:00:00 2001 From: lvlcn-t <75443136+lvlcn-t@users.noreply.github.com> Date: Sun, 17 Nov 2024 02:39:55 +0100 Subject: [PATCH 09/17] fix: use timers instead of tickers to avoid zero duration panics Signed-off-by: lvlcn-t <75443136+lvlcn-t@users.noreply.github.com> --- pkg/checks/dns/dns.go | 10 ++++++---- pkg/checks/dns/dns_test.go | 2 +- pkg/checks/health/health.go | 11 ++++++----- pkg/checks/health/health_test.go | 6 ++---- pkg/checks/latency/latency.go | 11 ++++++----- pkg/checks/latency/latency_test.go | 2 +- pkg/checks/traceroute/check.go | 11 ++++++----- 7 files changed, 28 insertions(+), 25 deletions(-) diff --git a/pkg/checks/dns/dns.go b/pkg/checks/dns/dns.go index 87b60b8e..2728d1b0 100644 --- a/pkg/checks/dns/dns.go +++ b/pkg/checks/dns/dns.go @@ -84,7 +84,7 @@ func (ch *check) Run(ctx context.Context, cResult chan checks.ResultDTO) error { defer cancel() log := logger.FromContext(ctx) - ticker := time.NewTicker(ch.config.Interval) + timer := time.NewTimer(ch.config.Interval) log.InfoContext(ctx, "Starting dns check", "interval", ch.config.Interval.String()) for { select { @@ -95,11 +95,10 @@ func (ch *check) Run(ctx context.Context, cResult chan checks.ResultDTO) error { return nil case <-ch.Update: ch.Mutex.Lock() - ticker.Stop() - ticker = time.NewTicker(ch.config.Interval) + timer.Reset(ch.config.Interval) log.DebugContext(ctx, "Interval of dns check updated", "interval", ch.config.Interval.String()) ch.Mutex.Unlock() - case <-ticker.C: + case <-timer.C: res := ch.check(ctx) cResult <- checks.ResultDTO{ Name: ch.Name(), @@ -109,6 +108,9 @@ func (ch *check) Run(ctx context.Context, cResult chan checks.ResultDTO) error { }, } log.DebugContext(ctx, "Successfully finished dns check run") + ch.Mutex.Lock() + timer.Reset(ch.config.Interval) + ch.Mutex.Unlock() } } } diff --git a/pkg/checks/dns/dns_test.go b/pkg/checks/dns/dns_test.go index 02cad0b2..976baaf8 100644 --- a/pkg/checks/dns/dns_test.go +++ b/pkg/checks/dns/dns_test.go @@ -274,7 +274,7 @@ func TestDNS_UpdateConfig(t *testing.T) { exampleURL, }, }, - want: Config{}, + want: Config{Retry: checks.DefaultRetry}, wantErr: true, }, } diff --git a/pkg/checks/health/health.go b/pkg/checks/health/health.go index be1443bc..7689df87 100644 --- a/pkg/checks/health/health.go +++ b/pkg/checks/health/health.go @@ -71,8 +71,7 @@ func (ch *check) Run(ctx context.Context, cResult chan checks.ResultDTO) error { defer cancel() log := logger.FromContext(ctx) - ticker := time.NewTicker(ch.config.Interval) - defer ticker.Stop() + timer := time.NewTimer(ch.config.Interval) log.InfoContext(ctx, "Starting health check", "interval", ch.config.Interval.String()) for { select { @@ -83,11 +82,10 @@ func (ch *check) Run(ctx context.Context, cResult chan checks.ResultDTO) error { return nil case <-ch.Update: ch.Mutex.Lock() - ticker.Stop() - ticker = time.NewTicker(ch.config.Interval) + timer.Reset(ch.config.Interval) log.DebugContext(ctx, "Interval of health check updated", "interval", ch.config.Interval.String()) ch.Mutex.Unlock() - case <-ticker.C: + case <-timer.C: res := ch.check(ctx) cResult <- checks.ResultDTO{ Name: ch.Name(), @@ -97,6 +95,9 @@ func (ch *check) Run(ctx context.Context, cResult chan checks.ResultDTO) error { }, } log.DebugContext(ctx, "Successfully finished health check run") + ch.Mutex.Lock() + timer.Reset(ch.config.Interval) + ch.Mutex.Unlock() } } } diff --git a/pkg/checks/health/health_test.go b/pkg/checks/health/health_test.go index 7c53b538..5b387170 100644 --- a/pkg/checks/health/health_test.go +++ b/pkg/checks/health/health_test.go @@ -63,15 +63,13 @@ func TestHealth_UpdateConfig(t *testing.T) { inputConfig: &latency.Config{ Targets: []string{"test"}, }, - expectedConfig: Config{}, + expectedConfig: Config{Retry: checks.DefaultRetry}, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - h := &check{ - metrics: newMetrics(), - } + h := NewCheck().(*check) if err := h.UpdateConfig(tt.inputConfig); (err != nil) != tt.wantErr { t.Errorf("Health.UpdateConfig() error = %v, wantErr %v", err, tt.wantErr) diff --git a/pkg/checks/latency/latency.go b/pkg/checks/latency/latency.go index aab44e76..f187eb1d 100644 --- a/pkg/checks/latency/latency.go +++ b/pkg/checks/latency/latency.go @@ -73,8 +73,7 @@ func (ch *check) Run(ctx context.Context, cResult chan checks.ResultDTO) error { defer cancel() log := logger.FromContext(ctx) - ticker := time.NewTicker(ch.config.Interval) - defer ticker.Stop() + timer := time.NewTimer(ch.config.Interval) log.InfoContext(ctx, "Starting latency check", "interval", ch.config.Interval.String()) for { select { @@ -85,11 +84,10 @@ func (ch *check) Run(ctx context.Context, cResult chan checks.ResultDTO) error { return nil case <-ch.Update: ch.Mutex.Lock() - ticker.Stop() - ticker = time.NewTicker(ch.config.Interval) + timer.Reset(ch.config.Interval) log.DebugContext(ctx, "Interval of latency check updated", "interval", ch.config.Interval.String()) ch.Mutex.Unlock() - case <-ticker.C: + case <-timer.C: res := ch.check(ctx) cResult <- checks.ResultDTO{ Name: ch.Name(), @@ -99,6 +97,9 @@ func (ch *check) Run(ctx context.Context, cResult chan checks.ResultDTO) error { }, } log.DebugContext(ctx, "Successfully finished latency check run") + ch.Mutex.Lock() + timer.Reset(ch.config.Interval) + ch.Mutex.Unlock() } } } diff --git a/pkg/checks/latency/latency_test.go b/pkg/checks/latency/latency_test.go index f08543ae..89351108 100644 --- a/pkg/checks/latency/latency_test.go +++ b/pkg/checks/latency/latency_test.go @@ -306,7 +306,7 @@ func TestLatency_check(t *testing.T) { } func TestLatency_UpdateConfig(t *testing.T) { - c := check{} + c := NewCheck().(*check) wantCfg := Config{ Targets: []string{"http://localhost:9090"}, } diff --git a/pkg/checks/traceroute/check.go b/pkg/checks/traceroute/check.go index 0b022be6..873e9e3e 100644 --- a/pkg/checks/traceroute/check.go +++ b/pkg/checks/traceroute/check.go @@ -96,8 +96,7 @@ func (ch *check) Run(ctx context.Context, cResult chan checks.ResultDTO) error { defer cancel() log := logger.FromContext(ctx) - ticker := time.NewTicker(ch.config.Interval) - defer ticker.Stop() + timer := time.NewTimer(ch.config.Interval) log.InfoContext(ctx, "Starting traceroute check", "interval", ch.config.Interval.String()) for { select { @@ -108,11 +107,10 @@ func (ch *check) Run(ctx context.Context, cResult chan checks.ResultDTO) error { return nil case <-ch.Update: ch.Mutex.Lock() - ticker.Stop() - ticker = time.NewTicker(ch.config.Interval) + timer.Reset(ch.config.Interval) log.DebugContext(ctx, "Interval of traceroute check updated", "interval", ch.config.Interval.String()) ch.Mutex.Unlock() - case <-ticker.C: + case <-timer.C: res := ch.check(ctx) ch.metrics.MinHops(res) cResult <- checks.ResultDTO{ @@ -123,6 +121,9 @@ func (ch *check) Run(ctx context.Context, cResult chan checks.ResultDTO) error { }, } log.DebugContext(ctx, "Successfully finished traceroute check run") + ch.Mutex.Lock() + timer.Reset(ch.config.Interval) + ch.Mutex.Unlock() } } } From 783b60c669b3814d9cd2e4ef703560ca33df251e Mon Sep 17 00:00:00 2001 From: lvlcn-t <75443136+lvlcn-t@users.noreply.github.com> Date: Sun, 17 Nov 2024 02:40:47 +0100 Subject: [PATCH 10/17] ci: add tparse to improve test output formatting Signed-off-by: lvlcn-t <75443136+lvlcn-t@users.noreply.github.com> --- .github/workflows/test.yml | 10 ++++++++-- .gitignore | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 451ac702..7173e5ad 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,10 +17,16 @@ jobs: with: go-version-file: go.mod - - name: Run all go tests + - name: Install dependencies run: | + go install github.com/mfridman/tparse@latest go mod download - go test -race -count=1 -coverprofile cover.out -v ./... + + - name: Run all go tests + run: | + go test -v -count=1 -race ./... -json -coverpkg ./... \ + | tee output.jsonl | tparse -notests -follow -all || true + tparse -format markdown -file output.jsonl -all -slow 20 > $GITHUB_STEP_SUMMARY traceroute: name: E2E - Traceroute diff --git a/.gitignore b/.gitignore index 22485668..2528e247 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out +output.jsonl # Dependency directories (remove the comment below to include it) # vendor/ From 0585f8090d137c3c9611add52f330a29d5253c27 Mon Sep 17 00:00:00 2001 From: lvlcn-t <75443136+lvlcn-t@users.noreply.github.com> Date: Sun, 17 Nov 2024 02:46:26 +0100 Subject: [PATCH 11/17] chore: update error messages in shutdown tests for clarity Signed-off-by: lvlcn-t <75443136+lvlcn-t@users.noreply.github.com> --- pkg/checks/base_test.go | 4 ++-- pkg/checks/dns/dns_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/checks/base_test.go b/pkg/checks/base_test.go index afd99d48..74655122 100644 --- a/pkg/checks/base_test.go +++ b/pkg/checks/base_test.go @@ -33,12 +33,12 @@ func TestBase_Shutdown(t *testing.T) { tt.b.Shutdown() if !tt.b.closed { - t.Error("Base.Shutdown() should close DoneChan") + t.Error("Base.Shutdown() should close Base.Done") } assert.Panics(t, func() { tt.b.Done <- struct{}{} - }, "Base.DoneChan should be closed") + }, "Base.Done should be closed") }) } } diff --git a/pkg/checks/dns/dns_test.go b/pkg/checks/dns/dns_test.go index 976baaf8..31f5aac8 100644 --- a/pkg/checks/dns/dns_test.go +++ b/pkg/checks/dns/dns_test.go @@ -280,7 +280,7 @@ func TestDNS_UpdateConfig(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - c := &check{} + c := NewCheck().(*check) if err := c.UpdateConfig(tt.input); (err != nil) != tt.wantErr { t.Errorf("DNS.UpdateConfig() error = %v, wantErr %v", err, tt.wantErr) From b5ffcb4542b8b3cdc9dacc5b5b13352252c2dfb7 Mon Sep 17 00:00:00 2001 From: lvlcn-t <75443136+lvlcn-t@users.noreply.github.com> Date: Sun, 17 Nov 2024 03:30:46 +0100 Subject: [PATCH 12/17] test: add remote config loader e2e tests Signed-off-by: lvlcn-t <75443136+lvlcn-t@users.noreply.github.com> --- test/e2e.go | 38 ++++++++++- test/e2e/main_test.go | 153 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 184 insertions(+), 7 deletions(-) diff --git a/test/e2e.go b/test/e2e.go index 09754e26..799e31ce 100644 --- a/test/e2e.go +++ b/test/e2e.go @@ -31,6 +31,7 @@ var _ Runner = (*E2E)(nil) type E2E struct { t *testing.T config config.Config + server *http.Server sparrow *sparrow.Sparrow checks map[string]CheckBuilder buf bytes.Buffer @@ -54,6 +55,16 @@ func (t *E2E) WithChecks(builders ...CheckBuilder) *E2E { return t } +// WithRemote sets up a remote server to serve the check config. +func (t *E2E) WithRemote() *E2E { + t.server = &http.Server{ + Addr: "localhost:50505", + Handler: http.HandlerFunc(t.serveConfig), + ReadHeaderTimeout: 3 * time.Second, + } + return t +} + // UpdateChecks updates the checks of the test. func (t *E2E) UpdateChecks(builders ...CheckBuilder) *E2E { t.checks = map[string]CheckBuilder{} @@ -87,6 +98,21 @@ func (t *E2E) Run(ctx context.Context) error { t.t.Fatalf("Failed to write check config: %v", err) } + if t.server != nil { + go func() { + err := t.server.ListenAndServe() + if err != nil && !errors.Is(err, http.ErrServerClosed) { + t.t.Errorf("Failed to start server: %v", err) + } + }() + defer func() { + err := t.server.Shutdown(ctx) + if err != nil { + t.t.Fatalf("Failed to shutdown server: %v", err) + } + }() + } + t.mu.Lock() t.running = true t.mu.Unlock() @@ -150,7 +176,7 @@ func (t *E2E) AwaitLoader() *E2E { func (t *E2E) AwaitChecks() *E2E { t.t.Helper() if !t.isRunning() { - t.t.Fatal("E2E.AwaitReadiness must be called after E2E.Run") + t.t.Fatal("E2E.AwaitChecks must be called after E2E.Run") } wait := 5 * time.Second @@ -184,6 +210,16 @@ func (t *E2E) isRunning() bool { return t.running } +// serveConfig serves the check config over HTTP as text/yaml. +func (t *E2E) serveConfig(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "text/yaml") + w.WriteHeader(http.StatusOK) + _, err := w.Write(t.buf.Bytes()) + if err != nil { + t.t.Fatalf("Failed to write response: %v", err) + } +} + // e2eHttpAsserter is an HTTP asserter for end-to-end tests. type e2eHttpAsserter struct { e2e *E2E diff --git a/test/e2e/main_test.go b/test/e2e/main_test.go index 9f016659..412849d3 100644 --- a/test/e2e/main_test.go +++ b/test/e2e/main_test.go @@ -7,6 +7,7 @@ import ( "time" "github.com/caas-team/sparrow/pkg/checks" + "github.com/caas-team/sparrow/pkg/config" "github.com/caas-team/sparrow/test" ) @@ -100,13 +101,14 @@ func TestE2E_Sparrow_WithChecks_ConfigureOnce(t *testing.T) { const loaderInterval = 5 * time.Second +type result struct { + status int + response checks.Result +} + func TestE2E_Sparrow_WithChecks_Reconfigure(t *testing.T) { framework := test.NewFramework(t) - type result struct { - status int - response checks.Result - } tests := []struct { name string startup test.ConfigBuilder @@ -210,7 +212,7 @@ func TestE2E_Sparrow_WithChecks_Reconfigure(t *testing.T) { WithTimeout(checkTimeout). WithTargets("www.example.com"), }, - wantSecond: map[string]result{ + wantSecond: map[string]result{ //nolint:dupl // This is a test "http://localhost:8080/v1/metrics/health": { status: http.StatusOK, response: checks.Result{ @@ -363,6 +365,145 @@ func TestE2E_Sparrow_WithChecks_Reconfigure(t *testing.T) { } } -func TestE2E_Sparrow_WithRemoteConfig(t *testing.T) {} +func TestE2E_Sparrow_WithRemoteConfig(t *testing.T) { + framework := test.NewFramework(t) + tests := []struct { + name string + startup test.ConfigBuilder + initialChecks []test.CheckBuilder + wantInitial map[string]result + secondChecks []test.CheckBuilder + wantSecond map[string]result + }{ + { + name: "with health check in remote config", + startup: *test.NewSparrowConfig(). + WithLoader( + test.NewLoaderConfig(). + WithInterval(loaderInterval). + FromHTTP(config.HttpLoaderConfig{Url: "http://localhost:50505/", Timeout: 5 * time.Second}). + Build(), + ), + initialChecks: []test.CheckBuilder{ + test.NewHealthCheck(). + WithInterval(checkInterval). + WithTimeout(checkTimeout). + WithTargets("https://www.example.com/"), + }, + wantInitial: map[string]result{ + "http://localhost:8080/v1/metrics/health": { + status: http.StatusOK, + response: checks.Result{ + Data: map[string]any{ + "https://www.example.com/": "healthy", + }, + Timestamp: time.Now(), + }, + }, + "http://localhost:8080/v1/metrics/latency": {status: http.StatusNotFound}, + "http://localhost:8080/v1/metrics/dns": {status: http.StatusNotFound}, + "http://localhost:8080/v1/metrics/traceroute": {status: http.StatusNotFound}, + }, + }, + { + name: "with health check in remote config then dns check", + startup: *test.NewSparrowConfig(). + WithLoader( + test.NewLoaderConfig(). + WithInterval(loaderInterval). + FromHTTP(config.HttpLoaderConfig{Url: "http://localhost:50505/", Timeout: 5 * time.Second}). + Build(), + ), + initialChecks: []test.CheckBuilder{ + test.NewHealthCheck(). + WithInterval(checkInterval). + WithTimeout(checkTimeout). + WithTargets("https://www.example.com/"), + }, + wantInitial: map[string]result{ + "http://localhost:8080/v1/metrics/health": { + status: http.StatusOK, + response: checks.Result{ + Data: map[string]any{ + "https://www.example.com/": "healthy", + }, + Timestamp: time.Now(), + }, + }, + "http://localhost:8080/v1/metrics/latency": {status: http.StatusNotFound}, + "http://localhost:8080/v1/metrics/dns": {status: http.StatusNotFound}, + "http://localhost:8080/v1/metrics/traceroute": {status: http.StatusNotFound}, + }, + secondChecks: []test.CheckBuilder{ + test.NewDNSCheck(). + WithInterval(checkInterval). + WithTimeout(checkTimeout). + WithTargets("www.example.com"), + }, + wantSecond: map[string]result{ //nolint:dupl // This is a test + "http://localhost:8080/v1/metrics/health": { + status: http.StatusOK, + response: checks.Result{ + Data: map[string]any{ + "https://www.example.com/": "healthy", + }, + Timestamp: time.Now(), + }, + }, + "http://localhost:8080/v1/metrics/latency": {status: http.StatusNotFound}, + "http://localhost:8080/v1/metrics/dns": { + status: http.StatusOK, + response: checks.Result{ + Data: map[string]any{ + "www.example.com": map[string]any{ + "resolved": []string{"1.2.3.4"}, + "error": nil, + "total": time.Since(time.Now().Add(-100 * time.Millisecond)).Seconds(), + }, + }, + Timestamp: time.Now(), + }, + }, + "http://localhost:8080/v1/metrics/traceroute": {status: http.StatusNotFound}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e2e := framework.E2E(t, tt.startup.Config(t)). + WithChecks(tt.initialChecks...). + WithRemote() + + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) + defer cancel() + + finish := make(chan error, 1) + go func() { + finish <- e2e.Run(ctx) + }() + e2e.AwaitStartup("http://localhost:8080", checkTimeout).AwaitChecks() + + for url, result := range tt.wantInitial { + e2e.HttpAssertion(url). + WithSchema(). + WithCheckResult(result.response). + Assert(result.status) + } + + if len(tt.secondChecks) > 0 { + e2e.UpdateChecks(tt.secondChecks...).AwaitLoader().AwaitChecks() + for url, result := range tt.wantSecond { + e2e.HttpAssertion(url). + WithSchema(). + WithCheckResult(result.response). + Assert(result.status) + } + } + cancel() + <-finish + }) + } +} func TestE2E_Sparrow_WithTargetManager(t *testing.T) {} From dd455b1b685342b5c54c96c4cf5b7df8a2d1fc4f Mon Sep 17 00:00:00 2001 From: lvlcn-t <75443136+lvlcn-t@users.noreply.github.com> Date: Sun, 17 Nov 2024 15:27:13 +0100 Subject: [PATCH 13/17] test: split test types to short/long to improve test execution speed Signed-off-by: lvlcn-t <75443136+lvlcn-t@users.noreply.github.com> --- .github/workflows/test.yml | 26 +++- internal/helper/retry_test.go | 6 + internal/logger/logger_test.go | 14 ++ pkg/api/api_test.go | 12 +- pkg/checks/base_test.go | 3 + pkg/checks/dns/config_test.go | 3 + pkg/checks/dns/dns_test.go | 10 +- pkg/checks/dns/metrics_test.go | 3 + pkg/checks/health/config_test.go | 4 + pkg/checks/health/health_test.go | 7 + pkg/checks/latency/config_test.go | 4 + pkg/checks/latency/latency_test.go | 10 +- pkg/checks/oapi_test.go | 3 + pkg/checks/traceroute/check_test.go | 5 + pkg/checks/traceroute/traceroute_test.go | 6 + pkg/config/file_test.go | 8 +- pkg/config/http_test.go | 13 ++ pkg/config/validate_test.go | 6 +- pkg/db/db_test.go | 11 ++ pkg/factory/factory_test.go | 5 + pkg/sparrow/controller_test.go | 15 +- pkg/sparrow/handlers_test.go | 5 + pkg/sparrow/metrics/metrics_test.go | 7 + pkg/sparrow/run_test.go | 7 + pkg/sparrow/targets/manager_test.go | 31 ++++ .../targets/remote/gitlab/gitlab_test.go | 15 ++ pkg/sparrow/targets/targetmanager_test.go | 4 + test/e2e/main_test.go | 147 +++++++++--------- test/flags.go | 19 +++ test/{ => framework/builder}/checks.go | 12 +- test/{ => framework/builder}/startup.go | 22 +-- test/{ => framework}/e2e.go | 73 ++++----- test/{ => framework}/framework.go | 25 +-- test/unit.go | 19 --- 34 files changed, 389 insertions(+), 171 deletions(-) create mode 100644 test/flags.go rename test/{ => framework/builder}/checks.go (97%) rename test/{ => framework/builder}/startup.go (82%) rename test/{ => framework}/e2e.go (89%) rename test/{ => framework}/framework.go (58%) delete mode 100644 test/unit.go diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7173e5ad..5c8f5a9e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,10 +6,28 @@ permissions: contents: read jobs: - go: - # TODO: Split go tests into multiple jobs to reduce the time - # e.g. go-unit, go-e2e - name: Go - Tests + go-unit: + name: Unit - Go + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Install dependencies + run: | + go install github.com/mfridman/tparse@latest + go mod download + + - name: Run all go tests + run: | + go test -v -count=1 -test.short -race ./... -json -coverpkg ./... \ + | tee output.jsonl | tparse -notests -follow -all || true + tparse -format markdown -file output.jsonl -all -slow 20 > $GITHUB_STEP_SUMMARY + + go-e2e: + name: E2E - Go runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/internal/helper/retry_test.go b/internal/helper/retry_test.go index 28230d6c..e065b080 100644 --- a/internal/helper/retry_test.go +++ b/internal/helper/retry_test.go @@ -24,9 +24,13 @@ import ( "fmt" "testing" "time" + + "github.com/caas-team/sparrow/test" ) func TestRetry(t *testing.T) { + test.MarkAsShort(t) + effectorFuncCallCounter := 0 ctx, cancel := context.WithCancel(context.Background()) @@ -127,6 +131,8 @@ func TestRetry(t *testing.T) { } func Test_getExpBackoff(t *testing.T) { + test.MarkAsShort(t) + type args struct { initialDelay time.Duration iteration int diff --git a/internal/logger/logger_test.go b/internal/logger/logger_test.go index 6707f435..efcbae04 100644 --- a/internal/logger/logger_test.go +++ b/internal/logger/logger_test.go @@ -26,9 +26,13 @@ import ( "os" "reflect" "testing" + + "github.com/caas-team/sparrow/test" ) func TestNewLogger(t *testing.T) { + test.MarkAsShort(t) + tests := []struct { name string handlers []slog.Handler @@ -81,6 +85,8 @@ func TestNewLogger(t *testing.T) { } func TestNewContextWithLogger(t *testing.T) { + test.MarkAsShort(t) + tests := []struct { name string parentCtx context.Context @@ -112,6 +118,8 @@ func TestNewContextWithLogger(t *testing.T) { } func TestFromContext(t *testing.T) { + test.MarkAsShort(t) + tests := []struct { name string ctx context.Context @@ -145,6 +153,8 @@ func TestFromContext(t *testing.T) { } func TestMiddleware(t *testing.T) { + test.MarkAsShort(t) + tests := []struct { name string parentCtx context.Context @@ -181,6 +191,8 @@ func TestMiddleware(t *testing.T) { } func TestNewHandler(t *testing.T) { + test.MarkAsShort(t) + tests := []struct { name string format string @@ -239,6 +251,8 @@ func TestNewHandler(t *testing.T) { } func TestGetLevel(t *testing.T) { + test.MarkAsShort(t) + tests := []struct { name string input string diff --git a/pkg/api/api_test.go b/pkg/api/api_test.go index 1637e16e..85227591 100644 --- a/pkg/api/api_test.go +++ b/pkg/api/api_test.go @@ -26,10 +26,13 @@ import ( "testing" "time" + "github.com/caas-team/sparrow/test" "github.com/go-chi/chi/v5" ) func TestAPI_Run(t *testing.T) { + test.MarkAsShort(t) + tests := []struct { name string want struct { @@ -96,6 +99,8 @@ func TestAPI_Run(t *testing.T) { } func TestAPI_RegisterRoutes(t *testing.T) { + test.MarkAsShort(t) + tests := []struct { name string routes []Route @@ -196,6 +201,8 @@ func TestAPI_RegisterRoutes(t *testing.T) { } func TestAPI_ShutdownWhenContextCanceled(t *testing.T) { + test.MarkAsShort(t) + a := api{ router: chi.NewRouter(), server: &http.Server{}, //nolint:gosec @@ -213,8 +220,9 @@ func TestAPI_ShutdownWhenContextCanceled(t *testing.T) { } func TestAPI_OkHandler(t *testing.T) { - ctx := context.Background() + test.MarkAsShort(t) + ctx := context.Background() req, err := http.NewRequestWithContext(ctx, "GET", "/okHandler", http.NoBody) if err != nil { t.Fatal(err) @@ -238,6 +246,8 @@ func TestAPI_OkHandler(t *testing.T) { } func TestConfig_Validate(t *testing.T) { + test.MarkAsShort(t) + cases := []struct { name string config Config diff --git a/pkg/checks/base_test.go b/pkg/checks/base_test.go index 74655122..47e10493 100644 --- a/pkg/checks/base_test.go +++ b/pkg/checks/base_test.go @@ -3,10 +3,13 @@ package checks import ( "testing" + "github.com/caas-team/sparrow/test" "github.com/stretchr/testify/assert" ) func TestBase_Shutdown(t *testing.T) { + test.MarkAsShort(t) + tests := []struct { name string b *Base diff --git a/pkg/checks/dns/config_test.go b/pkg/checks/dns/config_test.go index 5cbc2767..7f34fb77 100644 --- a/pkg/checks/dns/config_test.go +++ b/pkg/checks/dns/config_test.go @@ -21,9 +21,12 @@ package dns import ( "testing" "time" + + "github.com/caas-team/sparrow/test" ) func TestConfig_Validate(t *testing.T) { + test.MarkAsShort(t) tests := []struct { name string config Config diff --git a/pkg/checks/dns/dns_test.go b/pkg/checks/dns/dns_test.go index 31f5aac8..1520ee9d 100644 --- a/pkg/checks/dns/dns_test.go +++ b/pkg/checks/dns/dns_test.go @@ -28,6 +28,7 @@ import ( "github.com/caas-team/sparrow/pkg/checks" "github.com/caas-team/sparrow/pkg/checks/health" + "github.com/caas-team/sparrow/test" "github.com/stretchr/testify/assert" ) @@ -40,6 +41,8 @@ const ( ) func TestDNS_Run(t *testing.T) { + test.MarkAsShort(t) + tests := []struct { name string mockSetup func() *check @@ -208,8 +211,9 @@ func TestDNS_Run(t *testing.T) { } func TestDNS_Run_Context_Done(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) + test.MarkAsShort(t) + ctx, cancel := context.WithCancel(context.Background()) c := NewCheck() cResult := make(chan checks.ResultDTO, 1) defer close(cResult) @@ -238,6 +242,8 @@ func TestDNS_Run_Context_Done(t *testing.T) { } func TestDNS_UpdateConfig(t *testing.T) { + test.MarkAsShort(t) + tests := []struct { name string input checks.Runtime @@ -291,6 +297,8 @@ func TestDNS_UpdateConfig(t *testing.T) { } func TestNewCheck(t *testing.T) { + test.MarkAsShort(t) + c := NewCheck() if c == nil { t.Error("NewLatencyCheck() should not be nil") diff --git a/pkg/checks/dns/metrics_test.go b/pkg/checks/dns/metrics_test.go index e3930088..2697ee16 100644 --- a/pkg/checks/dns/metrics_test.go +++ b/pkg/checks/dns/metrics_test.go @@ -21,10 +21,13 @@ package dns import ( "testing" + "github.com/caas-team/sparrow/test" "github.com/prometheus/client_golang/prometheus" ) func TestMetrics_GetCollectors(t *testing.T) { + test.MarkAsShort(t) + tests := []struct { name string metrics metrics diff --git a/pkg/checks/health/config_test.go b/pkg/checks/health/config_test.go index 2e7d1199..27c998be 100644 --- a/pkg/checks/health/config_test.go +++ b/pkg/checks/health/config_test.go @@ -21,9 +21,13 @@ package health import ( "testing" "time" + + "github.com/caas-team/sparrow/test" ) func TestConfig_Validate(t *testing.T) { + test.MarkAsShort(t) + tests := []struct { name string config Config diff --git a/pkg/checks/health/health_test.go b/pkg/checks/health/health_test.go index 5b387170..76a2d55b 100644 --- a/pkg/checks/health/health_test.go +++ b/pkg/checks/health/health_test.go @@ -26,12 +26,15 @@ import ( "github.com/caas-team/sparrow/pkg/checks" "github.com/caas-team/sparrow/pkg/checks/latency" + "github.com/caas-team/sparrow/test" "github.com/jarcoal/httpmock" "github.com/stretchr/testify/assert" ) func TestHealth_UpdateConfig(t *testing.T) { + test.MarkAsShort(t) + tests := []struct { name string inputConfig checks.Runtime @@ -80,6 +83,8 @@ func TestHealth_UpdateConfig(t *testing.T) { } func Test_getHealth(t *testing.T) { + test.MarkAsShort(t) + httpmock.Activate() defer httpmock.DeactivateAndReset() endpoint := "https://api.test.com/test" @@ -148,6 +153,8 @@ func Test_getHealth(t *testing.T) { } func TestHealth_Check(t *testing.T) { + test.MarkAsShort(t) + httpmock.Activate() defer httpmock.DeactivateAndReset() diff --git a/pkg/checks/latency/config_test.go b/pkg/checks/latency/config_test.go index 4deb128f..8d326a4f 100644 --- a/pkg/checks/latency/config_test.go +++ b/pkg/checks/latency/config_test.go @@ -21,9 +21,13 @@ package latency import ( "testing" "time" + + "github.com/caas-team/sparrow/test" ) func TestConfig_Validate(t *testing.T) { + test.MarkAsShort(t) + tests := []struct { name string config Config diff --git a/pkg/checks/latency/latency_test.go b/pkg/checks/latency/latency_test.go index 89351108..d8683554 100644 --- a/pkg/checks/latency/latency_test.go +++ b/pkg/checks/latency/latency_test.go @@ -27,6 +27,7 @@ import ( "time" "github.com/caas-team/sparrow/pkg/checks" + "github.com/caas-team/sparrow/test" "github.com/jarcoal/httpmock" "github.com/stretchr/testify/assert" @@ -43,6 +44,8 @@ func stringPointer(s string) *string { } func TestLatency_Run(t *testing.T) { + test.MarkAsShort(t) + httpmock.Activate() defer httpmock.DeactivateAndReset() @@ -180,9 +183,10 @@ func TestLatency_Run(t *testing.T) { } func TestLatency_check(t *testing.T) { + test.MarkAsShort(t) + httpmock.Activate() t.Cleanup(httpmock.DeactivateAndReset) - tests := []struct { name string registeredEndpoints []struct { @@ -306,6 +310,8 @@ func TestLatency_check(t *testing.T) { } func TestLatency_UpdateConfig(t *testing.T) { + test.MarkAsShort(t) + c := NewCheck().(*check) wantCfg := Config{ Targets: []string{"http://localhost:9090"}, @@ -321,6 +327,8 @@ func TestLatency_UpdateConfig(t *testing.T) { } func TestNewLatencyCheck(t *testing.T) { + test.MarkAsShort(t) + c := NewCheck() if c == nil { t.Error("NewLatencyCheck() should not be nil") diff --git a/pkg/checks/oapi_test.go b/pkg/checks/oapi_test.go index 3085f2ed..f161b690 100644 --- a/pkg/checks/oapi_test.go +++ b/pkg/checks/oapi_test.go @@ -22,10 +22,13 @@ import ( "reflect" "testing" + "github.com/caas-team/sparrow/test" "github.com/getkin/kin-openapi/openapi3" ) func TestOpenapiFromPerfData(t *testing.T) { + test.MarkAsShort(t) + type args[T any] struct { perfData T } diff --git a/pkg/checks/traceroute/check_test.go b/pkg/checks/traceroute/check_test.go index e597b472..6762c570 100644 --- a/pkg/checks/traceroute/check_test.go +++ b/pkg/checks/traceroute/check_test.go @@ -25,11 +25,14 @@ import ( "time" "github.com/caas-team/sparrow/pkg/checks" + "github.com/caas-team/sparrow/test" "github.com/google/go-cmp/cmp" "go.opentelemetry.io/otel" ) func TestCheck(t *testing.T) { + test.MarkAsShort(t) + cases := []struct { name string c *check @@ -137,6 +140,8 @@ func ipFromInt(i int) string { } func TestIpFromInt(t *testing.T) { + test.MarkAsShort(t) + cases := []struct { In int Expected string diff --git a/pkg/checks/traceroute/traceroute_test.go b/pkg/checks/traceroute/traceroute_test.go index 2b6f3bb9..b9b886cf 100644 --- a/pkg/checks/traceroute/traceroute_test.go +++ b/pkg/checks/traceroute/traceroute_test.go @@ -22,9 +22,13 @@ import ( "net" "reflect" "testing" + + "github.com/caas-team/sparrow/test" ) func TestHopAddress_String(t *testing.T) { + test.MarkAsShort(t) + type fields struct { IP string Port int @@ -51,6 +55,8 @@ func TestHopAddress_String(t *testing.T) { } func Test_newHopAddress(t *testing.T) { + test.MarkAsShort(t) + type args struct { addr net.Addr } diff --git a/pkg/config/file_test.go b/pkg/config/file_test.go index 76fab30e..688fd566 100644 --- a/pkg/config/file_test.go +++ b/pkg/config/file_test.go @@ -29,12 +29,14 @@ import ( "github.com/caas-team/sparrow/pkg/checks/health" "github.com/caas-team/sparrow/pkg/checks/runtime" "github.com/caas-team/sparrow/pkg/config/test" + testUtils "github.com/caas-team/sparrow/test" "github.com/goccy/go-yaml" ) func TestNewFileLoader(t *testing.T) { - l := NewFileLoader(&Config{Loader: LoaderConfig{File: FileLoaderConfig{Path: "config.yaml"}}}, make(chan runtime.Config, 1)) + testUtils.MarkAsShort(t) + l := NewFileLoader(&Config{Loader: LoaderConfig{File: FileLoaderConfig{Path: "config.yaml"}}}, make(chan runtime.Config, 1)) if l.config.File.Path != "config.yaml" { t.Errorf("Expected path to be config.yaml, got %s", l.config.File.Path) } @@ -47,6 +49,8 @@ func TestNewFileLoader(t *testing.T) { } func TestFileLoader_Run(t *testing.T) { + testUtils.MarkAsShort(t) + tests := []struct { name string config LoaderConfig @@ -119,6 +123,8 @@ func TestFileLoader_Run(t *testing.T) { } func TestFileLoader_getRuntimeConfig(t *testing.T) { + testUtils.MarkAsShort(t) + tests := []struct { name string config LoaderConfig diff --git a/pkg/config/http_test.go b/pkg/config/http_test.go index e1bb41f1..235f4a16 100644 --- a/pkg/config/http_test.go +++ b/pkg/config/http_test.go @@ -33,12 +33,15 @@ import ( "github.com/caas-team/sparrow/internal/logger" "github.com/caas-team/sparrow/pkg/checks/health" "github.com/caas-team/sparrow/pkg/checks/runtime" + "github.com/caas-team/sparrow/test" "github.com/goccy/go-yaml" "github.com/jarcoal/httpmock" "github.com/stretchr/testify/require" ) func TestHttpLoader_GetRuntimeConfig(t *testing.T) { + test.MarkAsShort(t) + httpmock.Activate() defer httpmock.DeactivateAndReset() @@ -155,6 +158,8 @@ func TestHttpLoader_GetRuntimeConfig(t *testing.T) { // The test runs the Run method for a while // and then shuts it down via a goroutine func TestHttpLoader_Run(t *testing.T) { + test.MarkAsShort(t) + httpmock.Activate() defer httpmock.DeactivateAndReset() @@ -255,6 +260,8 @@ func TestHttpLoader_Run(t *testing.T) { } func TestHttpLoader_Shutdown(t *testing.T) { + test.MarkAsShort(t) + tests := []struct { name string }{ @@ -283,6 +290,8 @@ func TestHttpLoader_Shutdown(t *testing.T) { // TestHttpLoader_Run_config_sent_to_channel tests if the config is sent to the channel // when the Run method is called and the remote endpoint returns a valid response func TestHttpLoader_Run_config_sent_to_channel(t *testing.T) { + test.MarkAsShort(t) + httpmock.Activate() defer httpmock.DeactivateAndReset() @@ -345,6 +354,8 @@ func TestHttpLoader_Run_config_sent_to_channel(t *testing.T) { // when the Run method is called // and the remote endpoint returns a non-200 response func TestHttpLoader_Run_empty_config_sent_to_channel_500(t *testing.T) { + test.MarkAsShort(t) + httpmock.Activate() defer httpmock.DeactivateAndReset() @@ -396,6 +407,8 @@ func TestHttpLoader_Run_empty_config_sent_to_channel_500(t *testing.T) { // when the Run method is called // and the client can't execute the requests func TestHttpLoader_Run_empty_config_sent_to_channel_client_error(t *testing.T) { + test.MarkAsShort(t) + httpmock.Activate() defer httpmock.DeactivateAndReset() diff --git a/pkg/config/validate_test.go b/pkg/config/validate_test.go index 398e66a9..cc52a4b0 100644 --- a/pkg/config/validate_test.go +++ b/pkg/config/validate_test.go @@ -25,11 +25,13 @@ import ( "github.com/caas-team/sparrow/internal/helper" "github.com/caas-team/sparrow/pkg/api" + "github.com/caas-team/sparrow/test" ) func TestConfig_Validate(t *testing.T) { - ctx := context.Background() + test.MarkAsShort(t) + ctx := context.Background() tests := []struct { name string config Config @@ -170,6 +172,8 @@ func TestConfig_Validate(t *testing.T) { } func Test_isDNSName(t *testing.T) { + test.MarkAsShort(t) + tests := []struct { name string dnsName string diff --git a/pkg/db/db_test.go b/pkg/db/db_test.go index 120d0414..155bca16 100644 --- a/pkg/db/db_test.go +++ b/pkg/db/db_test.go @@ -24,9 +24,12 @@ import ( "testing" "github.com/caas-team/sparrow/pkg/checks" + "github.com/caas-team/sparrow/test" ) func TestInMemory_Save(t *testing.T) { + test.MarkAsShort(t) + type fields struct { data map[string]checks.Result } @@ -63,6 +66,8 @@ func TestInMemory_Save(t *testing.T) { } func TestNewInMemory(t *testing.T) { + test.MarkAsShort(t) + tests := []struct { name string want *InMemory @@ -79,6 +84,8 @@ func TestNewInMemory(t *testing.T) { } func TestInMemory_Get(t *testing.T) { + test.MarkAsShort(t) + type fields struct { data map[string]*checks.Result } @@ -121,6 +128,8 @@ func TestInMemory_Get(t *testing.T) { } func TestInMemory_List(t *testing.T) { + test.MarkAsShort(t) + type fields struct { data map[string]*checks.Result } @@ -172,6 +181,8 @@ func TestInMemory_List(t *testing.T) { } func TestInMemory_ListThreadsafe(t *testing.T) { + test.MarkAsShort(t) + db := NewInMemory() db.Save(checks.ResultDTO{Name: "alpha", Result: &checks.Result{Data: 0}}) db.Save(checks.ResultDTO{Name: "beta", Result: &checks.Result{Data: 1}}) diff --git a/pkg/factory/factory_test.go b/pkg/factory/factory_test.go index ea403146..0b19dfbd 100644 --- a/pkg/factory/factory_test.go +++ b/pkg/factory/factory_test.go @@ -27,6 +27,7 @@ import ( "github.com/caas-team/sparrow/pkg/checks/health" "github.com/caas-team/sparrow/pkg/checks/latency" "github.com/caas-team/sparrow/pkg/checks/runtime" + "github.com/caas-team/sparrow/test" ) var ( @@ -43,6 +44,8 @@ var ( ) func TestNewChecksFromConfig(t *testing.T) { + test.MarkAsShort(t) + tests := []struct { name string cfg runtime.Config @@ -121,6 +124,8 @@ func newLatencyCheck() checks.Check { } func TestNewCheck(t *testing.T) { + test.MarkAsShort(t) + tests := []struct { name string cfg checks.Runtime diff --git a/pkg/sparrow/controller_test.go b/pkg/sparrow/controller_test.go index f68887db..0a2008dd 100644 --- a/pkg/sparrow/controller_test.go +++ b/pkg/sparrow/controller_test.go @@ -33,12 +33,15 @@ import ( "github.com/caas-team/sparrow/pkg/checks/runtime" "github.com/caas-team/sparrow/pkg/db" "github.com/caas-team/sparrow/pkg/sparrow/metrics" + "github.com/caas-team/sparrow/test" "github.com/getkin/kin-openapi/openapi3" "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/assert" ) func TestRun_CheckRunError(t *testing.T) { + test.MarkAsShort(t) + ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -80,8 +83,10 @@ func TestRun_CheckRunError(t *testing.T) { } func TestRun_ContextCancellation(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) + test.MarkAsShort(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() cc := NewChecksController(db.NewInMemory(), metrics.New(metrics.Config{})) done := make(chan struct{}) @@ -104,6 +109,8 @@ func TestRun_ContextCancellation(t *testing.T) { } func TestChecksController_Reconcile(t *testing.T) { + test.MarkAsShort(t) + ctx, cancel := logger.NewContextWithLogger(context.Background()) defer cancel() rtcfg := &runtime.Config{} @@ -236,6 +243,8 @@ func TestChecksController_Reconcile(t *testing.T) { } func TestChecksController_RegisterCheck(t *testing.T) { + test.MarkAsShort(t) + tests := []struct { name string setup func() *ChecksController @@ -262,6 +271,8 @@ func TestChecksController_RegisterCheck(t *testing.T) { } func TestChecksController_UnregisterCheck(t *testing.T) { + test.MarkAsShort(t) + tests := []struct { name string check checks.Check @@ -286,6 +297,8 @@ func TestChecksController_UnregisterCheck(t *testing.T) { } func TestGenerateCheckSpecs(t *testing.T) { + test.MarkAsShort(t) + tests := []struct { name string checks []checks.Check diff --git a/pkg/sparrow/handlers_test.go b/pkg/sparrow/handlers_test.go index 555b1b08..1a34fa12 100644 --- a/pkg/sparrow/handlers_test.go +++ b/pkg/sparrow/handlers_test.go @@ -32,12 +32,15 @@ import ( "github.com/caas-team/sparrow/pkg/checks" "github.com/caas-team/sparrow/pkg/checks/runtime" "github.com/caas-team/sparrow/pkg/db" + "github.com/caas-team/sparrow/test" "github.com/getkin/kin-openapi/openapi3" "github.com/go-chi/chi/v5" "github.com/goccy/go-yaml" ) func TestSparrow_handleOpenAPI(t *testing.T) { + test.MarkAsShort(t) + s := Sparrow{ controller: &ChecksController{ checks: runtime.Checks{}, @@ -91,6 +94,8 @@ func TestSparrow_handleOpenAPI(t *testing.T) { } func TestSparrow_handleCheckMetrics(t *testing.T) { + test.MarkAsShort(t) + tests := []struct { name string want []byte diff --git a/pkg/sparrow/metrics/metrics_test.go b/pkg/sparrow/metrics/metrics_test.go index 80c0f566..cf58cb32 100644 --- a/pkg/sparrow/metrics/metrics_test.go +++ b/pkg/sparrow/metrics/metrics_test.go @@ -23,12 +23,15 @@ import ( "reflect" "testing" + "github.com/caas-team/sparrow/test" "github.com/prometheus/client_golang/prometheus" "go.opentelemetry.io/otel" sdktrace "go.opentelemetry.io/otel/sdk/trace" ) func TestPrometheusMetrics_GetRegistry(t *testing.T) { + test.MarkAsShort(t) + tests := []struct { name string registry *prometheus.Registry @@ -53,6 +56,8 @@ func TestPrometheusMetrics_GetRegistry(t *testing.T) { } func TestNewMetrics(t *testing.T) { + test.MarkAsShort(t) + testMetrics := New(Config{}) testGauge := prometheus.NewGauge( prometheus.GaugeOpts{ @@ -68,6 +73,8 @@ func TestNewMetrics(t *testing.T) { } func TestMetrics_InitTracing(t *testing.T) { + test.MarkAsShort(t) + tests := []struct { name string config Config diff --git a/pkg/sparrow/run_test.go b/pkg/sparrow/run_test.go index a004db2a..5d8018d3 100644 --- a/pkg/sparrow/run_test.go +++ b/pkg/sparrow/run_test.go @@ -34,12 +34,15 @@ import ( "github.com/caas-team/sparrow/pkg/sparrow/targets/interactor" "github.com/caas-team/sparrow/pkg/sparrow/targets/remote/gitlab" managermock "github.com/caas-team/sparrow/pkg/sparrow/targets/test" + "github.com/caas-team/sparrow/test" "github.com/stretchr/testify/assert" ) // TestSparrow_Run_FullComponentStart tests that the Run method starts the API, // loader and a targetManager all start. func TestSparrow_Run_FullComponentStart(t *testing.T) { + test.MarkAsShort(t) + c := &config.Config{ Api: api.Config{ListeningAddress: ":9090"}, Loader: config.LoaderConfig{ @@ -81,6 +84,8 @@ func TestSparrow_Run_FullComponentStart(t *testing.T) { // TestSparrow_Run_ContextCancel tests that after a context cancels the Run method // will return an error and all started components will be shut down. func TestSparrow_Run_ContextCancel(t *testing.T) { + test.MarkAsShort(t) + c := &config.Config{ Api: api.Config{ListeningAddress: ":9090"}, Loader: config.LoaderConfig{ @@ -112,6 +117,8 @@ func TestSparrow_Run_ContextCancel(t *testing.T) { // TestSparrow_enrichTargets tests that the enrichTargets method // updates the targets of the configured checks. func TestSparrow_enrichTargets(t *testing.T) { + test.MarkAsShort(t) + t.Parallel() now := time.Now() testTarget := "https://localhost.de" diff --git a/pkg/sparrow/targets/manager_test.go b/pkg/sparrow/targets/manager_test.go index 8081eef7..915449c8 100644 --- a/pkg/sparrow/targets/manager_test.go +++ b/pkg/sparrow/targets/manager_test.go @@ -27,6 +27,7 @@ import ( "time" "github.com/caas-team/sparrow/pkg/checks" + "github.com/caas-team/sparrow/test" remotemock "github.com/caas-team/sparrow/pkg/sparrow/targets/remote/test" ) @@ -42,6 +43,8 @@ const ( // targets list. When an unhealthyTheshold is set, it will also unregister // unhealthy targets func Test_gitlabTargetManager_refreshTargets(t *testing.T) { + test.MarkAsShort(t) + now := time.Now() tooOld := now.Add(-time.Hour * 2) @@ -137,6 +140,8 @@ func Test_gitlabTargetManager_refreshTargets(t *testing.T) { // refreshTargets method will not unregister unhealthy targets if the // unhealthyThreshold is 0 func Test_gitlabTargetManager_refreshTargets_No_Threshold(t *testing.T) { + test.MarkAsShort(t) + tests := []struct { name string mockTargets []checks.GlobalTarget @@ -206,6 +211,8 @@ func Test_gitlabTargetManager_refreshTargets_No_Threshold(t *testing.T) { } func Test_gitlabTargetManager_GetTargets(t *testing.T) { + test.MarkAsShort(t) + now := time.Now() tests := []struct { name string @@ -282,6 +289,8 @@ func Test_gitlabTargetManager_GetTargets(t *testing.T) { // Test_gitlabTargetManager_registerSparrow tests that the register method will // register the sparrow instance in the remote instance func Test_gitlabTargetManager_register(t *testing.T) { + test.MarkAsShort(t) + tests := []struct { name string wantErr bool @@ -321,6 +330,8 @@ func Test_gitlabTargetManager_register(t *testing.T) { // Test_gitlabTargetManager_update tests that the update // method will update the registration of the sparrow instance in the remote instance func Test_gitlabTargetManager_update(t *testing.T) { + test.MarkAsShort(t) + tests := []struct { name string wantPutError bool @@ -355,6 +366,8 @@ func Test_gitlabTargetManager_update(t *testing.T) { // will register the target if it is not registered yet and update the // registration if it is already registered func Test_gitlabTargetManager_Reconcile_success(t *testing.T) { + test.MarkAsShort(t) + tests := []struct { name string registered bool @@ -415,6 +428,8 @@ func Test_gitlabTargetManager_Reconcile_success(t *testing.T) { // method will register the sparrow, and then update the registration after the // registration interval has passed func Test_gitlabTargetManager_Reconcile_Registration_Update(t *testing.T) { + test.MarkAsShort(t) + glmock := remotemock.New( []checks.GlobalTarget{ { @@ -475,6 +490,8 @@ func Test_gitlabTargetManager_Reconcile_Registration_Update(t *testing.T) { // Test_gitlabTargetManager_Reconcile_failure tests that the Reconcile method // will handle API failures gracefully func Test_gitlabTargetManager_Reconcile_failure(t *testing.T) { + test.MarkAsShort(t) + tests := []struct { name string registered bool @@ -539,6 +556,8 @@ func Test_gitlabTargetManager_Reconcile_failure(t *testing.T) { // Test_gitlabTargetManager_Reconcile_Context_Canceled tests that the Reconcile // method will shutdown gracefully when the context is canceled. func Test_gitlabTargetManager_Reconcile_Context_Canceled(t *testing.T) { + test.MarkAsShort(t) + glmock := remotemock.New( []checks.GlobalTarget{ { @@ -579,6 +598,8 @@ func Test_gitlabTargetManager_Reconcile_Context_Canceled(t *testing.T) { // Test_gitlabTargetManager_Reconcile_Context_Done tests that the Reconcile // method will shut down gracefully when the context is done. func Test_gitlabTargetManager_Reconcile_Context_Done(t *testing.T) { + test.MarkAsShort(t) + glmock := remotemock.New( []checks.GlobalTarget{ { @@ -617,6 +638,8 @@ func Test_gitlabTargetManager_Reconcile_Context_Done(t *testing.T) { // Test_gitlabTargetManager_Reconcile_Shutdown tests that the Reconcile // method will shut down gracefully when the Shutdown method is called. func Test_gitlabTargetManager_Reconcile_Shutdown(t *testing.T) { + test.MarkAsShort(t) + glmock := remotemock.New( []checks.GlobalTarget{ { @@ -660,6 +683,8 @@ func Test_gitlabTargetManager_Reconcile_Shutdown(t *testing.T) { // method will fail the graceful shutdown when the Shutdown method is called // and the unregistering fails. func Test_gitlabTargetManager_Reconcile_Shutdown_Fail_Unregister(t *testing.T) { + test.MarkAsShort(t) + glmock := remotemock.New( []checks.GlobalTarget{ { @@ -704,6 +729,8 @@ func Test_gitlabTargetManager_Reconcile_Shutdown_Fail_Unregister(t *testing.T) { // Test_gitlabTargetManager_Reconcile_No_Registration tests that the Reconcile // method will not register the instance if the registration interval is 0 func Test_gitlabTargetManager_Reconcile_No_Registration(t *testing.T) { + test.MarkAsShort(t) + glmock := remotemock.New( []checks.GlobalTarget{ { @@ -742,6 +769,8 @@ func Test_gitlabTargetManager_Reconcile_No_Registration(t *testing.T) { // Test_gitlabTargetManager_Reconcile_No_Update tests that the Reconcile // method will not update the registration if the update interval is 0 func Test_gitlabTargetManager_Reconcile_No_Update(t *testing.T) { + test.MarkAsShort(t) + glmock := remotemock.New( []checks.GlobalTarget{ { @@ -784,6 +813,8 @@ func Test_gitlabTargetManager_Reconcile_No_Update(t *testing.T) { // method will not register the instance if the registration interval is 0 // and will not update the registration if the update interval is 0 func Test_gitlabTargetManager_Reconcile_No_Registration_No_Update(t *testing.T) { + test.MarkAsShort(t) + glmock := remotemock.New( []checks.GlobalTarget{ { diff --git a/pkg/sparrow/targets/remote/gitlab/gitlab_test.go b/pkg/sparrow/targets/remote/gitlab/gitlab_test.go index 6b60aa50..2ee3151c 100644 --- a/pkg/sparrow/targets/remote/gitlab/gitlab_test.go +++ b/pkg/sparrow/targets/remote/gitlab/gitlab_test.go @@ -28,11 +28,14 @@ import ( "github.com/caas-team/sparrow/pkg/checks" "github.com/caas-team/sparrow/pkg/sparrow/targets/remote" + "github.com/caas-team/sparrow/test" "github.com/jarcoal/httpmock" ) func Test_gitlab_fetchFileList(t *testing.T) { + test.MarkAsShort(t) + type file struct { Name string `json:"name"` } @@ -133,6 +136,8 @@ func Test_gitlab_fetchFileList(t *testing.T) { // The filelist and url are the same, so we HTTP responders can // be created without much hassle func Test_gitlab_FetchFiles(t *testing.T) { + test.MarkAsShort(t) + type file struct { Name string `json:"name"` } @@ -232,6 +237,8 @@ func Test_gitlab_FetchFiles(t *testing.T) { } func Test_gitlab_fetchFiles_error_cases(t *testing.T) { + test.MarkAsShort(t) + type file struct { Name string `json:"name"` } @@ -316,6 +323,8 @@ func Test_gitlab_fetchFiles_error_cases(t *testing.T) { } func TestClient_PutFile(t *testing.T) { //nolint:dupl // no need to refactor yet + test.MarkAsShort(t) + now := time.Now() tests := []struct { name string @@ -391,6 +400,8 @@ func TestClient_PutFile(t *testing.T) { //nolint:dupl // no need to refactor yet } func TestClient_PostFile(t *testing.T) { //nolint:dupl // no need to refactor yet + test.MarkAsShort(t) + now := time.Now() tests := []struct { name string @@ -466,6 +477,8 @@ func TestClient_PostFile(t *testing.T) { //nolint:dupl // no need to refactor ye } func TestClient_DeleteFile(t *testing.T) { + test.MarkAsShort(t) + tests := []struct { name string fileName string @@ -522,6 +535,8 @@ func TestClient_DeleteFile(t *testing.T) { } func TestClient_fetchDefaultBranch(t *testing.T) { + test.MarkAsShort(t) + tests := []struct { name string code int diff --git a/pkg/sparrow/targets/targetmanager_test.go b/pkg/sparrow/targets/targetmanager_test.go index 324b8204..f43bc9e3 100644 --- a/pkg/sparrow/targets/targetmanager_test.go +++ b/pkg/sparrow/targets/targetmanager_test.go @@ -22,9 +22,13 @@ import ( "context" "testing" "time" + + "github.com/caas-team/sparrow/test" ) func TestTargetManagerConfig_Validate(t *testing.T) { + test.MarkAsShort(t) + tests := []struct { name string cfg TargetManagerConfig diff --git a/test/e2e/main_test.go b/test/e2e/main_test.go index 412849d3..f8f02ec4 100644 --- a/test/e2e/main_test.go +++ b/test/e2e/main_test.go @@ -9,6 +9,8 @@ import ( "github.com/caas-team/sparrow/pkg/checks" "github.com/caas-team/sparrow/pkg/config" "github.com/caas-team/sparrow/test" + "github.com/caas-team/sparrow/test/framework" + "github.com/caas-team/sparrow/test/framework/builder" ) const ( @@ -17,16 +19,18 @@ const ( ) func TestE2E_Sparrow_WithChecks_ConfigureOnce(t *testing.T) { - framework := test.NewFramework(t) + test.MarkAsLong(t) + + fw := framework.New(t) tests := []struct { name string - startup test.ConfigBuilder - checks []test.CheckBuilder + startup builder.SparrowConfig + checks []builder.Check wantEndpoints map[string]int }{ { name: "no checks", - startup: *test.NewSparrowConfig(), + startup: *builder.NewSparrowConfig(), checks: nil, wantEndpoints: map[string]int{ "http://localhost:8080/v1/metrics/health": http.StatusNotFound, @@ -37,9 +41,9 @@ func TestE2E_Sparrow_WithChecks_ConfigureOnce(t *testing.T) { }, { name: "with health check", - startup: *test.NewSparrowConfig(), - checks: []test.CheckBuilder{ - test.NewHealthCheck(). + startup: *builder.NewSparrowConfig(), + checks: []builder.Check{ + builder.NewHealthCheck(). WithInterval(checkInterval). WithTimeout(checkTimeout). WithTargets("https://www.example.com/", "https://www.google.com/"), @@ -53,17 +57,17 @@ func TestE2E_Sparrow_WithChecks_ConfigureOnce(t *testing.T) { }, { name: "with health, latency and dns checks", - startup: *test.NewSparrowConfig(), - checks: []test.CheckBuilder{ - test.NewHealthCheck(). + startup: *builder.NewSparrowConfig(), + checks: []builder.Check{ + builder.NewHealthCheck(). WithInterval(checkInterval). WithTimeout(checkTimeout). WithTargets("https://www.example.com/"), - test.NewLatencyCheck(). + builder.NewLatencyCheck(). WithInterval(checkInterval). WithTimeout(checkTimeout). WithTargets("https://www.example.com/"), - test.NewDNSCheck(). + builder.NewDNSCheck(). WithInterval(checkInterval). WithTimeout(checkTimeout). WithTargets("www.example.com"), @@ -79,7 +83,7 @@ func TestE2E_Sparrow_WithChecks_ConfigureOnce(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - e2e := framework.E2E(t, tt.startup.Config(t)).WithChecks(tt.checks...) + e2e := fw.E2E(t, tt.startup.Config(t)).WithChecks(tt.checks...) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) defer cancel() @@ -107,25 +111,26 @@ type result struct { } func TestE2E_Sparrow_WithChecks_Reconfigure(t *testing.T) { - framework := test.NewFramework(t) + test.MarkAsLong(t) + fw := framework.New(t) tests := []struct { name string - startup test.ConfigBuilder - initialChecks []test.CheckBuilder + startup builder.SparrowConfig + initialChecks []builder.Check wantInitial map[string]result - secondChecks []test.CheckBuilder + secondChecks []builder.Check wantSecond map[string]result }{ { name: "with health check then latency check", - startup: *test.NewSparrowConfig().WithLoader( - test.NewLoaderConfig(). + startup: *builder.NewSparrowConfig().WithLoader( + builder.NewLoaderConfig(). WithInterval(loaderInterval). Build(), ), - initialChecks: []test.CheckBuilder{ - test.NewHealthCheck(). + initialChecks: []builder.Check{ + builder.NewHealthCheck(). WithInterval(checkInterval). WithTimeout(checkTimeout). WithTargets("https://www.example.com/", "https://www.google.com/"), @@ -145,8 +150,8 @@ func TestE2E_Sparrow_WithChecks_Reconfigure(t *testing.T) { "http://localhost:8080/v1/metrics/dns": {status: http.StatusNotFound}, "http://localhost:8080/v1/metrics/traceroute": {status: http.StatusNotFound}, }, - secondChecks: []test.CheckBuilder{ - test.NewLatencyCheck(). + secondChecks: []builder.Check{ + builder.NewLatencyCheck(). WithInterval(checkInterval). WithTimeout(checkTimeout). WithTargets("https://www.example.com/"), @@ -181,13 +186,13 @@ func TestE2E_Sparrow_WithChecks_Reconfigure(t *testing.T) { }, { name: "with health check then dns check", - startup: *test.NewSparrowConfig().WithLoader( - test.NewLoaderConfig(). + startup: *builder.NewSparrowConfig().WithLoader( + builder.NewLoaderConfig(). WithInterval(loaderInterval). Build(), ), - initialChecks: []test.CheckBuilder{ - test.NewHealthCheck(). + initialChecks: []builder.Check{ + builder.NewHealthCheck(). WithInterval(checkInterval). WithTimeout(checkTimeout). WithTargets("https://www.example.com/"), @@ -206,8 +211,8 @@ func TestE2E_Sparrow_WithChecks_Reconfigure(t *testing.T) { "http://localhost:8080/v1/metrics/dns": {status: http.StatusNotFound}, "http://localhost:8080/v1/metrics/traceroute": {status: http.StatusNotFound}, }, - secondChecks: []test.CheckBuilder{ - test.NewDNSCheck(). + secondChecks: []builder.Check{ + builder.NewDNSCheck(). WithInterval(checkInterval). WithTimeout(checkTimeout). WithTargets("www.example.com"), @@ -241,13 +246,13 @@ func TestE2E_Sparrow_WithChecks_Reconfigure(t *testing.T) { }, { name: "with health check then updated health check", - startup: *test.NewSparrowConfig().WithLoader( - test.NewLoaderConfig(). + startup: *builder.NewSparrowConfig().WithLoader( + builder.NewLoaderConfig(). WithInterval(loaderInterval). Build(), ), - initialChecks: []test.CheckBuilder{ - test.NewHealthCheck(). + initialChecks: []builder.Check{ + builder.NewHealthCheck(). WithInterval(checkInterval). WithTimeout(checkTimeout). WithTargets("https://www.example.com/"), @@ -266,8 +271,8 @@ func TestE2E_Sparrow_WithChecks_Reconfigure(t *testing.T) { "http://localhost:8080/v1/metrics/dns": {status: http.StatusNotFound}, "http://localhost:8080/v1/metrics/traceroute": {status: http.StatusNotFound}, }, - secondChecks: []test.CheckBuilder{ - test.NewHealthCheck(). + secondChecks: []builder.Check{ + builder.NewHealthCheck(). WithInterval(checkInterval). WithTimeout(checkTimeout). WithTargets("https://www.google.com/"), @@ -289,13 +294,13 @@ func TestE2E_Sparrow_WithChecks_Reconfigure(t *testing.T) { }, { name: "with health check then no checks", - startup: *test.NewSparrowConfig().WithLoader( - test.NewLoaderConfig(). + startup: *builder.NewSparrowConfig().WithLoader( + builder.NewLoaderConfig(). WithInterval(loaderInterval). Build(), ), - initialChecks: []test.CheckBuilder{ - test.NewHealthCheck(). + initialChecks: []builder.Check{ + builder.NewHealthCheck(). WithInterval(checkInterval). WithTimeout(checkTimeout). WithTargets("https://www.example.com/"), @@ -334,7 +339,7 @@ func TestE2E_Sparrow_WithChecks_Reconfigure(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - e2e := framework.E2E(t, tt.startup.Config(t)).WithChecks(tt.initialChecks...) + e2e := fw.E2E(t, tt.startup.Config(t)).WithChecks(tt.initialChecks...) ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() @@ -366,32 +371,35 @@ func TestE2E_Sparrow_WithChecks_Reconfigure(t *testing.T) { } func TestE2E_Sparrow_WithRemoteConfig(t *testing.T) { - framework := test.NewFramework(t) + test.MarkAsLong(t) + + fw := framework.New(t) tests := []struct { name string - startup test.ConfigBuilder - initialChecks []test.CheckBuilder + startup builder.SparrowConfig + initialChecks []builder.Check wantInitial map[string]result - secondChecks []test.CheckBuilder + secondChecks []builder.Check wantSecond map[string]result }{ { name: "with health check in remote config", - startup: *test.NewSparrowConfig(). + startup: *builder.NewSparrowConfig(). + WithAPI(builder.NewAPIConfig("localhost:8081")). WithLoader( - test.NewLoaderConfig(). + builder.NewLoaderConfig(). WithInterval(loaderInterval). FromHTTP(config.HttpLoaderConfig{Url: "http://localhost:50505/", Timeout: 5 * time.Second}). Build(), ), - initialChecks: []test.CheckBuilder{ - test.NewHealthCheck(). + initialChecks: []builder.Check{ + builder.NewHealthCheck(). WithInterval(checkInterval). WithTimeout(checkTimeout). WithTargets("https://www.example.com/"), }, wantInitial: map[string]result{ - "http://localhost:8080/v1/metrics/health": { + "http://localhost:8081/v1/metrics/health": { status: http.StatusOK, response: checks.Result{ Data: map[string]any{ @@ -400,28 +408,29 @@ func TestE2E_Sparrow_WithRemoteConfig(t *testing.T) { Timestamp: time.Now(), }, }, - "http://localhost:8080/v1/metrics/latency": {status: http.StatusNotFound}, - "http://localhost:8080/v1/metrics/dns": {status: http.StatusNotFound}, - "http://localhost:8080/v1/metrics/traceroute": {status: http.StatusNotFound}, + "http://localhost:8081/v1/metrics/latency": {status: http.StatusNotFound}, + "http://localhost:8081/v1/metrics/dns": {status: http.StatusNotFound}, + "http://localhost:8081/v1/metrics/traceroute": {status: http.StatusNotFound}, }, }, { name: "with health check in remote config then dns check", - startup: *test.NewSparrowConfig(). + startup: *builder.NewSparrowConfig(). + WithAPI(builder.NewAPIConfig("localhost:8081")). WithLoader( - test.NewLoaderConfig(). + builder.NewLoaderConfig(). WithInterval(loaderInterval). FromHTTP(config.HttpLoaderConfig{Url: "http://localhost:50505/", Timeout: 5 * time.Second}). Build(), ), - initialChecks: []test.CheckBuilder{ - test.NewHealthCheck(). + initialChecks: []builder.Check{ + builder.NewHealthCheck(). WithInterval(checkInterval). WithTimeout(checkTimeout). WithTargets("https://www.example.com/"), }, wantInitial: map[string]result{ - "http://localhost:8080/v1/metrics/health": { + "http://localhost:8081/v1/metrics/health": { status: http.StatusOK, response: checks.Result{ Data: map[string]any{ @@ -430,18 +439,18 @@ func TestE2E_Sparrow_WithRemoteConfig(t *testing.T) { Timestamp: time.Now(), }, }, - "http://localhost:8080/v1/metrics/latency": {status: http.StatusNotFound}, - "http://localhost:8080/v1/metrics/dns": {status: http.StatusNotFound}, - "http://localhost:8080/v1/metrics/traceroute": {status: http.StatusNotFound}, + "http://localhost:8081/v1/metrics/latency": {status: http.StatusNotFound}, + "http://localhost:8081/v1/metrics/dns": {status: http.StatusNotFound}, + "http://localhost:8081/v1/metrics/traceroute": {status: http.StatusNotFound}, }, - secondChecks: []test.CheckBuilder{ - test.NewDNSCheck(). + secondChecks: []builder.Check{ + builder.NewDNSCheck(). WithInterval(checkInterval). WithTimeout(checkTimeout). WithTargets("www.example.com"), }, wantSecond: map[string]result{ //nolint:dupl // This is a test - "http://localhost:8080/v1/metrics/health": { + "http://localhost:8081/v1/metrics/health": { status: http.StatusOK, response: checks.Result{ Data: map[string]any{ @@ -450,8 +459,8 @@ func TestE2E_Sparrow_WithRemoteConfig(t *testing.T) { Timestamp: time.Now(), }, }, - "http://localhost:8080/v1/metrics/latency": {status: http.StatusNotFound}, - "http://localhost:8080/v1/metrics/dns": { + "http://localhost:8081/v1/metrics/latency": {status: http.StatusNotFound}, + "http://localhost:8081/v1/metrics/dns": { status: http.StatusOK, response: checks.Result{ Data: map[string]any{ @@ -464,14 +473,14 @@ func TestE2E_Sparrow_WithRemoteConfig(t *testing.T) { Timestamp: time.Now(), }, }, - "http://localhost:8080/v1/metrics/traceroute": {status: http.StatusNotFound}, + "http://localhost:8081/v1/metrics/traceroute": {status: http.StatusNotFound}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - e2e := framework.E2E(t, tt.startup.Config(t)). + e2e := fw.E2E(t, tt.startup.Config(t)). WithChecks(tt.initialChecks...). WithRemote() @@ -482,7 +491,7 @@ func TestE2E_Sparrow_WithRemoteConfig(t *testing.T) { go func() { finish <- e2e.Run(ctx) }() - e2e.AwaitStartup("http://localhost:8080", checkTimeout).AwaitChecks() + e2e.AwaitStartup("http://localhost:8081", checkTimeout).AwaitChecks() for url, result := range tt.wantInitial { e2e.HttpAssertion(url). @@ -505,5 +514,3 @@ func TestE2E_Sparrow_WithRemoteConfig(t *testing.T) { }) } } - -func TestE2E_Sparrow_WithTargetManager(t *testing.T) {} diff --git a/test/flags.go b/test/flags.go new file mode 100644 index 00000000..0c1cc1a0 --- /dev/null +++ b/test/flags.go @@ -0,0 +1,19 @@ +package test + +import "testing" + +// MarkAsShort marks the test as short, so it will be skipped if the -test.short flag is not provided. +func MarkAsShort(t *testing.T) { + t.Helper() + if !testing.Short() { + t.Skip("skipping short tests, to run them use the -test.short flag") + } +} + +// MarkAsLong marks the test as long, so it will be skipped if the -test.short flag is provided. +func MarkAsLong(t *testing.T) { + t.Helper() + if testing.Short() { + t.Skip("skipping long tests, to run them remove the -test.short flag") + } +} diff --git a/test/checks.go b/test/framework/builder/checks.go similarity index 97% rename from test/checks.go rename to test/framework/builder/checks.go index 3bc44071..3e3dfe50 100644 --- a/test/checks.go +++ b/test/framework/builder/checks.go @@ -1,4 +1,4 @@ -package test +package builder import ( "testing" @@ -13,7 +13,7 @@ import ( "github.com/goccy/go-yaml" ) -type CheckBuilder interface { +type Check interface { // For returns the name of the check. For() string // Check returns the check. @@ -50,7 +50,7 @@ func newCheckAsYAML(t *testing.T, cfg checkConfig) []byte { return out } -var _ CheckBuilder = (*healthCheckBuilder)(nil) +var _ Check = (*healthCheckBuilder)(nil) type healthCheckBuilder struct{ cfg health.Config } @@ -105,7 +105,7 @@ func (b *healthCheckBuilder) For() string { return b.cfg.For() } -var _ CheckBuilder = (*latencyConfigBuilder)(nil) +var _ Check = (*latencyConfigBuilder)(nil) type latencyConfigBuilder struct{ cfg latency.Config } @@ -160,7 +160,7 @@ func (b *latencyConfigBuilder) ExpectedWaitTime() time.Duration { return b.cfg.Interval + b.cfg.Timeout + time.Duration(b.cfg.Retry.Count)*b.cfg.Retry.Delay } -var _ CheckBuilder = (*dnsConfigBuilder)(nil) +var _ Check = (*dnsConfigBuilder)(nil) type dnsConfigBuilder struct{ cfg dns.Config } @@ -215,7 +215,7 @@ func (b *dnsConfigBuilder) For() string { return b.cfg.For() } -var _ CheckBuilder = (*tracerouteConfigBuilder)(nil) +var _ Check = (*tracerouteConfigBuilder)(nil) type tracerouteConfigBuilder struct{ cfg traceroute.Config } diff --git a/test/startup.go b/test/framework/builder/startup.go similarity index 82% rename from test/startup.go rename to test/framework/builder/startup.go index c7712402..ffdbc2d1 100644 --- a/test/startup.go +++ b/test/framework/builder/startup.go @@ -1,4 +1,4 @@ -package test +package builder import ( "context" @@ -18,10 +18,10 @@ import ( "github.com/goccy/go-yaml" ) -type ConfigBuilder struct{ cfg config.Config } +type SparrowConfig struct{ cfg config.Config } -func NewSparrowConfig() *ConfigBuilder { - return &ConfigBuilder{ +func NewSparrowConfig() *SparrowConfig { + return &SparrowConfig{ cfg: config.Config{ SparrowName: "sparrow.telekom.com", Loader: NewLoaderConfig().Build(), @@ -30,32 +30,32 @@ func NewSparrowConfig() *ConfigBuilder { } } -func (b *ConfigBuilder) WithName(n string) *ConfigBuilder { +func (b *SparrowConfig) WithName(n string) *SparrowConfig { b.cfg.SparrowName = n return b } -func (b *ConfigBuilder) WithLoader(cfg config.LoaderConfig) *ConfigBuilder { //nolint:gocritic // Performance is not a concern here +func (b *SparrowConfig) WithLoader(cfg config.LoaderConfig) *SparrowConfig { //nolint:gocritic // Performance is not a concern here b.cfg.Loader = cfg return b } -func (b *ConfigBuilder) WithAPI(cfg api.Config) *ConfigBuilder { +func (b *SparrowConfig) WithAPI(cfg api.Config) *SparrowConfig { b.cfg.Api = cfg return b } -func (b *ConfigBuilder) WithTargetManager(cfg targets.TargetManagerConfig) *ConfigBuilder { //nolint:gocritic // Performance is not a concern here +func (b *SparrowConfig) WithTargetManager(cfg targets.TargetManagerConfig) *SparrowConfig { //nolint:gocritic // Performance is not a concern here b.cfg.TargetManager = cfg return b } -func (b *ConfigBuilder) WithTelemetry(cfg metrics.Config) *ConfigBuilder { //nolint:gocritic // Performance is not a concern here +func (b *SparrowConfig) WithTelemetry(cfg metrics.Config) *SparrowConfig { //nolint:gocritic // Performance is not a concern here b.cfg.Telemetry = cfg return b } -func (b *ConfigBuilder) Config(t *testing.T) *config.Config { +func (b *SparrowConfig) Config(t *testing.T) *config.Config { t.Helper() if err := b.cfg.Validate(context.Background()); err != nil { t.Fatalf("config is not valid: %v", err) @@ -63,7 +63,7 @@ func (b *ConfigBuilder) Config(t *testing.T) *config.Config { return &b.cfg } -func (b *ConfigBuilder) YAML(t *testing.T) []byte { +func (b *SparrowConfig) YAML(t *testing.T) []byte { t.Helper() out, err := yaml.Marshal(b.cfg) if err != nil { diff --git a/test/e2e.go b/test/framework/e2e.go similarity index 89% rename from test/e2e.go rename to test/framework/e2e.go index 799e31ce..d3580c95 100644 --- a/test/e2e.go +++ b/test/framework/e2e.go @@ -1,4 +1,4 @@ -package test +package framework import ( "bytes" @@ -20,6 +20,7 @@ import ( "github.com/caas-team/sparrow/pkg/checks" "github.com/caas-team/sparrow/pkg/config" "github.com/caas-team/sparrow/pkg/sparrow" + "github.com/caas-team/sparrow/test/framework/builder" "github.com/getkin/kin-openapi/openapi3" "github.com/getkin/kin-openapi/routers" "github.com/getkin/kin-openapi/routers/gorillamux" @@ -33,7 +34,7 @@ type E2E struct { config config.Config server *http.Server sparrow *sparrow.Sparrow - checks map[string]CheckBuilder + checks map[string]builder.Check buf bytes.Buffer path string mu sync.Mutex @@ -47,7 +48,7 @@ func (t *E2E) WithConfigFile(path string) *E2E { } // WithChecks sets the checks in the test. -func (t *E2E) WithChecks(builders ...CheckBuilder) *E2E { +func (t *E2E) WithChecks(builders ...builder.Check) *E2E { for _, b := range builders { t.checks[b.For()] = b t.buf.Write(b.YAML(t.t)) @@ -66,17 +67,20 @@ func (t *E2E) WithRemote() *E2E { } // UpdateChecks updates the checks of the test. -func (t *E2E) UpdateChecks(builders ...CheckBuilder) *E2E { - t.checks = map[string]CheckBuilder{} +func (t *E2E) UpdateChecks(builders ...builder.Check) *E2E { + t.checks = map[string]builder.Check{} t.buf.Reset() for _, b := range builders { t.checks[b.For()] = b t.buf.Write(b.YAML(t.t)) } - err := t.writeCheckConfig() - if err != nil { - t.t.Fatalf("Failed to write check config: %v", err) + // If the test is running with a remote server, we don't need to write the check config into a file. + if t.server == nil { + err := t.writeCheckConfig() + if err != nil { + t.t.Fatalf("Failed to write check config: %v", err) + } } return t @@ -93,11 +97,6 @@ func (t *E2E) Run(ctx context.Context) error { t.path = "testdata/checks.yaml" } - err := t.writeCheckConfig() - if err != nil { - t.t.Fatalf("Failed to write check config: %v", err) - } - if t.server != nil { go func() { err := t.server.ListenAndServe() @@ -111,6 +110,11 @@ func (t *E2E) Run(ctx context.Context) error { t.t.Fatalf("Failed to shutdown server: %v", err) } }() + } else { + err := t.writeCheckConfig() + if err != nil { + t.t.Fatalf("Failed to write check config: %v", err) + } } t.mu.Lock() @@ -252,19 +256,16 @@ func (a *e2eHttpAsserter) Assert(status int) { req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, a.url, http.NoBody) if err != nil { a.e2e.t.Fatalf("Failed to create request: %v", err) - return } resp, err := http.DefaultClient.Do(req) if err != nil { a.e2e.t.Errorf("Failed to get %s: %v", a.url, err) - return } defer resp.Body.Close() if resp.StatusCode != status { a.e2e.t.Errorf("Want status code %d for %s, got %d", status, a.url, resp.StatusCode) - return } a.e2e.t.Logf("Got status code %d for %s", resp.StatusCode, a.url) @@ -272,12 +273,11 @@ func (a *e2eHttpAsserter) Assert(status int) { if a.schema != nil && a.router != nil { if err = a.assertSchema(req, resp); err != nil { a.e2e.t.Errorf("Response from %q does not match schema: %v", a.url, err) - return } } if a.response != nil { - err := a.response.asserter(resp) + err = a.response.asserter(resp) if err != nil { a.e2e.t.Errorf("Failed to assert response: %v", err) } @@ -397,18 +397,21 @@ func (a *e2eHttpAsserter) assertCheckResponse(resp *http.Response) error { a.e2e.t.Fatalf("Invalid response type: %T", a.response.want) } - var res checks.Result - if err := json.NewDecoder(resp.Body).Decode(&res); err != nil { + var got checks.Result + if err := json.NewDecoder(resp.Body).Decode(&got); err != nil { a.e2e.t.Errorf("Failed to decode response body: %v", err) } wantData := want.Data.(map[string]any) - gotData := res.Data.(map[string]any) + gotData, ok := got.Data.(map[string]any) + if !ok { + a.e2e.t.Errorf("Result.Data = %T (%v), want %T (%v)", got.Data, got.Data, want.Data, want.Data) + } assertMapEqual(a.e2e.t, wantData, gotData) const deltaTimeThreshold = 5 * time.Minute - if time.Since(res.Timestamp) > deltaTimeThreshold { - a.e2e.t.Errorf("Response timestamp is not recent: %v", res.Timestamp) + if time.Since(got.Timestamp) > deltaTimeThreshold { + a.e2e.t.Errorf("Response timestamp is not recent: %v", got.Timestamp) } return nil @@ -425,7 +428,7 @@ func assertMapEqual(t *testing.T, want, got map[string]any) { for k, w := range want { g, ok := got[k] if !ok { - t.Errorf("Missing key %q", k) + t.Errorf("got[%q] not found (%v), want %v", k, got, w) } if err := assertValueEqual(t, w, g); err != nil { @@ -440,11 +443,11 @@ func assertMapEqual(t *testing.T, want, got map[string]any) { func assertValueEqual(t *testing.T, want, got any) error { switch w := want.(type) { case map[string]any: - gm, ok := got.(map[string]any) + gotMap, ok := got.(map[string]any) if !ok { return fmt.Errorf("%v (%T), want %v (%T)", got, got, w, w) } - assertMapEqual(t, w, gm) + assertMapEqual(t, w, gotMap) return nil case time.Time, float32, float64: // Timestamps and floating-point numbers are time-sensitive and are never equal. @@ -461,13 +464,13 @@ func assertValueEqual(t *testing.T, want, got any) error { if !ok { return fmt.Errorf("%v (%T), want %v (%T)", got, got, w, w) } - gss := make([]string, len(gs)) + gotSlice := make([]string, len(gs)) for i, g := range gs { - gss[i] = g.(string) + gotSlice[i] = g.(string) } - for _, ipStr := range w { - wIP := net.ParseIP(ipStr) - if wIP == nil { + for _, wantIPStr := range w { + wantIP := net.ParseIP(wantIPStr) + if wantIP == nil { // This is a special case for string slices that might contain IP addresses. // If the `want` value is not a valid IP address, we skip the IP validation // and proceed to the default case for a generic equality check. @@ -479,10 +482,10 @@ func assertValueEqual(t *testing.T, want, got any) error { goto defaultCase } - for _, gipStr := range gss { - gIP := net.ParseIP(gipStr) - if gIP == nil { - return fmt.Errorf("%q, want an IP address (%s)", gipStr, wIP) + for _, gotIPStr := range gotSlice { + gotIP := net.ParseIP(gotIPStr) + if gotIP == nil { + return fmt.Errorf("%q, want an IP address (%s)", gotIPStr, wantIP) } } } diff --git a/test/framework.go b/test/framework/framework.go similarity index 58% rename from test/framework.go rename to test/framework/framework.go index 2de6982f..4ee1d5f8 100644 --- a/test/framework.go +++ b/test/framework/framework.go @@ -1,4 +1,4 @@ -package test +package framework import ( "context" @@ -6,6 +6,7 @@ import ( "github.com/caas-team/sparrow/pkg/config" "github.com/caas-team/sparrow/pkg/sparrow" + "github.com/caas-team/sparrow/test/framework/builder" ) // Runner is a test runner. @@ -21,38 +22,22 @@ type Framework struct { } // NewFramework creates a new test framework. -func NewFramework(t *testing.T) *Framework { +func New(t *testing.T) *Framework { t.Helper() return &Framework{t: t} } -// Unit creates a new unit test. -// If the test is not run in short mode, it will be skipped. -func (f *Framework) Unit(t *testing.T, run func(context.Context) error) *Unit { - if !testing.Short() { - f.t.Skip("skipping unit tests") - return nil - } - - return &Unit{t: t, run: run} -} - // E2E creates a new end-to-end test. // If the test is run in short mode, it will be skipped. func (f *Framework) E2E(t *testing.T, cfg *config.Config) *E2E { - if testing.Short() { - f.t.Skip("skipping e2e tests") - return nil - } - if cfg == nil { - cfg = NewSparrowConfig().Config(f.t) + cfg = builder.NewSparrowConfig().Config(f.t) } return &E2E{ t: t, config: *cfg, sparrow: sparrow.New(cfg), - checks: map[string]CheckBuilder{}, + checks: map[string]builder.Check{}, } } diff --git a/test/unit.go b/test/unit.go deleted file mode 100644 index 14c2c43b..00000000 --- a/test/unit.go +++ /dev/null @@ -1,19 +0,0 @@ -package test - -import ( - "context" - "testing" -) - -var _ Runner = (*Unit)(nil) - -// Unit is a unit test. -type Unit struct { - t *testing.T - run func(context.Context) error -} - -// Run runs the test. -func (t *Unit) Run(ctx context.Context) error { - return t.run(ctx) -} From eea9e17392ce1e56090b669f2b160a03cc5c0f05 Mon Sep 17 00:00:00 2001 From: lvlcn-t <75443136+lvlcn-t@users.noreply.github.com> Date: Sun, 17 Nov 2024 17:56:12 +0100 Subject: [PATCH 14/17] fix: metrics endpoints content type setting * fix: metrics endpoints content type setting (was ineffective thus always returning text/plain) Signed-off-by: lvlcn-t <75443136+lvlcn-t@users.noreply.github.com> --- .vscode/settings.json | 9 +++++++-- pkg/sparrow/handlers.go | 2 +- pkg/sparrow/handlers_test.go | 23 +++++++++++++++-------- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index cefc2e80..9234684f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,6 +4,11 @@ "-cover", "-count=1", "-timeout=240s", - "-v" - ] + // "-test.short", + "-v", + ], + "go.testEnvVars": { + // "LOG_LEVEL": "debug", + "LOG_FORMAT": "text", + } } \ No newline at end of file diff --git a/pkg/sparrow/handlers.go b/pkg/sparrow/handlers.go index 9a412b33..4c8a0c6d 100644 --- a/pkg/sparrow/handlers.go +++ b/pkg/sparrow/handlers.go @@ -122,6 +122,7 @@ func (s *Sparrow) handleCheckMetrics(w http.ResponseWriter, r *http.Request) { return } + w.Header().Add("Content-Type", "application/json") enc := json.NewEncoder(w) enc.SetIndent("", " ") @@ -134,5 +135,4 @@ func (s *Sparrow) handleCheckMetrics(w http.ResponseWriter, r *http.Request) { } return } - w.Header().Add("Content-Type", "application/json") } diff --git a/pkg/sparrow/handlers_test.go b/pkg/sparrow/handlers_test.go index 1a34fa12..b4ebaf12 100644 --- a/pkg/sparrow/handlers_test.go +++ b/pkg/sparrow/handlers_test.go @@ -132,12 +132,18 @@ func TestSparrow_handleCheckMetrics(t *testing.T) { if tt.wantCode == http.StatusBadRequest { r = chiRequest(httptest.NewRequest(http.MethodGet, "/v1/metrics/", bytes.NewBuffer([]byte{})), "") } + r.Header.Add("Accept", "application/json") s.handleCheckMetrics(w, r) - resp := w.Result() //nolint:bodyclose + resp := w.Result() + defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) if tt.wantCode == http.StatusOK { + if w.Header().Get("Content-Type") != "application/json" { + t.Errorf("Sparrow.getCheckMetrics() = %v, want %v", w.Header().Get("Content-Type"), "application/json") + } + if tt.wantCode != resp.StatusCode { t.Errorf("Sparrow.getCheckMetrics() = %v, want %v", resp.StatusCode, tt.wantCode) } @@ -155,13 +161,14 @@ func TestSparrow_handleCheckMetrics(t *testing.T) { if reflect.DeepEqual(got, want) { t.Errorf("Sparrow.getCheckMetrics() = %v, want %v", got, want) } - } else { - if tt.wantCode != resp.StatusCode { - t.Errorf("Sparrow.getCheckMetrics() = %v, want %v", resp.StatusCode, tt.wantCode) - } - if !reflect.DeepEqual(body, tt.want) { - t.Errorf("Sparrow.getCheckMetrics() = %v, want %v", body, tt.want) - } + return + } + + if tt.wantCode != resp.StatusCode { + t.Errorf("Sparrow.getCheckMetrics() = %v, want %v", resp.StatusCode, tt.wantCode) + } + if !reflect.DeepEqual(body, tt.want) { + t.Errorf("Sparrow.getCheckMetrics() = %v, want %v", body, tt.want) } }) } From 88c63e3d21060ba7be5be2afbad0f4def361df28 Mon Sep 17 00:00:00 2001 From: lvlcn-t <75443136+lvlcn-t@users.noreply.github.com> Date: Sun, 17 Nov 2024 19:24:40 +0100 Subject: [PATCH 15/17] refactor: simplify E2E test struct Signed-off-by: lvlcn-t <75443136+lvlcn-t@users.noreply.github.com> --- test/framework/e2e.go | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/test/framework/e2e.go b/test/framework/e2e.go index d3580c95..58e771bb 100644 --- a/test/framework/e2e.go +++ b/test/framework/e2e.go @@ -30,23 +30,16 @@ var _ Runner = (*E2E)(nil) // E2E is an end-to-end test. type E2E struct { - t *testing.T config config.Config - server *http.Server + buf bytes.Buffer sparrow *sparrow.Sparrow + t *testing.T checks map[string]builder.Check - buf bytes.Buffer - path string + server *http.Server mu sync.Mutex running bool } -// WithConfigFile sets the path to the config file. -func (t *E2E) WithConfigFile(path string) *E2E { - t.path = path - return t -} - // WithChecks sets the checks in the test. func (t *E2E) WithChecks(builders ...builder.Check) *E2E { for _, b := range builders { @@ -93,10 +86,6 @@ func (t *E2E) Run(ctx context.Context) error { t.t.Fatal("E2E.Run must be called once") } - if t.path == "" { - t.path = "testdata/checks.yaml" - } - if t.server != nil { go func() { err := t.server.ListenAndServe() @@ -195,14 +184,15 @@ func (t *E2E) AwaitChecks() *E2E { // writeCheckConfig writes the check config to a file at the provided path. func (t *E2E) writeCheckConfig() error { const fileMode = 0o755 - err := os.MkdirAll(filepath.Dir(t.path), fileMode) + path := "testdata/checks.yaml" + err := os.MkdirAll(filepath.Dir(path), fileMode) if err != nil { - return fmt.Errorf("failed to create %q: %w", filepath.Dir(t.path), err) + return fmt.Errorf("failed to create %q: %w", filepath.Dir(path), err) } - err = os.WriteFile(t.path, t.buf.Bytes(), fileMode) + err = os.WriteFile(path, t.buf.Bytes(), fileMode) if err != nil { - return fmt.Errorf("failed to write %q: %w", t.path, err) + return fmt.Errorf("failed to write %q: %w", path, err) } return nil } @@ -233,6 +223,7 @@ type e2eHttpAsserter struct { router routers.Router } +// e2eResponseAsserter is a response asserter for end-to-end tests. type e2eResponseAsserter struct { want any asserter func(r *http.Response) error @@ -376,7 +367,7 @@ func (a *e2eHttpAsserter) assertSchema(req *http.Request, resp *http.Response) e return errors.New("no media type defined in OpenAPI schema for Content-Type 'application/json'") } - var body any + var body map[string]any if err = json.Unmarshal(data, &body); err != nil { return fmt.Errorf("failed to unmarshal response body: %w", err) } From cb31835b4dd0925440d9e74247ae460c654e9612 Mon Sep 17 00:00:00 2001 From: lvlcn-t <75443136+lvlcn-t@users.noreply.github.com> Date: Sun, 17 Nov 2024 19:47:48 +0100 Subject: [PATCH 16/17] ci: add ability to provide logging options to tests Signed-off-by: lvlcn-t <75443136+lvlcn-t@users.noreply.github.com> --- .github/workflows/test.yml | 57 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5c8f5a9e..c55b769d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,19 @@ name: Tests -on: [push] +on: + push: + workflow_dispatch: + inputs: + log_format: + description: "Log format" + required: false + default: "text" + type: string + log_level: + description: "Log level" + required: false + default: "info" + type: string permissions: contents: read @@ -20,7 +33,24 @@ jobs: go install github.com/mfridman/tparse@latest go mod download - - name: Run all go tests + - name: Set log format and level + id: inputs + run: | + if [ -z "${{ inputs.log_format }}" ]; then + echo "LOG_FORMAT=text" >> $GITHUB_OUTPUT + else + echo "LOG_FORMAT=${{ inputs.log_format }}" >> $GITHUB_OUTPUT + fi + if [ -z "${{ inputs.log_level }}" ]; then + echo "LOG_LEVEL=info" >> $GITHUB_OUTPUT + else + echo "LOG_LEVEL=${{ inputs.log_level }}" >> $GITHUB_OUTPUT + fi + + - name: Run go unit tests + env: + LOG_FORMAT: ${{ steps.inputs.outputs.LOG_FORMAT }} + LOG_LEVEL: ${{ steps.inputs.outputs.LOG_LEVEL }} run: | go test -v -count=1 -test.short -race ./... -json -coverpkg ./... \ | tee output.jsonl | tparse -notests -follow -all || true @@ -40,7 +70,28 @@ jobs: go install github.com/mfridman/tparse@latest go mod download - - name: Run all go tests + - name: Set log format and level + id: inputs + run: | + if [ -z "${{ inputs.log_format }}" ]; then + echo "No log format provided, using default: text" + echo "LOG_FORMAT=text" >> $GITHUB_OUTPUT + else + echo "Log format provided: ${{ inputs.log_format }}" + echo "LOG_FORMAT=${{ inputs.log_format }}" >> $GITHUB_OUTPUT + fi + if [ -z "${{ inputs.log_level }}" ]; then + echo "No log level provided, using default: info" + echo "LOG_LEVEL=info" >> $GITHUB_OUTPUT + else + echo "Log level provided: ${{ inputs.log_level }}" + echo "LOG_LEVEL=${{ inputs.log_level }}" >> $GITHUB_OUTPUT + fi + + - name: Run go e2e tests + env: + LOG_FORMAT: ${{ steps.inputs.outputs.LOG_FORMAT }} + LOG_LEVEL: ${{ steps.inputs.outputs.LOG_LEVEL }} run: | go test -v -count=1 -race ./... -json -coverpkg ./... \ | tee output.jsonl | tparse -notests -follow -all || true From fc2aa54f6826f8816083c5e1b2e92d5c7d592e1b Mon Sep 17 00:00:00 2001 From: lvlcn-t <75443136+lvlcn-t@users.noreply.github.com> Date: Sun, 17 Nov 2024 21:50:03 +0100 Subject: [PATCH 17/17] refactor: remove unused Update channel from checks and simplify related logic Signed-off-by: lvlcn-t <75443136+lvlcn-t@users.noreply.github.com> --- pkg/checks/base.go | 3 --- pkg/checks/base_test.go | 21 ++++++++------------- pkg/checks/dns/dns.go | 8 +------- pkg/checks/health/health.go | 8 +------- pkg/checks/latency/latency.go | 8 +------- pkg/checks/traceroute/check.go | 10 ++-------- 6 files changed, 13 insertions(+), 45 deletions(-) diff --git a/pkg/checks/base.go b/pkg/checks/base.go index 8f98da6a..bde19e55 100644 --- a/pkg/checks/base.go +++ b/pkg/checks/base.go @@ -69,8 +69,6 @@ type Base struct { Mutex sync.Mutex // Done channel is used to notify about shutdown of a check. Done chan struct{} - // Update is a channel used to notify about configuration updates. - Update chan struct{} // closed is a flag indicating if the check has been shut down. closed bool } @@ -80,7 +78,6 @@ func NewBase() Base { return Base{ Mutex: sync.Mutex{}, Done: make(chan struct{}, 1), - Update: make(chan struct{}, 3), closed: false, } } diff --git a/pkg/checks/base_test.go b/pkg/checks/base_test.go index 47e10493..8721fa65 100644 --- a/pkg/checks/base_test.go +++ b/pkg/checks/base_test.go @@ -12,35 +12,30 @@ func TestBase_Shutdown(t *testing.T) { tests := []struct { name string - b *Base + base *Base }{ { name: "shutdown", - b: &Base{ - Done: make(chan struct{}, 1), - }, + base: &Base{Done: make(chan struct{}, 1)}, }, { name: "already shutdown", - b: &Base{ - Done: make(chan struct{}, 1), - closed: true, - }, + base: &Base{Done: make(chan struct{}, 1), closed: true}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if tt.b.closed { - close(tt.b.Done) + if tt.base.closed { + close(tt.base.Done) } - tt.b.Shutdown() + tt.base.Shutdown() - if !tt.b.closed { + if !tt.base.closed { t.Error("Base.Shutdown() should close Base.Done") } assert.Panics(t, func() { - tt.b.Done <- struct{}{} + tt.base.Done <- struct{}{} }, "Base.Done should be closed") }) } diff --git a/pkg/checks/dns/dns.go b/pkg/checks/dns/dns.go index 2728d1b0..00a2fe62 100644 --- a/pkg/checks/dns/dns.go +++ b/pkg/checks/dns/dns.go @@ -93,11 +93,6 @@ func (ch *check) Run(ctx context.Context, cResult chan checks.ResultDTO) error { return ctx.Err() case <-ch.Done: return nil - case <-ch.Update: - ch.Mutex.Lock() - timer.Reset(ch.config.Interval) - log.DebugContext(ctx, "Interval of dns check updated", "interval", ch.config.Interval.String()) - ch.Mutex.Unlock() case <-timer.C: res := ch.check(ctx) cResult <- checks.ResultDTO{ @@ -119,7 +114,7 @@ func (ch *check) UpdateConfig(cfg checks.Runtime) error { if c, ok := cfg.(*Config); ok { ch.Mutex.Lock() defer ch.Mutex.Unlock() - if reflect.DeepEqual(ch.config, *c) { + if c == nil || reflect.DeepEqual(&ch.config, c) { return nil } @@ -133,7 +128,6 @@ func (ch *check) UpdateConfig(cfg checks.Runtime) error { } ch.config = *c - ch.Update <- struct{}{} return nil } diff --git a/pkg/checks/health/health.go b/pkg/checks/health/health.go index 7689df87..5a31e1fb 100644 --- a/pkg/checks/health/health.go +++ b/pkg/checks/health/health.go @@ -80,11 +80,6 @@ func (ch *check) Run(ctx context.Context, cResult chan checks.ResultDTO) error { return ctx.Err() case <-ch.Done: return nil - case <-ch.Update: - ch.Mutex.Lock() - timer.Reset(ch.config.Interval) - log.DebugContext(ctx, "Interval of health check updated", "interval", ch.config.Interval.String()) - ch.Mutex.Unlock() case <-timer.C: res := ch.check(ctx) cResult <- checks.ResultDTO{ @@ -107,7 +102,7 @@ func (ch *check) UpdateConfig(cfg checks.Runtime) error { if c, ok := cfg.(*Config); ok { ch.Mutex.Lock() defer ch.Mutex.Unlock() - if reflect.DeepEqual(ch.config, *c) { + if c == nil || reflect.DeepEqual(&ch.config, c) { return nil } @@ -121,7 +116,6 @@ func (ch *check) UpdateConfig(cfg checks.Runtime) error { } ch.config = *c - ch.Update <- struct{}{} return nil } diff --git a/pkg/checks/latency/latency.go b/pkg/checks/latency/latency.go index f187eb1d..d78b318f 100644 --- a/pkg/checks/latency/latency.go +++ b/pkg/checks/latency/latency.go @@ -82,11 +82,6 @@ func (ch *check) Run(ctx context.Context, cResult chan checks.ResultDTO) error { return ctx.Err() case <-ch.Done: return nil - case <-ch.Update: - ch.Mutex.Lock() - timer.Reset(ch.config.Interval) - log.DebugContext(ctx, "Interval of latency check updated", "interval", ch.config.Interval.String()) - ch.Mutex.Unlock() case <-timer.C: res := ch.check(ctx) cResult <- checks.ResultDTO{ @@ -109,7 +104,7 @@ func (ch *check) UpdateConfig(cfg checks.Runtime) error { if c, ok := cfg.(*Config); ok { ch.Mutex.Lock() defer ch.Mutex.Unlock() - if reflect.DeepEqual(ch.config, *c) { + if c == nil || reflect.DeepEqual(&ch.config, c) { return nil } @@ -123,7 +118,6 @@ func (ch *check) UpdateConfig(cfg checks.Runtime) error { } ch.config = *c - ch.Update <- struct{}{} return nil } diff --git a/pkg/checks/traceroute/check.go b/pkg/checks/traceroute/check.go index 873e9e3e..2ad5daed 100644 --- a/pkg/checks/traceroute/check.go +++ b/pkg/checks/traceroute/check.go @@ -65,7 +65,7 @@ type check struct { func NewCheck() checks.Check { c := &check{ Base: checks.NewBase(), - config: Config{}, + config: Config{Retry: checks.DefaultRetry}, traceroute: TraceRoute, metrics: newMetrics(), } @@ -105,11 +105,6 @@ func (ch *check) Run(ctx context.Context, cResult chan checks.ResultDTO) error { return ctx.Err() case <-ch.Done: return nil - case <-ch.Update: - ch.Mutex.Lock() - timer.Reset(ch.config.Interval) - log.DebugContext(ctx, "Interval of traceroute check updated", "interval", ch.config.Interval.String()) - ch.Mutex.Unlock() case <-timer.C: res := ch.check(ctx) ch.metrics.MinHops(res) @@ -229,7 +224,7 @@ func (ch *check) UpdateConfig(cfg checks.Runtime) error { if c, ok := cfg.(*Config); ok { ch.Mutex.Lock() defer ch.Mutex.Unlock() - if reflect.DeepEqual(ch.config, *c) { + if c == nil || reflect.DeepEqual(&ch.config, c) { return nil } @@ -243,7 +238,6 @@ func (ch *check) UpdateConfig(cfg checks.Runtime) error { } ch.config = *c - ch.Update <- struct{}{} return nil }