0.1.0 Release

This commit is contained in:
Fredrik Berntsson
2024-08-08 10:33:44 +02:00
parent fa05b6414c
commit 460b0b678b
29 changed files with 2603 additions and 1 deletions
+54
View File
@@ -0,0 +1,54 @@
name: release
on:
push:
tags:
- "v*.*.*"
jobs:
docker:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to ghcr.io
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract tag
id: extract_tag
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
ghcr.io/lovelaze/nebula-sync:latest
ghcr.io/lovelaze/nebula-sync:${{ env.TAG }}
platforms: linux/amd64,linux/arm64
provenance: false
release:
needs: docker
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Release
uses: softprops/action-gh-release@v2
+25
View File
@@ -0,0 +1,25 @@
name: Test
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
- name: Build
run: go build -v ./...
- name: Test
run: go test -cover -v ./...
+21
View File
@@ -0,0 +1,21 @@
# intellij
.idea
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
nebula-sync
# Test binary, built with `go test -c`
*.test
.env
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Go workspace file
go.work
go.work.sum
+10
View File
@@ -0,0 +1,10 @@
with-expecter: True
dir: internal/mocks/{{.PackageName}}
mockname: "{{.InterfaceName}}"
outpkg: "{{.PackageName}}"
filename: "{{.InterfaceName}}.go"
all: True
packages:
github.com/lovelaze/nebula-sync:
config:
recursive: True
+30
View File
@@ -0,0 +1,30 @@
FROM golang:1.22-alpine AS golang
WORKDIR /app
RUN apk add -U tzdata upx && \
apk --update add ca-certificates
COPY . .
RUN go mod download
RUN go mod verify
ENV GO111MODULE=on
ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOFLAGS="-a -ldflags=-w -ldflags=-s -trimpath -o=nebula-sync"
RUN go build . && \
upx -q nebula-sync
FROM scratch
COPY --from=golang /usr/share/zoneinfo/ /usr/share/zoneinfo/
COPY --from=golang /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=golang /app/nebula-sync /usr/local/bin/
USER 1001
ENTRYPOINT ["nebula-sync"]
CMD ["run"]
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Fredrik Berntsson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+92 -1
View File
@@ -1 +1,92 @@
# nebula-sync
# nebula-sync
[![Go](https://github.com/lovelaze/nebula-sync/actions/workflows/test.yml/badge.svg)](https://github.com/lovelaze/nebula-sync/actions/workflows/test.yml)
Synchronize Pi-hole 6+ configuration to replicas.
This project is not a part of the [official Pi-hole project](https://github.com/pi-hole), but uses the api provided by Pi-hole instances to perform the synchronization actions.
## Features
- **Full sync**: Use Pi-hole Teleporter for full synchronization.
- **Manual sync**: Selective feature synchronization.
- **Cron schedule**: Run on chron schedule.
## Installation
### Docker Compose (recommended)
```yaml
---
services:
nebula-sync:
image: ghcr.io/lovelaze/nebula-sync:latest
container_name: nebula-sync
environment:
- PRIMARY=http://ph1.example.com|password
- REPLICAS=http://ph2.example.com|password,http://ph3.example.com|password
- FULL_SYNC=true
- CRON=0 * * * *
restart: unless-stopped
```
### Docker CLI
```bash
docker run -d \
--name nebula-sync \
-e PRIMARY=http://ph1.example.com|password \
-e REPLICAS=http://ph2.example.com|password,http://ph3.example.com|password \
-e FULL_SYNC=true \
-e CRON=0 * * * * \
--restart unless-stopped \
ghcr.io/lovelaze/nebula-sync:latest
```
## Configuration
The following environment variables can be specified:
### Required Environment Variables
| Name | Default | Example | Description |
|-----------|---------|--------------------------------------------------|----------------------------------------------------------|
| `PRIMARY` | n/a | `http://ph1.example.com\|password` | Specifies the primary Pi-hole configuration |
| `REPLICAS`| n/a | `http://ph2.example.com\|password,http://ph3.example.com\|password` | Specifies the list of replica Pi-hole configurations |
| `FULL_SYNC` | n/a | `true` | Specifies whether to perform a full synchronization |
> **Note:** When `FULL_SYNC=true`, the system will perform a full Teleporter import/export from the primary Pi-hole to the replicas. This will synchronize all settings and configurations.
### Optional Environment Variables
| Name | Default | Example | Description |
|----------|---------|---------------|------------------------------------------------|
| `CRON` | n/a | `0 * * * *` | Specifies the cron schedule for synchronization |
> **Note:** The following optional settings apply only if `FULL_SYNC=false`. They allow for granular control of synchronization if a full sync is not wanted.
| Name | Default | Description |
|-----------------------------------|---------|----------------------------------------|
| `SYNC_CONFIG_DNS` | false | Synchronize DNS settings |
| `SYNC_CONFIG_DHCP` | false | Synchronize DHCP settings |
| `SYNC_CONFIG_NTP` | false | Synchronize NTP settings |
| `SYNC_CONFIG_RESOLVER` | false | Synchronize resolver settings |
| `SYNC_CONFIG_DATABASE` | false | Synchronize database settings |
| `SYNC_CONFIG_MISC` | false | Synchronize miscellaneous settings |
| `SYNC_CONFIG_DEBUG` | false | Synchronize debug settings |
| `SYNC_GRAVITY_DHCP_LEASES` | false | Synchronize DHCP leases |
| `SYNC_GRAVITY_GROUP` | false | Synchronize groups |
| `SYNC_GRAVITY_AD_LIST` | false | Synchronize ad lists |
| `SYNC_GRAVITY_AD_LIST_BY_GROUP` | false | Synchronize ad lists by group |
| `SYNC_GRAVITY_DOMAIN_LIST` | false | Synchronize domain lists |
| `SYNC_GRAVITY_DOMAIN_LIST_BY_GROUP`| false | Synchronize domain lists by group |
| `SYNC_GRAVITY_CLIENT` | false | Synchronize clients |
| `SYNC_GRAVITY_CLIENT_BY_GROUP` | false | Synchronize clients by group |
## Disclaimer
This project is an unofficial, community-maintained project and is not affiliated with the [official Pi-hole project](https://github.com/pi-hole). It aims to add sync/replication features not available in the core Pi-hole product but operates independently of Pi-hole LLC. Although tested across various environments, using any software from the Internet involves inherent risks. See the [license](https://github.com/lovelaze/nebula-sync/blob/main/LICENSE) for more details.
Pi-hole and the Pi-hole logo are [registered trademarks](https://pi-hole.net/trademark-rules-and-brand-guidelines) of Pi-hole LLC.
+21
View File
@@ -0,0 +1,21 @@
package cmd
import (
"github.com/lovelaze/nebula-sync/internal/log"
"github.com/lovelaze/nebula-sync/version"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "nebula-sync",
Version: version.Version,
}
func Execute() {
rootCmd.Execute()
}
func init() {
cobra.OnInitialize(log.Init)
rootCmd.CompletionOptions.HiddenDefaultCmd = true
}
+23
View File
@@ -0,0 +1,23 @@
package cmd
import (
"github.com/lovelaze/nebula-sync/internal/config"
"github.com/lovelaze/nebula-sync/internal/service"
"github.com/spf13/cobra"
)
var runCmd = &cobra.Command{
Use: "run",
Short: "Run sync",
Run: func(cmd *cobra.Command, args []string) {
conf := config.Config{}
conf.Load()
service := service.NewService(conf)
service.Run()
},
}
func init() {
rootCmd.AddCommand(runCmd)
}
+73
View File
@@ -0,0 +1,73 @@
module github.com/lovelaze/nebula-sync
go 1.22
require (
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
github.com/stretchr/testify v1.9.0
github.com/testcontainers/testcontainers-go v0.32.0
)
require (
dario.cat/mergo v1.0.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/Microsoft/hcsshim v0.11.5 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/containerd/containerd v1.7.18 // indirect
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/davecgh/go-spew v1.1.1 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/docker v27.0.3+incompatible // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
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/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
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/moby/sys/user v0.1.0 // indirect
github.com/moby/term v0.5.0 // indirect
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/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // 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
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
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/metric v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/sys v0.23.0 // indirect
golang.org/x/time v0.1.0 // 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
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+223
View File
@@ -0,0 +1,223 @@
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/Microsoft/hcsshim v0.11.5 h1:haEcLNpj9Ka1gd3B3tAEs9CpE0c+1IhoL59w/exYU38=
github.com/Microsoft/hcsshim v0.11.5/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA09d4bExKcU=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao=
github.com/containerd/containerd v1.7.18/go.mod h1:IYEk9/IO6wAPUz2bCMVUbsfXjzw5UNP5fLz4PsUygQ4=
github.com/containerd/errdefs v0.1.0 h1:m0wCRBiu1WJT/Fr+iOoQHMQS/eP5myQ8lCv4Dz5ZURM=
github.com/containerd/errdefs v0.1.0/go.mod h1:YgWiiHtLmSeBrvpw+UfPijzbLaB77mEG1WwJTDETIV0=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E=
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/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=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v27.0.3+incompatible h1:aBGI9TeQ4MPlhquTQKq9XbK79rKFVwXNUAYz9aXyEBE=
github.com/docker/docker v27.0.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
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.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
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/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=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg=
github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
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/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=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
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/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=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4=
github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/testcontainers/testcontainers-go v0.32.0 h1:ug1aK08L3gCHdhknlTTwWjPHPS+/alvLJU/DRxTD/ME=
github.com/testcontainers/testcontainers-go v0.32.0/go.mod h1:CRHrzHLQhlXUsa5gXjTOfqIEJcrK5+xMDmBr/WMI88E=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
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/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/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=
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
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/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=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
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/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=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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/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=
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
+70
View File
@@ -0,0 +1,70 @@
package config
import (
"github.com/kelseyhightower/envconfig"
"github.com/lovelaze/nebula-sync/internal/pihole/model"
"log"
)
type Config struct {
Primary model.PiHole `required:"true" envconfig:"PRIMARY"`
Replicas []model.PiHole `required:"true" envconfig:"REPLICAS"`
FullSync bool `required:"true" envconfig:"FULL_SYNC"`
Cron *string `envconfig:"CRON"`
SyncSettings *SyncSettings `ignored:"true"`
}
type ManualGravity struct {
DHCPLeases bool `default:"false" envconfig:"SYNC_GRAVITY_DHCP_LEASES"`
Group bool `default:"false" envconfig:"SYNC_GRAVITY_GROUP"`
Adlist bool `default:"false" envconfig:"SYNC_GRAVITY_AD_LIST"`
AdlistByGroup bool `default:"false" envconfig:"SYNC_GRAVITY_AD_LIST_BY_GROUP"`
Domainlist bool `default:"false" envconfig:"SYNC_GRAVITY_DOMAIN_LIST"`
DomainlistByGroup bool `default:"false" envconfig:"SYNC_GRAVITY_DOMAIN_LIST_BY_GROUP"`
Client bool `default:"false" envconfig:"SYNC_GRAVITY_CLIENT"`
ClientByGroup bool `default:"false" envconfig:"SYNC_GRAVITY_CLIENT_BY_GROUP"`
}
type ManualConfig struct {
DNS bool `default:"false" envconfig:"SYNC_CONFIG_DNS"`
DHCP bool `default:"false" envconfig:"SYNC_CONFIG_DHCP"`
NTP bool `default:"false" envconfig:"SYNC_CONFIG_NTP"`
Resolver bool `default:"false" envconfig:"SYNC_CONFIG_RESOLVER"`
Database bool `default:"false" envconfig:"SYNC_CONFIG_DATABASE"`
Webserver bool `default:"false" ignored:"true"` // ignore for now
Files bool `default:"false" ignored:"true"` // ignore for now
Misc bool `default:"false" envconfig:"SYNC_CONFIG_MISC"`
Debug bool `default:"false" envconfig:"SYNC_CONFIG_DEBUG"`
}
type SyncSettings struct {
Gravity *ManualGravity `ignored:"true"`
Config *ManualConfig `ignored:"true"`
}
func (c *Config) Load() {
if err := envconfig.Process("", c); err != nil {
log.Fatal(err)
}
if !c.FullSync {
c.loadSyncSettings()
}
}
func (c *Config) loadSyncSettings() {
manualGravity := ManualGravity{}
if err := envconfig.Process("", &manualGravity); err != nil {
log.Fatal(err)
}
manualConfig := ManualConfig{}
if err := envconfig.Process("", &manualConfig); err != nil {
log.Fatal(err)
}
c.SyncSettings = &SyncSettings{
Gravity: &manualGravity,
Config: &manualConfig,
}
}
+70
View File
@@ -0,0 +1,70 @@
package config
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestConfig_Load(t *testing.T) {
conf := Config{}
t.Setenv("PRIMARY", "http://localhost:1337|asdf")
t.Setenv("REPLICAS", "http://localhost:1338|qwerty")
t.Setenv("FULL_SYNC", "true")
t.Setenv("CRON", "* * * * *")
conf.Load()
assert.Equal(t, "http://localhost:1337", conf.Primary.Url.String())
assert.Equal(t, "asdf", conf.Primary.Password)
assert.Len(t, conf.Replicas, 1)
assert.Equal(t, "http://localhost:1338", conf.Replicas[0].Url.String())
assert.Equal(t, "qwerty", conf.Replicas[0].Password)
assert.Equal(t, true, conf.FullSync)
assert.Equal(t, "* * * * *", *conf.Cron)
assert.Nil(t, conf.SyncSettings)
}
func TestConfig_loadSyncSettings(t *testing.T) {
conf := Config{}
assert.Nil(t, conf.SyncSettings)
t.Setenv("SYNC_CONFIG_DNS", "true")
t.Setenv("SYNC_CONFIG_DHCP", "true")
t.Setenv("SYNC_CONFIG_NTP", "true")
t.Setenv("SYNC_CONFIG_RESOLVER", "true")
t.Setenv("SYNC_CONFIG_DATABASE", "true")
t.Setenv("SYNC_CONFIG_MISC", "true")
t.Setenv("SYNC_CONFIG_DEBUG", "true")
t.Setenv("SYNC_GRAVITY_DHCP_LEASES", "true")
t.Setenv("SYNC_GRAVITY_GROUP", "true")
t.Setenv("SYNC_GRAVITY_AD_LIST", "true")
t.Setenv("SYNC_GRAVITY_AD_LIST_BY_GROUP", "true")
t.Setenv("SYNC_GRAVITY_DOMAIN_LIST", "true")
t.Setenv("SYNC_GRAVITY_DOMAIN_LIST_BY_GROUP", "true")
t.Setenv("SYNC_GRAVITY_CLIENT", "true")
t.Setenv("SYNC_GRAVITY_CLIENT_BY_GROUP", "true")
conf.loadSyncSettings()
assert.NotNil(t, conf.SyncSettings.Config)
assert.NotNil(t, conf.SyncSettings.Gravity)
assert.True(t, conf.SyncSettings.Config.DNS)
assert.True(t, conf.SyncSettings.Config.DHCP)
assert.True(t, conf.SyncSettings.Config.NTP)
assert.True(t, conf.SyncSettings.Config.Resolver)
assert.True(t, conf.SyncSettings.Config.Database)
assert.True(t, conf.SyncSettings.Config.Misc)
assert.True(t, conf.SyncSettings.Config.Debug)
assert.True(t, conf.SyncSettings.Gravity.DHCPLeases)
assert.True(t, conf.SyncSettings.Gravity.Group)
assert.True(t, conf.SyncSettings.Gravity.Adlist)
assert.True(t, conf.SyncSettings.Gravity.AdlistByGroup)
assert.True(t, conf.SyncSettings.Gravity.Domainlist)
assert.True(t, conf.SyncSettings.Gravity.DomainlistByGroup)
assert.True(t, conf.SyncSettings.Gravity.Client)
assert.True(t, conf.SyncSettings.Gravity.ClientByGroup)
}
+29
View File
@@ -0,0 +1,29 @@
package log
import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"os"
"strconv"
"time"
)
func Init() {
logger := log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339})
log.Logger = logger
zerolog.SetGlobalLevel(zerolog.InfoLevel)
if debugEnv := os.Getenv("NS_DEBUG"); debugEnv != "" {
debug, err := strconv.ParseBool(debugEnv)
if err != nil {
log.Warn().Err(err).Msgf("failed to parse boolean env NS_DEBUG")
}
if debug {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
logger = logger.With().Caller().Logger()
}
}
log.Logger = logger
}
+20
View File
@@ -0,0 +1,20 @@
package log
import (
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"testing"
)
func TestInit_info(t *testing.T) {
Init()
assert.Equal(t, zerolog.InfoLevel, zerolog.GlobalLevel())
}
func TestInit_debug(t *testing.T) {
t.Setenv("NS_DEBUG", "true")
Init()
assert.Equal(t, zerolog.DebugLevel, zerolog.GlobalLevel())
}
+480
View File
@@ -0,0 +1,480 @@
// Code generated by mockery v2.44.1. DO NOT EDIT.
package pihole
import (
model "github.com/lovelaze/nebula-sync/internal/pihole/model"
mock "github.com/stretchr/testify/mock"
)
// Client is an autogenerated mock type for the Client type
type Client struct {
mock.Mock
}
type Client_Expecter struct {
mock *mock.Mock
}
func (_m *Client) EXPECT() *Client_Expecter {
return &Client_Expecter{mock: &_m.Mock}
}
// ApiPath provides a mock function with given fields: target
func (_m *Client) ApiPath(target string) string {
ret := _m.Called(target)
if len(ret) == 0 {
panic("no return value specified for ApiPath")
}
var r0 string
if rf, ok := ret.Get(0).(func(string) string); ok {
r0 = rf(target)
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// Client_ApiPath_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ApiPath'
type Client_ApiPath_Call struct {
*mock.Call
}
// ApiPath is a helper method to define mock.On call
// - target string
func (_e *Client_Expecter) ApiPath(target interface{}) *Client_ApiPath_Call {
return &Client_ApiPath_Call{Call: _e.mock.On("ApiPath", target)}
}
func (_c *Client_ApiPath_Call) Run(run func(target string)) *Client_ApiPath_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *Client_ApiPath_Call) Return(_a0 string) *Client_ApiPath_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *Client_ApiPath_Call) RunAndReturn(run func(string) string) *Client_ApiPath_Call {
_c.Call.Return(run)
return _c
}
// Authenticate provides a mock function with given fields:
func (_m *Client) Authenticate() error {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for Authenticate")
}
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// Client_Authenticate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Authenticate'
type Client_Authenticate_Call struct {
*mock.Call
}
// Authenticate is a helper method to define mock.On call
func (_e *Client_Expecter) Authenticate() *Client_Authenticate_Call {
return &Client_Authenticate_Call{Call: _e.mock.On("Authenticate")}
}
func (_c *Client_Authenticate_Call) Run(run func()) *Client_Authenticate_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *Client_Authenticate_Call) Return(_a0 error) *Client_Authenticate_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *Client_Authenticate_Call) RunAndReturn(run func() error) *Client_Authenticate_Call {
_c.Call.Return(run)
return _c
}
// DeleteSession provides a mock function with given fields:
func (_m *Client) DeleteSession() error {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for DeleteSession")
}
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// Client_DeleteSession_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteSession'
type Client_DeleteSession_Call struct {
*mock.Call
}
// DeleteSession is a helper method to define mock.On call
func (_e *Client_Expecter) DeleteSession() *Client_DeleteSession_Call {
return &Client_DeleteSession_Call{Call: _e.mock.On("DeleteSession")}
}
func (_c *Client_DeleteSession_Call) Run(run func()) *Client_DeleteSession_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *Client_DeleteSession_Call) Return(_a0 error) *Client_DeleteSession_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *Client_DeleteSession_Call) RunAndReturn(run func() error) *Client_DeleteSession_Call {
_c.Call.Return(run)
return _c
}
// GetConfig provides a mock function with given fields:
func (_m *Client) GetConfig() (*model.ConfigResponse, error) {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for GetConfig")
}
var r0 *model.ConfigResponse
var r1 error
if rf, ok := ret.Get(0).(func() (*model.ConfigResponse, error)); ok {
return rf()
}
if rf, ok := ret.Get(0).(func() *model.ConfigResponse); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.ConfigResponse)
}
}
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Client_GetConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetConfig'
type Client_GetConfig_Call struct {
*mock.Call
}
// GetConfig is a helper method to define mock.On call
func (_e *Client_Expecter) GetConfig() *Client_GetConfig_Call {
return &Client_GetConfig_Call{Call: _e.mock.On("GetConfig")}
}
func (_c *Client_GetConfig_Call) Run(run func()) *Client_GetConfig_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *Client_GetConfig_Call) Return(configResponse *model.ConfigResponse, err error) *Client_GetConfig_Call {
_c.Call.Return(configResponse, err)
return _c
}
func (_c *Client_GetConfig_Call) RunAndReturn(run func() (*model.ConfigResponse, error)) *Client_GetConfig_Call {
_c.Call.Return(run)
return _c
}
// GetTeleporter provides a mock function with given fields:
func (_m *Client) GetTeleporter() ([]byte, error) {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for GetTeleporter")
}
var r0 []byte
var r1 error
if rf, ok := ret.Get(0).(func() ([]byte, error)); ok {
return rf()
}
if rf, ok := ret.Get(0).(func() []byte); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]byte)
}
}
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Client_GetTeleporter_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTeleporter'
type Client_GetTeleporter_Call struct {
*mock.Call
}
// GetTeleporter is a helper method to define mock.On call
func (_e *Client_Expecter) GetTeleporter() *Client_GetTeleporter_Call {
return &Client_GetTeleporter_Call{Call: _e.mock.On("GetTeleporter")}
}
func (_c *Client_GetTeleporter_Call) Run(run func()) *Client_GetTeleporter_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *Client_GetTeleporter_Call) Return(_a0 []byte, _a1 error) *Client_GetTeleporter_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *Client_GetTeleporter_Call) RunAndReturn(run func() ([]byte, error)) *Client_GetTeleporter_Call {
_c.Call.Return(run)
return _c
}
// GetVersion provides a mock function with given fields:
func (_m *Client) GetVersion() (*model.VersionResponse, error) {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for GetVersion")
}
var r0 *model.VersionResponse
var r1 error
if rf, ok := ret.Get(0).(func() (*model.VersionResponse, error)); ok {
return rf()
}
if rf, ok := ret.Get(0).(func() *model.VersionResponse); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.VersionResponse)
}
}
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Client_GetVersion_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetVersion'
type Client_GetVersion_Call struct {
*mock.Call
}
// GetVersion is a helper method to define mock.On call
func (_e *Client_Expecter) GetVersion() *Client_GetVersion_Call {
return &Client_GetVersion_Call{Call: _e.mock.On("GetVersion")}
}
func (_c *Client_GetVersion_Call) Run(run func()) *Client_GetVersion_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *Client_GetVersion_Call) Return(_a0 *model.VersionResponse, _a1 error) *Client_GetVersion_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *Client_GetVersion_Call) RunAndReturn(run func() (*model.VersionResponse, error)) *Client_GetVersion_Call {
_c.Call.Return(run)
return _c
}
// PatchConfig provides a mock function with given fields: patchRequest
func (_m *Client) PatchConfig(patchRequest *model.PatchConfigRequest) error {
ret := _m.Called(patchRequest)
if len(ret) == 0 {
panic("no return value specified for PatchConfig")
}
var r0 error
if rf, ok := ret.Get(0).(func(*model.PatchConfigRequest) error); ok {
r0 = rf(patchRequest)
} else {
r0 = ret.Error(0)
}
return r0
}
// Client_PatchConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PatchConfig'
type Client_PatchConfig_Call struct {
*mock.Call
}
// PatchConfig is a helper method to define mock.On call
// - patchRequest *model.PatchConfigRequest
func (_e *Client_Expecter) PatchConfig(patchRequest interface{}) *Client_PatchConfig_Call {
return &Client_PatchConfig_Call{Call: _e.mock.On("PatchConfig", patchRequest)}
}
func (_c *Client_PatchConfig_Call) Run(run func(patchRequest *model.PatchConfigRequest)) *Client_PatchConfig_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(*model.PatchConfigRequest))
})
return _c
}
func (_c *Client_PatchConfig_Call) Return(_a0 error) *Client_PatchConfig_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *Client_PatchConfig_Call) RunAndReturn(run func(*model.PatchConfigRequest) error) *Client_PatchConfig_Call {
_c.Call.Return(run)
return _c
}
// PostTeleporter provides a mock function with given fields: payload, teleporterRequest
func (_m *Client) PostTeleporter(payload []byte, teleporterRequest *model.PostTeleporterRequest) error {
ret := _m.Called(payload, teleporterRequest)
if len(ret) == 0 {
panic("no return value specified for PostTeleporter")
}
var r0 error
if rf, ok := ret.Get(0).(func([]byte, *model.PostTeleporterRequest) error); ok {
r0 = rf(payload, teleporterRequest)
} else {
r0 = ret.Error(0)
}
return r0
}
// Client_PostTeleporter_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PostTeleporter'
type Client_PostTeleporter_Call struct {
*mock.Call
}
// PostTeleporter is a helper method to define mock.On call
// - payload []byte
// - teleporterRequest *model.PostTeleporterRequest
func (_e *Client_Expecter) PostTeleporter(payload interface{}, teleporterRequest interface{}) *Client_PostTeleporter_Call {
return &Client_PostTeleporter_Call{Call: _e.mock.On("PostTeleporter", payload, teleporterRequest)}
}
func (_c *Client_PostTeleporter_Call) Run(run func(payload []byte, teleporterRequest *model.PostTeleporterRequest)) *Client_PostTeleporter_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].([]byte), args[1].(*model.PostTeleporterRequest))
})
return _c
}
func (_c *Client_PostTeleporter_Call) Return(_a0 error) *Client_PostTeleporter_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *Client_PostTeleporter_Call) RunAndReturn(run func([]byte, *model.PostTeleporterRequest) error) *Client_PostTeleporter_Call {
_c.Call.Return(run)
return _c
}
// String provides a mock function with given fields:
func (_m *Client) String() string {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for String")
}
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// Client_String_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'String'
type Client_String_Call struct {
*mock.Call
}
// String is a helper method to define mock.On call
func (_e *Client_Expecter) String() *Client_String_Call {
return &Client_String_Call{Call: _e.mock.On("String")}
}
func (_c *Client_String_Call) Run(run func()) *Client_String_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *Client_String_Call) Return(_a0 string) *Client_String_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *Client_String_Call) RunAndReturn(run func() string) *Client_String_Call {
_c.Call.Return(run)
return _c
}
// NewClient creates a new instance of Client. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewClient(t interface {
mock.TestingT
Cleanup(func())
}) *Client {
mock := &Client{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
+126
View File
@@ -0,0 +1,126 @@
// Code generated by mockery v2.44.1. DO NOT EDIT.
package sync
import (
config "github.com/lovelaze/nebula-sync/internal/config"
mock "github.com/stretchr/testify/mock"
)
// Target is an autogenerated mock type for the Target type
type Target struct {
mock.Mock
}
type Target_Expecter struct {
mock *mock.Mock
}
func (_m *Target) EXPECT() *Target_Expecter {
return &Target_Expecter{mock: &_m.Mock}
}
// FullSync provides a mock function with given fields:
func (_m *Target) FullSync() error {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for FullSync")
}
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// Target_FullSync_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FullSync'
type Target_FullSync_Call struct {
*mock.Call
}
// FullSync is a helper method to define mock.On call
func (_e *Target_Expecter) FullSync() *Target_FullSync_Call {
return &Target_FullSync_Call{Call: _e.mock.On("FullSync")}
}
func (_c *Target_FullSync_Call) Run(run func()) *Target_FullSync_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *Target_FullSync_Call) Return(_a0 error) *Target_FullSync_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *Target_FullSync_Call) RunAndReturn(run func() error) *Target_FullSync_Call {
_c.Call.Return(run)
return _c
}
// ManualSync provides a mock function with given fields: syncSettings
func (_m *Target) ManualSync(syncSettings *config.SyncSettings) error {
ret := _m.Called(syncSettings)
if len(ret) == 0 {
panic("no return value specified for ManualSync")
}
var r0 error
if rf, ok := ret.Get(0).(func(*config.SyncSettings) error); ok {
r0 = rf(syncSettings)
} else {
r0 = ret.Error(0)
}
return r0
}
// Target_ManualSync_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ManualSync'
type Target_ManualSync_Call struct {
*mock.Call
}
// ManualSync is a helper method to define mock.On call
// - syncSettings *config.SyncSettings
func (_e *Target_Expecter) ManualSync(syncSettings interface{}) *Target_ManualSync_Call {
return &Target_ManualSync_Call{Call: _e.mock.On("ManualSync", syncSettings)}
}
func (_c *Target_ManualSync_Call) Run(run func(syncSettings *config.SyncSettings)) *Target_ManualSync_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(*config.SyncSettings))
})
return _c
}
func (_c *Target_ManualSync_Call) Return(_a0 error) *Target_ManualSync_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *Target_ManualSync_Call) RunAndReturn(run func(*config.SyncSettings) error) *Target_ManualSync_Call {
_c.Call.Return(run)
return _c
}
// NewTarget creates a new instance of Target. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewTarget(t interface {
mock.TestingT
Cleanup(func())
}) *Target {
mock := &Target{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
+323
View File
@@ -0,0 +1,323 @@
package pihole
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"github.com/lovelaze/nebula-sync/internal/pihole/model"
"github.com/lovelaze/nebula-sync/version"
"github.com/rs/zerolog/log"
"io"
"mime/multipart"
"net/http"
"time"
)
var (
userAgent = fmt.Sprintf("nebula-sync/%s", version.Version)
httpClient = &http.Client{Timeout: 5 * time.Second}
)
func NewClient(piHole model.PiHole) Client {
return &client{PiHole: piHole}
}
type Client interface {
Authenticate() error
DeleteSession() error
GetVersion() (*model.VersionResponse, error)
GetTeleporter() ([]byte, error)
PostTeleporter(payload []byte, teleporterRequest *model.PostTeleporterRequest) error
GetConfig() (configResponse *model.ConfigResponse, err error)
PatchConfig(patchRequest *model.PatchConfigRequest) error
String() string
ApiPath(target string) string
}
type client struct {
PiHole model.PiHole
auth auth
}
type auth struct {
sid string
csrf string
validity int
valid bool
}
func (a *auth) verify() error {
if !a.valid {
return errors.New("invalid sid found")
}
if a.sid == "" {
return errors.New("no sid found")
}
if a.validity <= 0 {
return errors.New("expired sid found")
}
return nil
}
func (client *client) Authenticate() error {
log.Debug().Msgf("Authenticate, client %s", client.String())
authResponse := model.AuthResponse{}
reqBytes, err := json.Marshal(model.AuthRequest{Password: client.PiHole.Password})
if err != nil {
return err
}
req, err := http.NewRequest("POST", client.ApiPath("/auth"), bytes.NewReader(reqBytes))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", userAgent)
response, err := httpClient.Do(req)
if err != nil {
return err
}
if err := successfulHttpStatus(response.StatusCode); err != nil {
return err
}
body, err := io.ReadAll(response.Body)
if err != nil {
return err
}
if err = json.Unmarshal(body, &authResponse); err != nil {
return err
}
client.auth = auth{
sid: authResponse.Session.Sid,
csrf: authResponse.Session.Csrf,
validity: authResponse.Session.Validity,
valid: authResponse.Session.Valid,
}
return client.auth.verify()
}
func (client *client) DeleteSession() error {
log.Debug().Msgf("Delete session, client %s", client.String())
if err := client.auth.verify(); err != nil {
return err
}
req, err := http.NewRequest("DELETE", client.ApiPath("auth"), nil)
if err != nil {
return err
}
req.Header.Set("sid", client.auth.sid)
req.Header.Set("User-Agent", userAgent)
response, err := httpClient.Do(req)
if err != nil {
return err
}
if err := successfulHttpStatus(response.StatusCode); err != nil {
return err
}
return err
}
func (client *client) GetVersion() (*model.VersionResponse, error) {
log.Debug().Msgf("Get version, client %s", client.String())
versionResponse := model.VersionResponse{}
if err := client.auth.verify(); err != nil {
return &versionResponse, err
}
req, err := http.NewRequest("GET", client.ApiPath("info/version"), nil)
if err != nil {
return &versionResponse, err
}
req.Header.Set("sid", client.auth.sid)
req.Header.Set("User-Agent", userAgent)
response, err := httpClient.Do(req)
if err != nil {
return &versionResponse, err
}
if err := successfulHttpStatus(response.StatusCode); err != nil {
return &versionResponse, err
}
body, err := io.ReadAll(response.Body)
if err != nil {
return &versionResponse, err
}
err = json.Unmarshal(body, &versionResponse)
return &versionResponse, err
}
func (client *client) GetTeleporter() ([]byte, error) {
log.Debug().Msgf("Get teleporter, client %s", client.String())
if err := client.auth.verify(); err != nil {
return nil, err
}
req, err := http.NewRequest("GET", client.ApiPath("teleporter"), nil)
if err != nil {
return nil, err
}
req.Header.Set("sid", client.auth.sid)
req.Header.Set("User-Agent", userAgent)
response, err := httpClient.Do(req)
if err != nil {
return nil, err
}
if err := successfulHttpStatus(response.StatusCode); err != nil {
return nil, err
}
body, err := io.ReadAll(response.Body)
return body, err
}
func (client *client) PostTeleporter(payload []byte, teleporterRequest *model.PostTeleporterRequest) error {
log.Debug().Msgf("Post teleporter, client %s, request %v", client.String(), teleporterRequest)
if err := client.auth.verify(); err != nil {
return err
}
var requestBody bytes.Buffer
writer := multipart.NewWriter(&requestBody)
fileWriter, _ := writer.CreateFormFile("file", "config.zip")
if _, err := io.Copy(fileWriter, bytes.NewReader(payload)); err != nil {
return err
}
if teleporterRequest != nil {
jsonData, err := json.Marshal(teleporterRequest)
if err != nil {
return err
}
if err = writer.WriteField("import", string(jsonData)); err != nil {
return err
}
}
if err := writer.Close(); err != nil {
return err
}
req, err := http.NewRequest("POST", client.ApiPath("teleporter"), &requestBody)
if err != nil {
return err
}
req.Header.Set("sid", client.auth.sid)
req.Header.Set("Content-Type", writer.FormDataContentType())
req.Header.Set("User-Agent", userAgent)
response, err := httpClient.Do(req)
if err != nil {
return err
}
if err := successfulHttpStatus(response.StatusCode); err != nil {
return err
}
return nil
}
func (client *client) GetConfig() (configResponse *model.ConfigResponse, err error) {
log.Debug().Msgf("Get config, client %s", client.String())
if err := client.auth.verify(); err != nil {
return configResponse, err
}
req, err := http.NewRequest("GET", client.ApiPath("config"), nil)
if err != nil {
return configResponse, err
}
req.Header.Set("sid", client.auth.sid)
req.Header.Set("User-Agent", userAgent)
response, err := httpClient.Do(req)
if err != nil {
return configResponse, err
}
if err := successfulHttpStatus(response.StatusCode); err != nil {
return configResponse, err
}
body, err := io.ReadAll(response.Body)
if err != nil {
return configResponse, err
}
if err := json.Unmarshal(body, &configResponse); err != nil {
return configResponse, err
}
return configResponse, err
}
func (client *client) PatchConfig(patchRequest *model.PatchConfigRequest) error {
log.Debug().Msgf("Patch config, client %s", client.String())
if err := client.auth.verify(); err != nil {
return err
}
reqBytes, err := json.Marshal(patchRequest)
if err != nil {
return err
}
req, err := http.NewRequest("PATCH", client.ApiPath("config"), bytes.NewReader(reqBytes))
if err != nil {
return err
}
req.Header.Set("sid", client.auth.sid)
req.Header.Set("User-Agent", userAgent)
response, err := httpClient.Do(req)
if err != nil {
return err
}
if err := successfulHttpStatus(response.StatusCode); err != nil {
return err
}
return err
}
func (client *client) String() string {
return client.PiHole.Url.String()
}
func (client *client) ApiPath(target string) string {
return client.PiHole.Url.JoinPath("api", target).String()
}
func successfulHttpStatus(statusCode int) error {
if statusCode >= 200 && statusCode <= 299 {
return nil
}
return fmt.Errorf("unexpected status code: %d", statusCode)
}
+179
View File
@@ -0,0 +1,179 @@
package pihole
import (
"context"
"fmt"
"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"
"testing"
"time"
)
const (
dockerImage string = "pihole/pihole:development-v6"
apiPassword string = "test"
)
var (
container = startContainer()
)
type ClientTestSuite struct {
suite.Suite
client Client
}
func (suite *ClientTestSuite) SetupTest() {
client := createClient(container)
err := client.Authenticate()
require.NoError(suite.T(), err)
suite.client = client
}
func TestClientIntegration(t *testing.T) {
suite.Run(t, new(ClientTestSuite))
}
func (suite *ClientTestSuite) TestClient_Authenticate() {
err := suite.client.Authenticate()
assert.NoError(suite.T(), err)
}
func (suite *ClientTestSuite) TestClient_DeleteSession() {
err := suite.client.DeleteSession()
assert.NoError(suite.T(), err)
}
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() {
payload, err := suite.client.GetTeleporter()
assert.NoError(suite.T(), err)
assert.NotNil(suite.T(), payload)
}
func (suite *ClientTestSuite) TestClient_PostTeleporter() {
payload, _ := suite.client.GetTeleporter()
err := suite.client.PostTeleporter(payload, &model.PostTeleporterRequest{
Config: true,
DHCPLeases: true,
Gravity: model.PostGravityRequest{
Group: true,
Adlist: true,
AdlistByGroup: true,
Domainlist: true,
DomainlistByGroup: true,
Client: true,
ClientByGroup: true,
},
})
assert.NoError(suite.T(), err)
}
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() {
request := model.PatchConfigRequest{
Config: model.PatchConfig{
DNS: nil,
DHCP: nil,
NTP: nil,
Resolver: nil,
Database: nil,
Misc: nil,
Debug: nil,
}}
err := suite.client.PatchConfig(&request)
assert.NoError(suite.T(), err)
}
func TestClient_String(t *testing.T) {
piHole := model.NewPiHole("http://asdfasdf.com:1234", apiPassword)
s := NewClient(piHole).String()
assert.Equal(t, "http://asdfasdf.com:1234", s)
}
func TestClient_ApiPath(t *testing.T) {
piHole := model.NewPiHole("http://asdfasdf.com:1234", apiPassword)
c := NewClient(piHole)
url := c.String()
path := c.ApiPath("testing")
expectedPath := fmt.Sprintf("%s/api/testing", url)
assert.Equal(t, expectedPath, path)
}
func Test_auth_verify(t *testing.T) {
a := auth{
sid: "",
csrf: "",
validity: 0,
valid: false,
}
assert.Error(t, a.verify())
a.valid = true
assert.Error(t, a.verify())
a.sid = "sid123"
assert.Error(t, a.verify())
a.validity = 1
assert.NoError(t, a.verify())
}
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 {
apiPort, err := container.MappedPort(context.Background(), "80/tcp")
if err != nil {
panic(err)
}
host := fmt.Sprintf("http://localhost:%s", apiPort.Port())
return NewClient(model.NewPiHole(host, "test"))
}
+44
View File
@@ -0,0 +1,44 @@
package model
import (
"fmt"
"github.com/rs/zerolog/log"
"net/url"
"strings"
)
type PiHole struct {
Url *url.URL
Password string
}
func NewPiHole(host, password string) PiHole {
u, err := url.Parse(host)
if err != nil {
log.Error().Err(err).Msgf("Error parsing host %s", host)
}
return PiHole{
Url: u,
Password: password,
}
}
func (piHole *PiHole) Decode(value string) error {
split := strings.Split(value, "|")
if len(split) != 2 {
return fmt.Errorf("invalid pihole format")
}
res, err := url.Parse(split[0])
if err != nil {
return fmt.Errorf("failed to parse url: %s", err)
}
*piHole = PiHole{
Url: res,
Password: split[1],
}
return nil
}
+21
View File
@@ -0,0 +1,21 @@
package model
import (
"github.com/stretchr/testify/assert"
"net/url"
"testing"
)
func TestPiHole_Decode(t *testing.T) {
ph := PiHole{}
err := ph.Decode("http://localhost:1337|asdfasdf")
assert.NoError(t, err)
expectedUrl, err := url.Parse("http://localhost:1337")
assert.NoError(t, err)
assert.Equal(t, expectedUrl, ph.Url)
assert.Equal(t, "asdfasdf", ph.Password)
}
+35
View File
@@ -0,0 +1,35 @@
package model
type AuthRequest struct {
Password string `json:"password"`
}
type PostGravityRequest struct {
Group bool `json:"group"`
Adlist bool `json:"adlist"`
AdlistByGroup bool `json:"adlist_by_group"`
Domainlist bool `json:"domainlist"`
DomainlistByGroup bool `json:"domainlist_by_group"`
Client bool `json:"client"`
ClientByGroup bool `json:"client_by_group"`
}
type PostTeleporterRequest struct {
Config bool `json:"config"`
DHCPLeases bool `json:"dhcp_leases"`
Gravity PostGravityRequest `json:"gravity"`
}
type PatchConfig struct {
DNS map[string]interface{} `json:"dns"`
DHCP map[string]interface{} `json:"dhcp"`
NTP map[string]interface{} `json:"ntp"`
Resolver map[string]interface{} `json:"resolver"`
Database map[string]interface{} `json:"database"`
Misc map[string]interface{} `json:"misc"`
Debug map[string]interface{} `json:"debug"`
}
type PatchConfigRequest struct {
Config PatchConfig `json:"config"`
}
+60
View File
@@ -0,0 +1,60 @@
package model
type AuthResponse struct {
Session struct {
Valid bool `json:"valid"`
Totp bool `json:"totp"`
Sid string `json:"sid"`
Csrf string `json:"csrf"`
Validity int `json:"validity"`
Message string `json:"message"`
} `json:"session"`
}
type VersionResponse struct {
Version struct {
Core struct {
Local struct {
Branch string `json:"branch"`
Version string `json:"version"`
Hash string `json:"hash"`
} `json:"local"`
Remote struct {
Version string `json:"version"`
Hash string `json:"hash"`
} `json:"remote"`
} `json:"core"`
Web struct {
Local struct {
Branch string `json:"branch"`
Version string `json:"version"`
Hash string `json:"hash"`
} `json:"local"`
Remote struct {
Version string `json:"version"`
Hash string `json:"hash"`
} `json:"remote"`
} `json:"web"`
Ftl struct {
Local struct {
Branch string `json:"branch"`
Version string `json:"version"`
Hash string `json:"hash"`
Date string `json:"date"`
} `json:"local"`
Remote struct {
Version string `json:"version"`
Hash string `json:"hash"`
} `json:"remote"`
} `json:"ftl"`
Docker struct {
Local string `json:"local"`
Remote string `json:"remote"`
} `json:"docker"`
} `json:"version"`
Took float64 `json:"took"`
}
type ConfigResponse struct {
Config map[string]interface{} `json:"config"`
}
+67
View File
@@ -0,0 +1,67 @@
package service
import (
"github.com/lovelaze/nebula-sync/internal/config"
"github.com/lovelaze/nebula-sync/internal/pihole"
"github.com/lovelaze/nebula-sync/internal/sync"
"github.com/lovelaze/nebula-sync/version"
"github.com/robfig/cron/v3"
"github.com/rs/zerolog/log"
)
type Service struct {
target sync.Target
conf config.Config
}
func NewService(conf config.Config) *Service {
primary := pihole.NewClient(conf.Primary)
var rs []pihole.Client
for _, replica := range conf.Replicas {
rs = append(rs, pihole.NewClient(replica))
}
return &Service{
target: sync.NewTarget(primary, rs),
conf: conf,
}
}
func (service *Service) Run() {
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)
} else {
service.startCron(func() {
service.doSync(service.target)
})
}
}
func (service *Service) doSync(t sync.Target) {
var err error
if service.conf.FullSync {
err = t.FullSync()
} else {
err = t.ManualSync(service.conf.SyncSettings)
}
if err != nil {
log.Error().Err(err).Msgf("Sync failed")
return
}
log.Info().Msg("Sync complete")
}
func (service *Service) startCron(cmd func()) {
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)
}
cron.Run()
}
+48
View File
@@ -0,0 +1,48 @@
package service
import (
"github.com/lovelaze/nebula-sync/internal/config"
syncmock "github.com/lovelaze/nebula-sync/internal/mocks/sync"
"github.com/lovelaze/nebula-sync/internal/pihole/model"
"testing"
)
func TestRun_full(t *testing.T) {
conf := config.Config{
Primary: model.PiHole{},
Replicas: []model.PiHole{},
FullSync: true,
Cron: nil,
SyncSettings: nil,
}
target := syncmock.NewTarget(t)
target.On("FullSync").Return(nil)
service := Service{
target: target,
conf: conf,
}
service.Run()
}
func TestRun_manual(t *testing.T) {
conf := config.Config{
Primary: model.PiHole{},
Replicas: []model.PiHole{},
FullSync: false,
Cron: nil,
SyncSettings: nil,
}
target := syncmock.NewTarget(t)
target.On("ManualSync", (*config.SyncSettings)(nil)).Return(nil)
service := Service{
target: target,
conf: conf,
}
service.Run()
}
+177
View File
@@ -0,0 +1,177 @@
package sync
import (
"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"
)
type Target interface {
FullSync() error
ManualSync(syncSettings *config.SyncSettings) error
}
type target struct {
Primary pihole.Client
Replicas []pihole.Client
}
func NewTarget(primary pihole.Client, replicas []pihole.Client) Target {
return &target{
Primary: primary,
Replicas: replicas,
}
}
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")
}
if err := target.syncTeleporters(nil); err != nil {
return errors.Wrap(err, "sync Teleporters failed")
}
if err := target.deleteSessions(); err != nil {
return errors.Wrap(err, "delete sessions failed")
}
return nil
}
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")
}
if err := target.syncTeleporters(syncSettings.Gravity); err != nil {
return errors.Wrap(err, "sync Teleporters failed")
}
if err := target.syncConfigs(syncSettings.Config); err != nil {
return errors.Wrap(err, "sync configs failed")
}
if err := target.deleteSessions(); err != nil {
return errors.Wrap(err, "delete sessions failed")
}
return nil
}
func (target *target) authenticate() (err error) {
log.Info().Msg("Authenticating clients...")
if err := target.Primary.Authenticate(); err != nil {
return err
}
for _, replica := range target.Replicas {
if err := replica.Authenticate(); err != nil {
return err
}
}
return err
}
func (target *target) deleteSessions() (err error) {
log.Info().Msg("Invalidating sessions...")
if err := target.Primary.DeleteSession(); err != nil {
return err
}
for _, replica := range target.Replicas {
if err := replica.DeleteSession(); err != nil {
return err
}
}
return err
}
func (target *target) syncTeleporters(manualGravity *config.ManualGravity) error {
log.Info().Msg("Syncing Teleporters...")
conf, err := target.Primary.GetTeleporter()
if err != nil {
return err
}
var teleporterRequest *model.PostTeleporterRequest = nil
if manualGravity != nil {
teleporterRequest = createPostTeleporterRequest(manualGravity)
}
for _, replica := range target.Replicas {
if err := replica.PostTeleporter(conf, teleporterRequest); err != nil {
return err
}
}
return err
}
func (target *target) syncConfigs(manualConfig *config.ManualConfig) error {
configResponse, err := target.Primary.GetConfig()
if err != nil {
return err
}
configRequest := createPatchConfigRequest(manualConfig, configResponse)
for _, replica := range target.Replicas {
if err := replica.PatchConfig(configRequest); err != nil {
return err
}
}
return err
}
func createPatchConfigRequest(config *config.ManualConfig, configResponse *model.ConfigResponse) *model.PatchConfigRequest {
patchConfig := model.PatchConfig{}
if config.DNS {
patchConfig.DNS = configResponse.Config["dns"].(map[string]interface{})
}
if config.DHCP {
patchConfig.DHCP = configResponse.Config["dhcp"].(map[string]interface{})
}
if config.NTP {
patchConfig.NTP = configResponse.Config["ntp"].(map[string]interface{})
}
if config.Resolver {
patchConfig.Resolver = configResponse.Config["resolver"].(map[string]interface{})
}
if config.Database {
patchConfig.Database = configResponse.Config["database"].(map[string]interface{})
}
if config.Misc {
patchConfig.Misc = configResponse.Config["misc"].(map[string]interface{})
}
if config.Debug {
patchConfig.Debug = configResponse.Config["debug"].(map[string]interface{})
}
return &model.PatchConfigRequest{Config: patchConfig}
}
func createPostTeleporterRequest(gravity *config.ManualGravity) *model.PostTeleporterRequest {
return &model.PostTeleporterRequest{
Config: false,
DHCPLeases: gravity.DHCPLeases,
Gravity: model.PostGravityRequest{
Group: gravity.Group,
Adlist: gravity.Adlist,
AdlistByGroup: gravity.AdlistByGroup,
Domainlist: gravity.Domainlist,
DomainlistByGroup: gravity.DomainlistByGroup,
Client: gravity.Client,
ClientByGroup: gravity.ClientByGroup,
},
}
}
+251
View File
@@ -0,0 +1,251 @@
package sync
import (
"github.com/lovelaze/nebula-sync/internal/config"
piholemock "github.com/lovelaze/nebula-sync/internal/mocks/pihole"
"github.com/lovelaze/nebula-sync/internal/pihole"
"github.com/lovelaze/nebula-sync/internal/pihole/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"testing"
)
func TestTarget_FullSync(t *testing.T) {
primary := piholemock.NewClient(t)
replica := piholemock.NewClient(t)
target := NewTarget(primary, []pihole.Client{replica})
primary.
EXPECT().
Authenticate().
Times(1).
Return(nil)
replica.
EXPECT().
Authenticate().
Times(1).
Return(nil)
primary.
EXPECT().
GetTeleporter().
Times(1).
Return([]byte{}, nil)
replica.
EXPECT().
PostTeleporter(mock.Anything, mock.Anything).
Times(1).
Return(nil)
primary.
EXPECT().
DeleteSession().
Times(1).
Return(nil)
replica.
EXPECT().
DeleteSession().
Times(1).
Return(nil)
target.FullSync()
}
func TestTarget_ManualSync(t *testing.T) {
primary := piholemock.NewClient(t)
replica := piholemock.NewClient(t)
target := NewTarget(primary, []pihole.Client{replica})
settings := config.SyncSettings{
Gravity: &config.ManualGravity{
DHCPLeases: false,
Group: false,
Adlist: false,
AdlistByGroup: false,
Domainlist: false,
DomainlistByGroup: false,
Client: false,
ClientByGroup: false,
},
Config: &config.ManualConfig{
DNS: false,
DHCP: false,
NTP: false,
Resolver: false,
Database: false,
Webserver: false,
Files: false,
Misc: false,
Debug: false,
},
}
primary.
EXPECT().
Authenticate().
Times(1).
Return(nil)
replica.
EXPECT().
Authenticate().
Times(1).
Return(nil)
primary.
EXPECT().
GetTeleporter().
Times(1).
Return([]byte{}, nil)
replica.
EXPECT().
PostTeleporter(mock.Anything, mock.Anything).
Times(1).
Return(nil)
primary.
EXPECT().
GetConfig().
Times(1).
Return(&model.ConfigResponse{Config: make(map[string]interface{})}, nil)
replica.
EXPECT().
PatchConfig(mock.Anything).
Times(1).
Return(nil)
primary.
EXPECT().
DeleteSession().
Times(1).
Return(nil)
replica.
EXPECT().
DeleteSession().
Times(1).
Return(nil)
target.ManualSync(&settings)
}
func Test_target_authenticate(t *testing.T) {
primary := piholemock.NewClient(t)
replica := piholemock.NewClient(t)
target := target{
Primary: primary,
Replicas: []pihole.Client{replica},
}
primary.
EXPECT().
Authenticate().
Times(1).
Return(nil)
replica.
EXPECT().
Authenticate().
Times(1).
Return(nil)
err := target.authenticate()
assert.NoError(t, err)
}
func Test_target_deleteSessions(t *testing.T) {
primary := piholemock.NewClient(t)
replica := piholemock.NewClient(t)
target := target{
Primary: primary,
Replicas: []pihole.Client{replica},
}
primary.
EXPECT().
DeleteSession().
Times(1).
Return(nil)
replica.
EXPECT().
DeleteSession().
Times(1).
Return(nil)
err := target.deleteSessions()
assert.NoError(t, err)
}
func Test_target_syncTeleporters(t *testing.T) {
primary := piholemock.NewClient(t)
replica := piholemock.NewClient(t)
target := target{
Primary: primary,
Replicas: []pihole.Client{replica},
}
manualGravity := config.ManualGravity{
DHCPLeases: false,
Group: false,
Adlist: false,
AdlistByGroup: false,
Domainlist: false,
DomainlistByGroup: false,
Client: false,
ClientByGroup: false,
}
primary.
EXPECT().
GetTeleporter().
Times(1).
Return([]byte{}, nil)
replica.
EXPECT().
PostTeleporter([]byte{}, createPostTeleporterRequest(&manualGravity)).
Times(1).
Return(nil)
err := target.syncTeleporters(&manualGravity)
assert.NoError(t, err)
}
func Test_target_syncConfigs(t *testing.T) {
primary := piholemock.NewClient(t)
replica := piholemock.NewClient(t)
target := target{
Primary: primary,
Replicas: []pihole.Client{replica},
}
configResponse := model.ConfigResponse{Config: make(map[string]interface{})}
manualConfig := config.ManualConfig{
DNS: false,
DHCP: false,
NTP: false,
Resolver: false,
Database: false,
Webserver: false,
Files: false,
Misc: false,
Debug: false,
}
primary.
EXPECT().
GetConfig().
Times(1).
Return(&configResponse, nil)
replica.
EXPECT().
PatchConfig(createPatchConfigRequest(&manualConfig, &configResponse)).
Times(1).
Return(nil)
err := target.syncConfigs(&manualConfig)
assert.NoError(t, err)
}
+7
View File
@@ -0,0 +1,7 @@
package main
import "github.com/lovelaze/nebula-sync/cmd"
func main() {
cmd.Execute()
}
+3
View File
@@ -0,0 +1,3 @@
package version
const Version = "0.1.0"