From 066cc77cedbf94cdae04dfca61b33c210ac490ed Mon Sep 17 00:00:00 2001 From: Fredrik Berntsson Date: Fri, 16 Aug 2024 10:51:51 +0200 Subject: [PATCH] First e2e test implementation using go-testcontainers --- cmd/run.go | 11 +++-- e2e/container.go | 61 ++++++++++++++++++++++++++++ e2e/e2e_test.go | 47 +++++++++++++++++++++ go.mod | 12 ++++-- go.sum | 39 +++++++++--------- internal/config/config.go | 5 ++- internal/config/config_test.go | 3 +- internal/pihole/client.go | 23 +++++++---- internal/pihole/client_test.go | 57 ++++++++------------------ internal/pihole/model/pihole_test.go | 1 - internal/service/service.go | 36 ++++++++++------ internal/sync/sync.go | 17 ++++---- 12 files changed, 213 insertions(+), 99 deletions(-) create mode 100644 e2e/container.go create mode 100644 e2e/e2e_test.go diff --git a/cmd/run.go b/cmd/run.go index 85ffd36..25df42a 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -15,11 +15,14 @@ var runCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { readEnvFile() - conf := config.Config{} - conf.Load() + service, err := service.Init() + if err != nil { + log.Fatal().Err(err).Msg("failed to initialize service") + } - service := service.NewService(conf) - service.Run() + if err = service.Run(); err != nil { + log.Fatal().Err(err).Msg("service error") + } }, } diff --git a/e2e/container.go b/e2e/container.go new file mode 100644 index 0000000..ab13e18 --- /dev/null +++ b/e2e/container.go @@ -0,0 +1,61 @@ +package e2e + +import ( + "context" + "fmt" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" + "time" +) + +type PiHoleContainer struct { + Container testcontainers.Container + password string +} + +func (c *PiHoleContainer) ConnectionString() string { + mappedPort, err := c.Container.MappedPort(context.Background(), "80/tcp") + if err != nil { + panic(err) + } + + hostIP, err := c.Container.Host(context.Background()) + if err != nil { + panic(err) + } + + return fmt.Sprintf("http://%s:%s", hostIP, mappedPort.Port()) +} + +func (c *PiHoleContainer) EnvString() string { + return fmt.Sprintf("%s|%s", c.ConnectionString(), c.password) +} + +func RunPiHole(ctx context.Context, password string, opts ...testcontainers.ContainerCustomizer) *PiHoleContainer { + req := testcontainers.ContainerRequest{ + Image: "pihole/pihole:development-v6", + ExposedPorts: []string{"80/tcp"}, + WaitingFor: wait.ForListeningPort("80").WithStartupTimeout(10 * time.Second), + Env: map[string]string{ + "FTLCONF_webserver_api_password": password, + }, + } + + genericContainerReq := testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + } + + for _, opt := range opts { + if err := opt.Customize(&genericContainerReq); err != nil { + panic(err) + } + } + + container, err := testcontainers.GenericContainer(ctx, genericContainerReq) + if err != nil { + panic(err) + } + + return &PiHoleContainer{Container: container, password: password} +} diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go new file mode 100644 index 0000000..9cc95cf --- /dev/null +++ b/e2e/e2e_test.go @@ -0,0 +1,47 @@ +package e2e + +import ( + "context" + "github.com/lovelaze/nebula-sync/internal/service" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "testing" +) + +type testSuite struct { + suite.Suite + piHole1 *PiHoleContainer + piHole2 *PiHoleContainer +} + +func (suite *testSuite) SetupTest() { + ctx := context.Background() + suite.piHole1 = RunPiHole(ctx, "foo1") + suite.piHole2 = RunPiHole(ctx, "foo2") +} + +func TestE2E(t *testing.T) { + suite.Run(t, new(testSuite)) +} + +func (suite *testSuite) Test_FullSync() { + suite.T().Setenv("PRIMARY", suite.piHole1.EnvString()) + suite.T().Setenv("REPLICAS", suite.piHole2.EnvString()) + suite.T().Setenv("FULL_SYNC", "true") + + srv, err := service.Init() + require.NoError(suite.T(), err) + err = srv.Run() + require.NoError(suite.T(), err) +} + +func (suite *testSuite) Test_ManualSync() { + suite.T().Setenv("PRIMARY", suite.piHole1.EnvString()) + suite.T().Setenv("REPLICAS", suite.piHole2.EnvString()) + suite.T().Setenv("FULL_SYNC", "false") + + srv, err := service.Init() + require.NoError(suite.T(), err) + err = srv.Run() + require.NoError(suite.T(), err) +} diff --git a/go.mod b/go.mod index 2aef30c..e84e654 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.22 require ( github.com/joho/godotenv v1.5.1 github.com/kelseyhightower/envconfig v1.4.0 - github.com/pkg/errors v0.9.1 github.com/robfig/cron/v3 v3.0.1 github.com/rs/zerolog v1.33.0 github.com/spf13/cobra v1.8.1 @@ -23,6 +22,7 @@ require ( github.com/containerd/errdefs v0.1.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/cpuguy83/dockercfg v0.3.1 // indirect + github.com/creack/pty v1.1.21 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v27.1.1+incompatible // indirect @@ -37,7 +37,7 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/klauspost/compress v1.17.4 // indirect - github.com/kr/text v0.2.0 // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect @@ -50,8 +50,10 @@ require ( github.com/morikuni/aec v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/shirou/gopsutil/v3 v3.23.12 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/sirupsen/logrus v1.9.3 // indirect @@ -62,11 +64,15 @@ require ( github.com/yusufpapurcu/wmi v1.2.3 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect go.opentelemetry.io/otel v1.24.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect + go.opentelemetry.io/otel/sdk v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect golang.org/x/crypto v0.22.0 // indirect + golang.org/x/net v0.24.0 // indirect golang.org/x/sys v0.24.0 // indirect - golang.org/x/time v0.1.0 // indirect + golang.org/x/time v0.3.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect google.golang.org/grpc v1.59.0 // indirect google.golang.org/protobuf v1.33.0 // indirect diff --git a/go.sum b/go.sum index 708a005..73e94ef 100644 --- a/go.sum +++ b/go.sum @@ -21,8 +21,8 @@ github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoY github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= -github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= +github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -66,8 +66,8 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= @@ -96,6 +96,7 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -104,8 +105,9 @@ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= -github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= -github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= @@ -147,14 +149,14 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 h1:digkEZCJWobwBqMwC0cwCq8/wkkRy/OowZg5OArWZrM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0/go.mod h1:/OpE/y70qVkndM0TrxT4KBoN3RsFZP0QaofcfYrj76I= go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= -go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= -go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= +go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= +go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= @@ -170,8 +172,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -196,8 +198,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA= -golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -206,9 +208,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto v0.0.0-20231012201019-e917dd12ba7a h1:fwgW9j3vHirt4ObdHoYNwuO24BEZjSzbh+zPaNWoiY8= -google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb h1:lK0oleSc7IQsUxO3U5TjL9DWlsxpEBemh+zpB7IqhWI= -google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= +google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b h1:CIC2YMXmIhYw6evmhPxBKJ4fmLbOFtXQN/GV3XOZR8k= +google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:IBQ646DjkDkvUIsVq/cc03FUFQ9wbZu7yE396YcL870= google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b h1:ZlWIi1wSK56/8hn4QcBp/j9M7Gt3U/3hZw3mC7vDICo= google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:swOH3j0KzcDDgGUWr+SNpyTen5YrXjS3eyPzFYKc6lc= google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= diff --git a/internal/config/config.go b/internal/config/config.go index 7d91634..8ca34af 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -43,14 +43,15 @@ type SyncSettings struct { Config *ManualConfig `ignored:"true"` } -func (c *Config) Load() { +func (c *Config) Load() error { if err := envconfig.Process("", c); err != nil { - log.Fatal().Err(err).Msg("Failed to process env vars") + return err } if !c.FullSync { c.loadSyncSettings() } + return nil } func (c *Config) loadSyncSettings() { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 9fcfd5f..54c3d63 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -15,7 +15,8 @@ func TestConfig_Load(t *testing.T) { t.Setenv("FULL_SYNC", "true") t.Setenv("CRON", "* * * * *") - conf.Load() + err := conf.Load() + require.NoError(t, err) assert.Equal(t, "http://localhost:1337", conf.Primary.Url.String()) assert.Equal(t, "asdf", conf.Primary.Password) diff --git a/internal/pihole/client.go b/internal/pihole/client.go index 612f38e..ff4017a 100644 --- a/internal/pihole/client.go +++ b/internal/pihole/client.go @@ -7,6 +7,7 @@ import ( "fmt" "github.com/lovelaze/nebula-sync/internal/pihole/model" "github.com/lovelaze/nebula-sync/version" + "github.com/rs/zerolog" "github.com/rs/zerolog/log" "io" "mime/multipart" @@ -20,7 +21,11 @@ var ( ) func NewClient(piHole model.PiHole) Client { - return &client{PiHole: piHole} + logger := log.With().Str("client", piHole.Url.String()).Logger() + return &client{ + PiHole: piHole, + logger: &logger, + } } type Client interface { @@ -38,6 +43,7 @@ type Client interface { type client struct { PiHole model.PiHole auth auth + logger *zerolog.Logger } type auth struct { @@ -64,7 +70,7 @@ func (a *auth) verify() error { } func (client *client) Authenticate() error { - log.Debug().Msgf("Authenticate, client %s", client.String()) + client.logger.Debug().Msg("Authenticate") authResponse := model.AuthResponse{} reqBytes, err := json.Marshal(model.AuthRequest{Password: client.PiHole.Password}) @@ -110,7 +116,7 @@ func (client *client) Authenticate() error { } func (client *client) DeleteSession() error { - log.Debug().Msgf("Delete session, client %s", client.String()) + client.logger.Debug().Msg("Delete session") if err := client.auth.verify(); err != nil { return err } @@ -137,7 +143,7 @@ func (client *client) DeleteSession() error { } func (client *client) GetVersion() (*model.VersionResponse, error) { - log.Debug().Msgf("Get version, client %s", client.String()) + client.logger.Debug().Msg("Get version") versionResponse := model.VersionResponse{} if err := client.auth.verify(); err != nil { return &versionResponse, err @@ -170,7 +176,7 @@ func (client *client) GetVersion() (*model.VersionResponse, error) { } func (client *client) GetTeleporter() ([]byte, error) { - log.Debug().Msgf("Get teleporter, client %s", client.String()) + client.logger.Debug().Msg("Get teleporter") if err := client.auth.verify(); err != nil { return nil, err } @@ -195,7 +201,8 @@ func (client *client) GetTeleporter() ([]byte, error) { } func (client *client) PostTeleporter(payload []byte, teleporterRequest *model.PostTeleporterRequest) error { - log.Debug().Msgf("Post teleporter, client %s, request %v", client.String(), teleporterRequest) + client.logger.Debug().Any("payload", teleporterRequest).Msg("Post teleporter") + if err := client.auth.verify(); err != nil { return err } @@ -243,7 +250,7 @@ func (client *client) PostTeleporter(payload []byte, teleporterRequest *model.Po } func (client *client) GetConfig() (configResponse *model.ConfigResponse, err error) { - log.Debug().Msgf("Get config, client %s", client.String()) + client.logger.Debug().Msg("Get config") if err := client.auth.verify(); err != nil { return configResponse, err } @@ -277,7 +284,7 @@ func (client *client) GetConfig() (configResponse *model.ConfigResponse, err err } func (client *client) PatchConfig(patchRequest *model.PatchConfigRequest) error { - log.Debug().Msgf("Patch config, client %s", client.String()) + client.logger.Debug().Any("payload", patchRequest).Msgf("Patch config") if err := client.auth.verify(); err != nil { return err } diff --git a/internal/pihole/client_test.go b/internal/pihole/client_test.go index a07221a..c642066 100644 --- a/internal/pihole/client_test.go +++ b/internal/pihole/client_test.go @@ -3,69 +3,66 @@ package pihole import ( "context" "fmt" + "github.com/lovelaze/nebula-sync/e2e" "github.com/lovelaze/nebula-sync/internal/pihole/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" - "github.com/testcontainers/testcontainers-go" - "github.com/testcontainers/testcontainers-go/wait" - "log" + tc "github.com/testcontainers/testcontainers-go" "testing" - "time" ) const ( - dockerImage string = "pihole/pihole:development-v6" apiPassword string = "test" ) var ( - container = startContainer() + piHole = e2e.RunPiHole(context.Background(), apiPassword).Container ) -type ClientTestSuite struct { +type clientTestSuite struct { suite.Suite client Client } -func (suite *ClientTestSuite) SetupTest() { - client := createClient(container) +func (suite *clientTestSuite) SetupTest() { + client := createClient(piHole) err := client.Authenticate() require.NoError(suite.T(), err) suite.client = client } func TestClientIntegration(t *testing.T) { - suite.Run(t, new(ClientTestSuite)) + suite.Run(t, new(clientTestSuite)) } -func (suite *ClientTestSuite) TestClient_Authenticate() { +func (suite *clientTestSuite) TestClient_Authenticate() { err := suite.client.Authenticate() assert.NoError(suite.T(), err) } -func (suite *ClientTestSuite) TestClient_DeleteSession() { +func (suite *clientTestSuite) TestClient_DeleteSession() { err := suite.client.DeleteSession() assert.NoError(suite.T(), err) } -func (suite *ClientTestSuite) TestClient_GetVersion() { +func (suite *clientTestSuite) TestClient_GetVersion() { version, err := suite.client.GetVersion() assert.NoError(suite.T(), err) assert.NotNil(suite.T(), version) } -func (suite *ClientTestSuite) TestClient_GetTeleporter() { +func (suite *clientTestSuite) TestClient_GetTeleporter() { payload, err := suite.client.GetTeleporter() assert.NoError(suite.T(), err) assert.NotNil(suite.T(), payload) } -func (suite *ClientTestSuite) TestClient_PostTeleporter() { +func (suite *clientTestSuite) TestClient_PostTeleporter() { payload, _ := suite.client.GetTeleporter() err := suite.client.PostTeleporter(payload, &model.PostTeleporterRequest{ Config: true, @@ -84,14 +81,14 @@ func (suite *ClientTestSuite) TestClient_PostTeleporter() { assert.NoError(suite.T(), err) } -func (suite *ClientTestSuite) TestClient_GetConfig() { +func (suite *clientTestSuite) TestClient_GetConfig() { conf, err := suite.client.GetConfig() assert.NoError(suite.T(), err) assert.NotNil(suite.T(), conf) } -func (suite *ClientTestSuite) TestClient_PatchConfig() { +func (suite *clientTestSuite) TestClient_PatchConfig() { request := model.PatchConfigRequest{ Config: model.PatchConfig{ DNS: nil, @@ -145,29 +142,7 @@ func Test_auth_verify(t *testing.T) { } -func startContainer() testcontainers.Container { - containerRequest := testcontainers.ContainerRequest{ - Image: dockerImage, - ExposedPorts: []string{"80/tcp", "53/tcp", "53/udp"}, - WaitingFor: wait.ForListeningPort("80").WithStartupTimeout(30 * time.Second), - Env: map[string]string{ - "FTLCONF_dns_upstreams": "8.8.8.8", - "FTLCONF_webserver_api_password": apiPassword, - }, - } - - container, err := testcontainers.GenericContainer(context.Background(), testcontainers.GenericContainerRequest{ - ContainerRequest: containerRequest, - Started: true, - }) - - if err != nil { - log.Fatalf("starting pihole test container: %v", err) - } - return container -} - -func createClient(container testcontainers.Container) Client { +func createClient(container tc.Container) Client { apiPort, err := container.MappedPort(context.Background(), "80/tcp") if err != nil { panic(err) @@ -175,5 +150,5 @@ func createClient(container testcontainers.Container) Client { host := fmt.Sprintf("http://localhost:%s", apiPort.Port()) - return NewClient(model.NewPiHole(host, "test")) + return NewClient(model.NewPiHole(host, apiPassword)) } diff --git a/internal/pihole/model/pihole_test.go b/internal/pihole/model/pihole_test.go index 686d6d3..408bd3d 100644 --- a/internal/pihole/model/pihole_test.go +++ b/internal/pihole/model/pihole_test.go @@ -20,5 +20,4 @@ func TestPiHole_Decode(t *testing.T) { assert.Equal(t, expectedUrl, ph.Url) assert.Equal(t, pw, ph.Password) - } diff --git a/internal/service/service.go b/internal/service/service.go index 1542066..9efecfc 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -1,6 +1,7 @@ package service import ( + "fmt" "github.com/lovelaze/nebula-sync/internal/config" "github.com/lovelaze/nebula-sync/internal/pihole" "github.com/lovelaze/nebula-sync/internal/sync" @@ -14,7 +15,12 @@ type Service struct { conf config.Config } -func NewService(conf config.Config) *Service { +func Init() (*Service, error) { + conf := config.Config{} + if err := conf.Load(); err != nil { + return nil, err + } + primary := pihole.NewClient(conf.Primary) var rs []pihole.Client for _, replica := range conf.Replicas { @@ -24,24 +30,25 @@ func NewService(conf config.Config) *Service { return &Service{ target: sync.NewTarget(primary, rs), conf: conf, - } + }, nil } -func (service *Service) Run() { +func (service *Service) Run() error { log.Info().Msgf("Starting nebula-sync v%s", version.Version) log.Debug().Msgf("Settings cron=%v, fullsync=%v, syncsettings=%v", service.conf.Cron, service.conf.FullSync, service.conf.SyncSettings) if service.conf.Cron == nil { - service.doSync(service.target) + return service.doSync(service.target) } else { - service.startCron(func() { - service.doSync(service.target) + return service.startCron(func() { + if err := service.doSync(service.target); err != nil { + log.Error().Err(err).Msg("sync failed") + } }) } } -func (service *Service) doSync(t sync.Target) { - var err error +func (service *Service) doSync(t sync.Target) (err error) { if service.conf.FullSync { err = t.FullSync() } else { @@ -49,19 +56,24 @@ func (service *Service) doSync(t sync.Target) { } if err != nil { - log.Error().Err(err).Msgf("Sync failed") - return + return fmt.Errorf("sync failed: %w", err) } log.Info().Msg("Sync complete") + return err } -func (service *Service) startCron(cmd func()) { +func (service *Service) startCron(cmd func()) error { cron := cron.New() if _, err := cron.AddFunc(*service.conf.Cron, cmd); err != nil { - log.Fatal().Err(err).Msgf("Failed to start cron: %s", *service.conf.Cron) + return fmt.Errorf("failed to start cron job: %w", err) } cron.Run() + return nil +} + +func (service *Service) Target() sync.Target { + return service.target } diff --git a/internal/sync/sync.go b/internal/sync/sync.go index 19892ad..dfe3e07 100644 --- a/internal/sync/sync.go +++ b/internal/sync/sync.go @@ -1,10 +1,10 @@ package sync import ( + "fmt" "github.com/lovelaze/nebula-sync/internal/config" "github.com/lovelaze/nebula-sync/internal/pihole" "github.com/lovelaze/nebula-sync/internal/pihole/model" - "github.com/pkg/errors" "github.com/rs/zerolog/log" ) @@ -28,15 +28,15 @@ func NewTarget(primary pihole.Client, replicas []pihole.Client) Target { func (target *target) FullSync() error { log.Info().Int("replicas", len(target.Replicas)).Msg("Running full sync") if err := target.authenticate(); err != nil { - return errors.Wrap(err, "authentication failed") + return fmt.Errorf("authentication failed: %w", err) } if err := target.syncTeleporters(nil); err != nil { - return errors.Wrap(err, "sync Teleporters failed") + return fmt.Errorf("sync Teleporters failed: %w", err) } if err := target.deleteSessions(); err != nil { - return errors.Wrap(err, "delete sessions failed") + return fmt.Errorf("delete sessions failed: %w", err) } return nil @@ -46,19 +46,19 @@ func (target *target) ManualSync(syncSettings *config.SyncSettings) error { log.Info().Int("replicas", len(target.Replicas)).Msg("Running manual sync") if err := target.authenticate(); err != nil { - return errors.Wrap(err, "authentication failed") + return fmt.Errorf("authentication failed: %w", err) } if err := target.syncTeleporters(syncSettings.Gravity); err != nil { - return errors.Wrap(err, "sync Teleporters failed") + return fmt.Errorf("sync Teleporters failed: %w", err) } if err := target.syncConfigs(syncSettings.Config); err != nil { - return errors.Wrap(err, "sync configs failed") + return fmt.Errorf("sync configs failed: %w", err) } if err := target.deleteSessions(); err != nil { - return errors.Wrap(err, "delete sessions failed") + return fmt.Errorf("delete sessions failed: %w", err) } return nil @@ -116,6 +116,7 @@ func (target *target) syncTeleporters(manualGravity *config.ManualGravity) error } func (target *target) syncConfigs(manualConfig *config.ManualConfig) error { + log.Info().Msg("Syncing configs...") configResponse, err := target.Primary.GetConfig() if err != nil { return err