Compare commits

..

62 Commits

Author SHA1 Message Date
Marc Brugger
64c922a1d8 feat: support request headers (#586) 2025-05-18 16:59:29 +02:00
bakito-renovate[bot]
905c783ca9 chore(deps): update module k8s.io/apimachinery to v0.33.1 (#585)
Co-authored-by: bakito-renovate[bot] <205501741+bakito-renovate[bot]@users.noreply.github.com>
2025-05-18 10:10:30 +02:00
bakito-renovate[bot]
4eac62e6ce chore(deps): update module sigs.k8s.io/controller-tools/cmd/controller-gen to v0.18.0 (#583)
Co-authored-by: bakito-renovate[bot] <205501741+bakito-renovate[bot]@users.noreply.github.com>
2025-05-12 07:18:01 +02:00
bakito
418409ac47 chore: re-generate mock and makefie deps 2025-05-05 20:16:27 +02:00
bakito-renovate[bot]
4895bfdc55 chore(deps): update module github.com/golangci/golangci-lint/v2/cmd/golangci-lint to v2.1.6 (#582)
Co-authored-by: bakito-renovate[bot] <205501741+bakito-renovate[bot]@users.noreply.github.com>
2025-05-05 07:26:23 +02:00
bakito-renovate[bot]
976902f861 chore(deps): update github.com/google/pprof digest to c008609 (#580)
Co-authored-by: bakito-renovate[bot] <205501741+bakito-renovate[bot]@users.noreply.github.com>
2025-05-03 09:05:28 +02:00
bakito-renovate[bot]
b06a1cfec0 chore(deps): update k8s.io/utils digest to 0f33e8f (#581)
Co-authored-by: bakito-renovate[bot] <205501741+bakito-renovate[bot]@users.noreply.github.com>
2025-05-03 09:05:13 +02:00
bakito-renovate[bot]
449fce9d6a chore(deps): update module github.com/goreleaser/goreleaser/v2 to v2.9.0 (#578)
Co-authored-by: bakito-renovate[bot] <205501741+bakito-renovate[bot]@users.noreply.github.com>
2025-05-02 21:16:37 +02:00
bakito-renovate[bot]
6b45caadfb chore(deps): update uber to v0.5.2 (#577)
Co-authored-by: bakito-renovate[bot] <205501741+bakito-renovate[bot]@users.noreply.github.com>
2025-04-30 00:04:50 +02:00
bakito-renovate[bot]
06b49cb810 chore(deps): update module github.com/go-faker/faker/v4 to v4.6.1 (#576)
Co-authored-by: bakito-renovate[bot] <205501741+bakito-renovate[bot]@users.noreply.github.com>
2025-04-27 08:28:35 +02:00
bakito
ae58133a0d chore: regenerate model 2025-04-26 15:23:37 +02:00
Marc Brugger
2b0bfb126e test: correct env var for test (#574) 2025-04-26 09:25:33 +02:00
Marc Brugger
97841e3f32 Correct env variable handling to not use unrelated variables (#571) 2025-04-26 09:01:30 +02:00
bakito-renovate[bot]
2fc2baf6f8 chore(deps): update module github.com/golangci/golangci-lint/v2/cmd/golangci-lint to v2.1.5 (#568)
Co-authored-by: bakito-renovate[bot] <205501741+bakito-renovate[bot]@users.noreply.github.com>
2025-04-25 07:54:58 +02:00
bakito-renovate[bot]
06355a71da chore(deps): update k8s to v0.33.0 (#567)
Co-authored-by: bakito-renovate[bot] <205501741+bakito-renovate[bot]@users.noreply.github.com>
2025-04-24 18:58:59 +02:00
Marc Brugger
9782c430af feat: implement sync metrics (#562) 2025-04-23 23:59:27 +02:00
bakito-renovate[bot]
cafdf14008 chore(deps): update indirect go dependencies (#563)
Co-authored-by: bakito-renovate[bot] <205501741+bakito-renovate[bot]@users.noreply.github.com>
2025-04-23 07:25:59 +02:00
bakito-renovate[bot]
e7569de5c6 chore(deps): update dependency adguardteam/adguardhome to v0.107.61 (#566)
Co-authored-by: bakito-renovate[bot] <205501741+bakito-renovate[bot]@users.noreply.github.com>
2025-04-23 07:20:21 +02:00
Sy
2b69fe15ed docs: Add Home Assistant paragraph to README (#564)
Adding guidance to help Home Assistant AdGuard Home Add-on users enable syncing.
2025-04-20 17:41:17 +02:00
Marc Brugger
bb1f2d02c5 fix: ignore empty config file in config validation (#561) 2025-04-16 12:51:27 +02:00
bakito-renovate[bot]
3bd093ceb9 chore(deps): update indirect go dependencies (#548)
Co-authored-by: bakito-renovate[bot] <205501741+bakito-renovate[bot]@users.noreply.github.com>
2025-04-15 19:33:48 +02:00
bakito-renovate[bot]
05c0eab4a8 chore(deps): update module github.com/golangci/golangci-lint/v2/cmd/golangci-lint to v2.1.2 (#558)
Co-authored-by: bakito-renovate[bot] <205501741+bakito-renovate[bot]@users.noreply.github.com>
2025-04-15 19:33:38 +02:00
bakito-renovate[bot]
57745f2953 chore(deps): update module github.com/prometheus/client_golang to v1.22.0 (#553)
Co-authored-by: bakito-renovate[bot] <205501741+bakito-renovate[bot]@users.noreply.github.com>
2025-04-15 18:19:04 +02:00
bakito-renovate[bot]
9f5ac14aa1 chore(deps): update module github.com/golangci/golangci-lint/v2/cmd/golangci-lint to v2.1.1 (#554)
Co-authored-by: bakito-renovate[bot] <205501741+bakito-renovate[bot]@users.noreply.github.com>
2025-04-15 18:18:48 +02:00
bakito-renovate[bot]
8f2aa58fe8 chore(deps): update module mvdan.cc/gofumpt to v0.8.0 (#555)
Co-authored-by: bakito-renovate[bot] <205501741+bakito-renovate[bot]@users.noreply.github.com>
2025-04-15 18:18:31 +02:00
bakito-renovate[bot]
01e23d3b69 chore(deps): update dependency adguardteam/adguardhome to v0.107.60 (#557)
Co-authored-by: bakito-renovate[bot] <205501741+bakito-renovate[bot]@users.noreply.github.com>
2025-04-15 18:18:18 +02:00
Marc Brugger
c2da171c85 Update bug_report.yaml 2025-04-14 21:03:57 +02:00
bakito-renovate[bot]
b44dd16ad8 chore(deps): update module github.com/onsi/ginkgo/v2 to v2.23.4 (#549)
Co-authored-by: bakito-renovate[bot] <205501741+bakito-renovate[bot]@users.noreply.github.com>
2025-04-09 04:00:36 +00:00
bakito-renovate[bot]
ba00016be8 chore(deps): update uber to v0.5.1 (#552)
Co-authored-by: bakito-renovate[bot] <205501741+bakito-renovate[bot]@users.noreply.github.com>
2025-04-08 09:21:20 +00:00
Marc Brugger
e301f483c6 docs: add env variables to readme (#547) 2025-04-05 21:44:46 +02:00
bakito-renovate[bot]
ebf25a8ddf chore(deps): update indirect go dependencies (#546)
Co-authored-by: bakito-renovate[bot] <205501741+bakito-renovate[bot]@users.noreply.github.com>
2025-04-05 21:39:59 +02:00
bakito-renovate[bot]
56d6644194 chore(deps): update module github.com/goreleaser/goreleaser/v2 to v2.8.2 (#544)
Co-authored-by: bakito-renovate[bot] <205501741+bakito-renovate[bot]@users.noreply.github.com>
2025-04-05 11:22:15 +01:00
bakito-renovate[bot]
51401dac81 chore(deps): update module github.com/onsi/gomega to v1.37.0 (#545)
Co-authored-by: bakito-renovate[bot] <205501741+bakito-renovate[bot]@users.noreply.github.com>
2025-04-04 04:00:44 +00:00
bakito-renovate[bot]
18b07898ea chore(deps): update indirect go dependencies (#543)
Co-authored-by: bakito-renovate[bot] <205501741+bakito-renovate[bot]@users.noreply.github.com>
2025-03-30 13:26:05 +02:00
Marc Brugger
223739f4a7 chore(deps): update module github.com/golangci/golangci-lint/v2/cmd/golangci-lint to v2.0.2 (#539)
Co-authored-by: Renovate Bot <renovate@whitesourcesoftware.com>
2025-03-25 22:48:39 +01:00
Marc Brugger
02ff6a11f0 feat: implement stricter lint rules git golanci-lint v2 (#538)
* feat: implement stricter lint rules git golanci-lint v2

* fix lint issues

* fix lint issues
2025-03-25 21:30:22 +01:00
Marc Brugger
2bff464f65 Correct metrics indent 2025-03-23 15:24:30 +01:00
Marc Brugger
8fe0fe27ea add unraid disclaimer (#535) 2025-03-23 15:07:31 +01:00
Marc Brugger
5c00caa296 chore(deps): update dependency adguardteam/adguardhome to v0.107.59 (#527)
Co-authored-by: Renovate Bot <renovate@whitesourcesoftware.com>
2025-03-22 18:16:35 +01:00
Marc Brugger
344337a3d5 chore(deps): update k8s.io/utils digest to 1f6e0b7 (#530)
Co-authored-by: Renovate Bot <renovate@whitesourcesoftware.com>
2025-03-22 18:16:25 +01:00
Marc Brugger
e1287564c3 chore(deps): update indirect go dependencies (#533)
Co-authored-by: Renovate Bot <renovate@whitesourcesoftware.com>
2025-03-22 18:16:11 +01:00
Marc Brugger
8f61de83bf chore(deps): update onsi (#528)
Co-authored-by: Renovate Bot <renovate@whitesourcesoftware.com>
2025-03-22 09:08:24 +01:00
bakito
f3810cf241 test: add test layout 2025-03-18 20:37:28 +01:00
Marc Brugger
3d385f409e chore(deps): update module github.com/golangci/golangci-lint/cmd/golangci-lint to v1.64.8 (#526)
Co-authored-by: Renovate Bot <renovate@whitesourcesoftware.com>
2025-03-18 06:38:12 +01:00
Marc Brugger
19bdd47b3c chore: update depenencies (#525) 2025-03-16 11:29:37 +01:00
Marc Brugger
32c728ccf8 fix: handle small passwords in mask function #522 (#523) 2025-03-13 19:10:37 +01:00
dependabot[bot]
327d67c9ef build(deps): bump golang.org/x/net from 0.35.0 to 0.36.0 (#519)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.35.0 to 0.36.0.
- [Commits](https://github.com/golang/net/compare/v0.35.0...v0.36.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-13 07:07:32 +01:00
Marc Brugger
edd7f82351 chore(deps): update k8s to v0.32.3 (#520)
Co-authored-by: Renovate Bot <renovate@whitesourcesoftware.com>
2025-03-13 07:07:21 +01:00
Marc Brugger
012d43b950 chore(deps): update module github.com/goreleaser/goreleaser/v2 to v2.8.0 (#521)
Co-authored-by: Renovate Bot <renovate@whitesourcesoftware.com>
2025-03-13 07:07:06 +01:00
Marc Brugger
e2298f6f1e chore(deps): update module github.com/golangci/golangci-lint/cmd/golangci-lint to v1.64.7 (#518)
Co-authored-by: Renovate Bot <renovate@whitesourcesoftware.com>
2025-03-12 22:45:48 +01:00
Marc Brugger
18350f94a6 test: add metrics test (#517) 2025-03-09 17:38:08 +01:00
bakito
1a9d5df5cb test: use files instead of const for test expects 2025-03-09 17:09:30 +01:00
bakito
8ef1eb8c2a feat: print runtime info in config only mode 2025-03-09 10:53:38 +01:00
Marc Brugger
7c2018acbc test: add print config tests (#508) 2025-03-06 07:28:25 +01:00
Marc Brugger
2079f6a3eb chore(deps): update module github.com/onsi/ginkgo/v2 to v2.23.0 (#515)
Co-authored-by: Renovate Bot <renovate@whitesourcesoftware.com>
2025-03-06 07:22:18 +01:00
Marc Brugger
d9419446bf chore(deps): update module golang.org/x/mod to v0.24.0 (#516)
Co-authored-by: Renovate Bot <renovate@whitesourcesoftware.com>
2025-03-06 07:22:03 +01:00
Marc Brugger
c0cbccb63c chore(deps): update module github.com/prometheus/client_golang to v1.21.1 (#514)
Co-authored-by: Renovate Bot <renovate@whitesourcesoftware.com>
2025-03-05 07:11:51 +01:00
Marc Brugger
5444dda08a chore(deps): update module github.com/golangci/golangci-lint/cmd/golangci-lint to v1.64.6 (#512)
Co-authored-by: Renovate Bot <renovate@whitesourcesoftware.com>
2025-03-03 07:11:01 +01:00
Marc Brugger
224617aae6 test: scan released windows files with VirusTotal (#510) 2025-02-28 20:36:35 +01:00
bakito
c941c8a100 test: add virustotal scan for windows files 2025-02-27 21:17:51 +01:00
Stephan
07a25cd094 docs: Add unraid section to README (#509)
Added comment for running in Unraid
2025-02-26 07:54:43 +01:00
bakito
54d98c10fe extend print config comments #507 2025-02-25 21:34:37 +01:00
70 changed files with 1936 additions and 893 deletions

View File

@@ -58,7 +58,7 @@ body:
- type: textarea
id: logs
attributes:
label: Relevant log output
label: Relevant DEBUG log output
description: |
Please copy and paste any relevant **debug** log output. This will be automatically formatted into code, so no need for backticks.
Enable debug logs by defining the following environment variable `LOG_LEVEL=debug`.

View File

@@ -22,14 +22,16 @@ jobs:
with:
go-version-file: "go.mod"
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
skip-cache: true
- name: Lint
run: make lint
test:
name: test
runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- name: Check out code into the Go module directory
@@ -47,6 +49,7 @@ jobs:
run: make test-ci
- name: Send coverage
if: runner.os == 'Linux'
uses: shogo82148/actions-goveralls@v1
with:
path-to-profile: coverage.out

16
.github/workflows/virustotal.yaml vendored Normal file
View File

@@ -0,0 +1,16 @@
name: Scan GitHub Release with VirusTotal
on:
release:
types: [released]
jobs:
scan_release:
runs-on: ubuntu-latest
steps:
- name: Analyze Build Assets
uses: bakito/virustotal-action@main
with:
release_name: ${{github.event.release.tag_name}}
vt_api_key: ${{secrets.VT_API_KEY}}

209
.golangci.yaml Normal file
View File

@@ -0,0 +1,209 @@
version: '2'
linters:
enable:
- asciicheck
- bidichk
- bodyclose
- canonicalheader
- containedctx
- copyloopvar
- decorder
- dogsled
- dupword
- durationcheck
- err113
- errname
- errorlint
- exptostd
- fatcontext
- forcetypeassert
- gocheckcompilerdirectives
- gochecksumtype
- gocritic
- godot
- gomodguard
- goprintffuncname
- gosmopolitan
- grouper
- iface
- importas
- inamedparam
- interfacebloat
- intrange
- loggercheck
- makezero
- mirror
- misspell
- nilerr
- nilnesserr
- noctx
- nolintlint
- nosprintfhostport
- perfsprint
- predeclared
- promlinter
- protogetter
- reassign
- revive
- rowserrcheck
- sloglint
- spancheck
- sqlclosecheck
- staticcheck
- tagalign
- testableexamples
- testifylint
- thelper
- unconvert
- unparam
- usestdlibvars
- usetesting
- wastedassign
- whitespace
- zerologlint
disable:
- asasalint
- contextcheck
- cyclop
- depguard
- dupl
- errchkjson
- exhaustive
- exhaustruct
- forbidigo
- funlen
- ginkgolinter
- gochecknoglobals
- gochecknoinits
- gocognit
- goconst
- gocyclo
- godox
- goheader
- gomoddirectives
- gosec
- ireturn
- lll
- maintidx
- musttag
- nakedret
- nestif
- nilnil
- nlreturn
- nonamedreturns
- paralleltest
- prealloc
- recvcheck
- tagliatelle
- testpackage
- tparallel
- varnamelen
- wrapcheck
- wsl
settings:
gocritic:
enable-all: true
disabled-checks:
- emptyFallthrough
- hugeParam
- rangeValCopy
- unnamedResult
- whyNoLint
govet:
disable:
- fieldalignment
- shadow
enable-all: true
misspell:
locale: US
revive:
enable-all-rules: true
rules:
- name: add-constant
disabled: true
- name: cognitive-complexity
disabled: true
- name: cyclomatic
disabled: true
- name: deep-exit
disabled: true
- name: dot-imports
severity: warning
disabled: false
exclude: [""]
arguments:
- allowedPackages: ["github.com/onsi/ginkgo/v2", "github.com/onsi/gomega"]
- name: empty-block
disabled: true
- name: exported
disabled: true
- name: filename-format
arguments:
- ^[a-z][-0-9_a-z]*(?:\.gen)?\.go$
- name: flag-parameter
disabled: true
- name: function-length
disabled: true
- name: function-result-limit
disabled: true
- name: import-shadowing
disabled: true
- name: line-length-limit
disabled: true
- name: max-control-nesting
disabled: true
- name: max-public-structs
disabled: true
- name: nested-structs
disabled: true
- name: package-comments
disabled: true
- name: unused-parameter
disabled: true
- name: unused-receiver
disabled: true
staticcheck:
checks:
- 'all'
- '-ST1000'
exclusions:
generated: lax
presets:
- common-false-positives
- legacy
- std-error-handling
rules:
- linters:
- err113
text: do not define dynamic errors, use wrapped static errors instead
- linters:
- forbidigo
path: ^internal/cmds/
- linters:
- forcetypeassert
path: _test\.go$
- linters:
- forbidigo
path: assets/scripts/generate-commit.go
formatters:
enable:
- gci
- gofmt
- gofumpt
- goimports
- golines
settings:
gci:
sections:
- standard
- default
- prefix(github.com/bakito/adguardhome-sync)
gofumpt:
module-path: github.com/bakito/adguardhome-sync
extra-rules: true
goimports:
local-prefixes:
- github.com/bakito/adguardhome-sync
golines:
max-len: 128
tab-len: 4

View File

@@ -1,37 +0,0 @@
run:
timeout: 5m
linters:
enable:
- asciicheck
- bodyclose
- dogsled
- durationcheck
- errcheck
- errorlint
- gci
- gofmt
- gofumpt
- goimports
- gosec
- gosimple
- govet
- importas
- ineffassign
- misspell
- nakedret
- nolintlint
- staticcheck
- unconvert
- unparam
- unused
linters-settings:
gosec:
# Exclude generated files
exclude-generated: true
excludes:
- G601 # not applicable in go 1.22 anymore
gofmt:
# simplify code: gofmt with `-s` option, true by default
simplify: true

View File

@@ -6,10 +6,10 @@ TB_LOCALDIR ?= $(shell which cygpath > /dev/null 2>&1 && cygpath -m $$(pwd) || p
## Location to install dependencies to
TB_LOCALBIN ?= $(TB_LOCALDIR)/bin
$(TB_LOCALBIN):
mkdir -p $(TB_LOCALBIN)
if [ ! -e $(TB_LOCALBIN) ]; then mkdir -p $(TB_LOCALBIN); fi
## Tool Binaries
TB_DEEPCOPY_GEN ?= $(TB_LOCALBIN)/deepcopy-gen
TB_CONTROLLER_GEN ?= $(TB_LOCALBIN)/controller-gen
TB_GINKGO ?= $(TB_LOCALBIN)/ginkgo
TB_GOFUMPT ?= $(TB_LOCALBIN)/gofumpt
TB_GOLANGCI_LINT ?= $(TB_LOCALBIN)/golangci-lint
@@ -20,28 +20,28 @@ TB_OAPI_CODEGEN ?= $(TB_LOCALBIN)/oapi-codegen
TB_SEMVER ?= $(TB_LOCALBIN)/semver
## Tool Versions
# renovate: packageName=k8s.io/code-generator/cmd/deepcopy-gen
TB_DEEPCOPY_GEN_VERSION ?= v0.32.2
# renovate: packageName=sigs.k8s.io/controller-tools/cmd/controller-gen
TB_CONTROLLER_GEN_VERSION ?= v0.18.0
# renovate: packageName=mvdan.cc/gofumpt
TB_GOFUMPT_VERSION ?= v0.7.0
# renovate: packageName=github.com/golangci/golangci-lint/cmd/golangci-lint
TB_GOLANGCI_LINT_VERSION ?= v1.64.5
TB_GOFUMPT_VERSION ?= v0.8.0
# renovate: packageName=github.com/golangci/golangci-lint/v2
TB_GOLANGCI_LINT_VERSION ?= v2.1.6
# renovate: packageName=github.com/segmentio/golines
TB_GOLINES_VERSION ?= v0.12.2
# renovate: packageName=github.com/goreleaser/goreleaser/v2
TB_GORELEASER_VERSION ?= v2.7.0
TB_GORELEASER_VERSION ?= v2.9.0
# renovate: packageName=go.uber.org/mock/mockgen
TB_MOCKGEN_VERSION ?= v0.5.0
# renovate: packageName=github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen
TB_MOCKGEN_VERSION ?= v0.5.2
# renovate: packageName=github.com/oapi-codegen/oapi-codegen/v2
TB_OAPI_CODEGEN_VERSION ?= v2.4.1
# renovate: packageName=github.com/bakito/semver
TB_SEMVER_VERSION ?= v1.1.3
## Tool Installer
.PHONY: tb.deepcopy-gen
tb.deepcopy-gen: $(TB_DEEPCOPY_GEN) ## Download deepcopy-gen locally if necessary.
$(TB_DEEPCOPY_GEN): $(TB_LOCALBIN)
test -s $(TB_LOCALBIN)/deepcopy-gen || GOBIN=$(TB_LOCALBIN) go install k8s.io/code-generator/cmd/deepcopy-gen@$(TB_DEEPCOPY_GEN_VERSION)
.PHONY: tb.controller-gen
tb.controller-gen: $(TB_CONTROLLER_GEN) ## Download controller-gen locally if necessary.
$(TB_CONTROLLER_GEN): $(TB_LOCALBIN)
test -s $(TB_LOCALBIN)/controller-gen || GOBIN=$(TB_LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(TB_CONTROLLER_GEN_VERSION)
.PHONY: tb.ginkgo
tb.ginkgo: $(TB_GINKGO) ## Download ginkgo locally if necessary.
$(TB_GINKGO): $(TB_LOCALBIN)
@@ -53,7 +53,7 @@ $(TB_GOFUMPT): $(TB_LOCALBIN)
.PHONY: tb.golangci-lint
tb.golangci-lint: $(TB_GOLANGCI_LINT) ## Download golangci-lint locally if necessary.
$(TB_GOLANGCI_LINT): $(TB_LOCALBIN)
test -s $(TB_LOCALBIN)/golangci-lint || GOBIN=$(TB_LOCALBIN) go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(TB_GOLANGCI_LINT_VERSION)
test -s $(TB_LOCALBIN)/golangci-lint || GOBIN=$(TB_LOCALBIN) go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(TB_GOLANGCI_LINT_VERSION)
.PHONY: tb.golines
tb.golines: $(TB_GOLINES) ## Download golines locally if necessary.
$(TB_GOLINES): $(TB_LOCALBIN)
@@ -79,7 +79,7 @@ $(TB_SEMVER): $(TB_LOCALBIN)
.PHONY: tb.reset
tb.reset:
@rm -f \
$(TB_LOCALBIN)/deepcopy-gen \
$(TB_LOCALBIN)/controller-gen \
$(TB_LOCALBIN)/ginkgo \
$(TB_LOCALBIN)/gofumpt \
$(TB_LOCALBIN)/golangci-lint \
@@ -93,9 +93,9 @@ tb.reset:
.PHONY: tb.update
tb.update: tb.reset
toolbox makefile --renovate -f $(TB_LOCALDIR)/Makefile \
k8s.io/code-generator/cmd/deepcopy-gen@github.com/kubernetes/code-generator \
sigs.k8s.io/controller-tools/cmd/controller-gen@github.com/kubernetes-sigs/controller-tools \
mvdan.cc/gofumpt@github.com/mvdan/gofumpt \
github.com/golangci/golangci-lint/cmd/golangci-lint \
github.com/golangci/golangci-lint/v2/cmd/golangci-lint \
github.com/segmentio/golines \
github.com/goreleaser/goreleaser/v2 \
go.uber.org/mock/mockgen@github.com/uber-go/mock \

View File

@@ -9,10 +9,11 @@ lint: tb.golangci-lint
tidy:
go mod tidy
generate: tb.deepcopy-gen
generate: model mocks deepcopy-gen
deepcopy-gen: tb.controller-gen
@mkdir -p ./tmp
@touch ./tmp/deepcopy-gen-boilerplate.go.txt
$(TB_DEEPCOPY_GEN) --go-header-file ./tmp/deepcopy-gen-boilerplate.go.txt --bounding-dirs ./pkg/types
$(TB_CONTROLLER_GEN) paths=./pkg/types object
fmt: tb.golines tb.gofumpt
$(TB_GOLINES) --base-formatter="$(TB_GOFUMPT)" --max-len=120 --write-output .
@@ -20,6 +21,9 @@ fmt: tb.golines tb.gofumpt
# Run tests
test: generate fmt lint test-ci
fuzz:
go test -fuzz=FuzzMask -v ./pkg/types/ -fuzztime=60s
# Run ci tests
test-ci: mocks tidy tb.ginkgo
$(TB_GINKGO) --cover --coverprofile coverage.out.tmp ./...
@@ -39,6 +43,7 @@ test-release: tb.goreleaser
$(TB_GORELEASER) --skip=publish --snapshot --clean
start-replica:
docker rm -f adguardhome-replica
docker run --pull always --name adguardhome-replica -p 9091:3000 --rm adguard/adguardhome:latest
# docker run --pull always --name adguardhome-replica -p 9090:80 -p 9091:3000 --rm adguard/adguardhome:v0.107.13
@@ -46,6 +51,7 @@ copy-replica-config:
docker cp adguardhome-replica:/opt/adguardhome/conf/AdGuardHome.yaml tmp/AdGuardHome.yaml
start-replica2:
docker rm -f adguardhome-replica2
docker run --pull always --name adguardhome-replica2 -p 9093:3000 --rm adguard/adguardhome:latest
# docker run --pull always --name adguardhome-replica -p 9090:80 -p 9091:3000 --rm adguard/adguardhome:v0.107.13
@@ -68,7 +74,7 @@ kind-test:
@./testdata/e2e/bin/install-chart.sh
# renovate: packageName=AdguardTeam/AdGuardHome
ADGUARD_HOME_VERSION ?= v0.107.57
ADGUARD_HOME_VERSION ?= v0.107.61
model: tb.oapi-codegen
@mkdir -p tmp
@@ -79,3 +85,6 @@ model-diff:
go run openapi/main.go $(ADGUARD_HOME_VERSION)
go run openapi/main.go
diff tmp/schema.yaml tmp/schema-master.yaml
zellij:
zellij -l ./testdata/test-layout.kdl

154
README.md
View File

@@ -141,9 +141,8 @@ set REPLICA1_USERNAME=username
set REPLICA1_PASSWORD=password
# set REPLICA1_COOKIE=Replica-Cookie-Name=CCCOOOKKKIIIEEE
set FEATURES_DHCP=false
set FEATURES_DHCP_SERVERCONFIG=false
set FEATURES_DHCP_STATICLEASES=false
set FEATURES_DHCP_SERVER_CONFIG=false
set FEATURES_DHCP_STATIC_LEASES=false
# run once
adguardhome-sync run
@@ -182,68 +181,68 @@ services:
restart: unless-stopped
```
### env
## Config via environment variables
```yaml
---
version: "2.1"
services:
adguardhome-sync:
image: ghcr.io/bakito/adguardhome-sync
container_name: adguardhome-sync
command: run
environment:
LOG_LEVEL: "info"
ORIGIN_URL: "https://192.168.1.2:3000"
# ORIGIN_WEB_URL: "https://some-other.url" # used in the web interface (default: <origin-url>
For Replicas replace `#` with the index number for the replica. E.g: `REPLICA#_URL` -> `REPLICA1_URL`
ORIGIN_USERNAME: "username"
ORIGIN_PASSWORD: "password"
REPLICA1_URL: "http://192.168.1.3"
REPLICA1_USERNAME: "username"
REPLICA1_PASSWORD: "password"
REPLICA2_URL: "http://192.168.1.4"
REPLICA2_USERNAME: "username"
REPLICA2_PASSWORD: "password"
REPLICA2_API_PATH: "/some/path/control"
# REPLICA2_WEB_URL: "https://some-other.url" # used in the web interface (default: <replica-url>
# REPLICA2_AUTO_SETUP: true # if true, AdGuardHome is automatically initialized.
# REPLICA2_INTERFACE_NAME: 'ens18' # use custom dhcp interface name
# REPLICA2_DHCP_SERVER_ENABLED: true/false (optional) enables/disables the dhcp server on the replica
CRON: "0 */2 * * *" # run every 2 hours
RUN_ON_START: "true"
# CONTINUE_ON_ERROR: false # If enabled, the synchronisation task will not fail on single errors, but will log the errors and continue
| Name | Type | Description |
|:-------------------------------------|--------|:----------------------------------------------------------|
| ORIGIN_URL (string) | string | URL of adguardhome instance |
| ORIGIN_WEB_URL (string) | string | Web URL of adguardhome instance |
| ORIGIN_API_PATH (string) | string | API Path |
| ORIGIN_USERNAME (string) | string | Adguardhome username |
| ORIGIN_PASSWORD (string) | string | Adguardhome password |
| ORIGIN_COOKIE (string) | string | Adguardhome cookie |
| ORIGIN_REQUEST_HEADERS (map) | map | Request Headers 'key1:value1,key2:value2' |
| ORIGIN_INSECURE_SKIP_VERIFY (bool) | bool | Skip TLS verification |
| ORIGIN_AUTO_SETUP (bool) | bool | Automatically setup the instance if it is not initialized |
| ORIGIN_INTERFACE_NAME (string) | string | Network interface name |
| ORIGIN_DHCP_SERVER_ENABLED (bool) | bool | Enable DHCP server |
| REPLICA#_URL (string) | string | URL of adguardhome instance |
| REPLICA#_WEB_URL (string) | string | Web URL of adguardhome instance |
| REPLICA#_API_PATH (string) | string | API Path |
| REPLICA#_USERNAME (string) | string | Adguardhome username |
| REPLICA#_PASSWORD (string) | string | Adguardhome password |
| REPLICA#_COOKIE (string) | string | Adguardhome cookie |
| REPLICA#_REQUEST_HEADERS (map) | map | Request Headers 'key1:value1,key2:value2' |
| REPLICA#_INSECURE_SKIP_VERIFY (bool) | bool | Skip TLS verification |
| REPLICA#_AUTO_SETUP (bool) | bool | Automatically setup the instance if it is not initialized |
| REPLICA#_INTERFACE_NAME (string) | string | Network interface name |
| REPLICA#_DHCP_SERVER_ENABLED (bool) | bool | Enable DHCP server |
| CRON (string) | string | Cron expression for the sync interval |
| RUN_ON_START (bool) | bool | Run the sung on startup |
| PRINT_CONFIG_ONLY (bool) | bool | Print current config only and stop the application |
| CONTINUE_ON_ERROR (bool) | bool | Continue sync on errors |
| API_PORT (int) | int | API port |
| API_USERNAME (string) | string | API username |
| API_PASSWORD (string) | string | API password |
| API_DARK_MODE (bool) | bool | API dark mode |
| API_METRICS_ENABLED (bool) | bool | Enable metrics |
| API_METRICS_SCRAPE_INTERVAL (int64) | int64 | Interval for metrics scraping |
| API_METRICS_QUERY_LOG_LIMIT (int) | int | Metrics log query limit |
| API_TLS_CERT_DIR (string) | string | API TLS certificate directory |
| API_TLS_CERT_NAME (string) | string | API TLS certificate file name |
| API_TLS_KEY_NAME (string) | string | API TLS key file name |
| FEATURES_DNS_ACCESS_LISTS (bool) | bool | Sync DNS access lists |
| FEATURES_DNS_SERVER_CONFIG (bool) | bool | Sync DNS server config |
| FEATURES_DNS_REWRITES (bool) | bool | Sync DNS rewrites |
| FEATURES_DHCP_SERVER_CONFIG (bool) | bool | Sync DHCP server config |
| FEATURES_DHCP_STATIC_LEASES (bool) | bool | Sync DHCP static leases |
| FEATURES_GENERAL_SETTINGS (bool) | bool | Sync general settings |
| FEATURES_QUERY_LOG_CONFIG (bool) | bool | Sync query log config |
| FEATURES_STATS_CONFIG (bool) | bool | Sync stats config |
| FEATURES_CLIENT_SETTINGS (bool) | bool | Sync client settings |
| FEATURES_SERVICES (bool) | bool | Sync services |
| FEATURES_FILTERS (bool) | bool | Sync filters |
| FEATURES_THEME (bool) | bool | Sync the weg UI theme |
# Configure the sync API server, disabled if api port is 0
API_PORT: 8080
# API_DARK_MODE: "true"
# API_USERNAME: admin
# API_PASSWORD: secret
# the directory of the provided tls certs
# API_TLS_CERT_DIR: /path/to/certs
# the name of the cert file (default: tls.crt)
# API_TLS_CERT_NAME: foo.crt
# the name of the key file (default: tls.key)
# API_TLS_KEY_NAME: bar.key
# API_METRICS_ENABLED: "true"
### Unraid
# Configure sync features; by default all features are enabled.
# FEATURES_GENERAL_SETTINGS: "true"
# FEATURES_QUERY_LOG_CONFIG: "true"
# FEATURES_STATS_CONFIG: "true"
# FEATURES_CLIENT_SETTINGS: "true"
# FEATURES_SERVICES: "true"
# FEATURES_FILTERS: "true"
# FEATURES_DHCP_SERVER_CONFIG: "true"
# FEATURES_DHCP_STATIC_LEASES: "true"
# FEATURES_DNS_SERVER_CONFIG: "true"
# FEATURES_DNS_ACCESS_LISTS: "true"
# FEATURES_DNS_REWRITES: "true"
# FEATURES_THEME: "true" # if false the UI theme is not synced
ports:
- 8080:8080
restart: unless-stopped
```
⚠️ Disclaimer: Tere exists an unraid tepmlate for this application. This template is not managed by this project.
Also, as unraid is not known to me, I can not give any support on unraind templates.
Note when running the Docker container in Unraid please remove unneeded env variables if don't needed.
If replica2 isn't used this can cause sync errors.
### Config file
@@ -267,6 +266,8 @@ origin:
username: username
password: password
# cookie: Origin-Cookie-Name=CCCOOOKKKIIIEEE
# requestHeaders: # Additional request headers
# AAA: bbb
# replicas instances
replicas:
@@ -281,6 +282,8 @@ replicas:
# cookie: Replica2-Cookie-Name=CCCOOOKKKIIIEEE
# autoSetup: true # if true, AdGuardHome is automatically initialized.
# webURL: "https://some-other.url" # used in the web interface (default: <replica-url>
# requestHeaders: # Additional request headers
# AAA: bbb
# Configure the sync API server, disabled if api port is 0
api:
@@ -294,9 +297,9 @@ api:
# enable metrics on path '/metrics' (api port must be != 0)
# metrics:
# enabled: true
# scrapeInterval: 30s
# queryLogLimit: 10000
# enabled: true
# scrapeInterval: 30s
# queryLogLimit: 10000
# enable tls for the api server
# tls:
@@ -324,6 +327,29 @@ features:
rewrites: true
```
## Home Assistant AdGuard Home Add-on users
To enable syncing with a Home Assistant instance using
the [AdGuard Home Add-on](https://github.com/hassio-addons/addon-adguard-home), you will need to enable the disabled
ports, under the Network heading
![show-disabled-ports](https://github.com/user-attachments/assets/1df5f352-37a2-4508-82ec-7f270087d0b4)
And then set the port of your choice for the Web interface
![web-interface-port](https://github.com/user-attachments/assets/286ed030-4831-4f49-8b29-53e8802129c3)
Don't forget to save and restart the add-on.
Depending on your setup, you may also need to disable SSL for the add-on.
The username:password required for the Home Assistant replica is the one you use to login to your instance, however it's
recommended to setup a new local only user with minimal permissions.
All credit for this method goes to [Brunty](https://github.com/brunty) who has a far
more [detailed write up](https://brunty.me/post/replicate-adguard-home-settings-into-home-assistant-adguard-home-addon/)
about this on his blog.
## Log Level
The log level can be set with the environment variable: `LOG_LEVEL`

View File

@@ -1,50 +0,0 @@
package cmd
import (
"bytes"
_ "embed"
"os"
"sort"
"strings"
"text/template"
"github.com/bakito/adguardhome-sync/pkg/types"
"gopkg.in/yaml.v3"
)
//go:embed print-config.md
var printConfigTemplate string
func printConfig(cfg *types.Config, usedCfgFile string, cfgContent string) error {
config, err := yaml.Marshal(cfg)
if err != nil {
logger.Error(err)
return err
}
t, err := template.New("printConfigTemplate").Parse(printConfigTemplate)
if err != nil {
return err
}
env := os.Environ()
sort.Strings(env)
var buf bytes.Buffer
if err = t.Execute(&buf, map[string]interface{}{
"AggregatedConfig": string(config),
"ConfigFilePath": usedCfgFile,
"ConfigFileContent": cfgContent,
"EnvironmentVariables": strings.Join(env, "\n"),
}); err != nil {
return err
}
logger.Infof(
"Printing adguardhome-sync aggregated config (THE APPLICATION WILL NOT START IN THIS MODE):\n%s",
buf.String(),
)
return nil
}

View File

@@ -1,24 +0,0 @@
<!-- PLEASE COPY THE FOLLOWING OUTPUT AS IS INTO THE GITHUB ISSUE (Don't forget to mask your usernames, passwords and IPs when using this in an issue ) -->
### AdGuardHome sync aggregated config
```yaml
{{ .AggregatedConfig }}
```
{{- if .ConfigFilePath }}
### AdGuardHome sync unmodified config file
Config file path: {{ .ConfigFilePath }}
```yaml
{{ .ConfigFileContent }}
```
{{- end }}
### Environment Variables
```ini
{{ .EnvironmentVariables }}
```
<!-- END OF GITHUB ISSUE CONTENT -->

View File

@@ -4,9 +4,10 @@ import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/bakito/adguardhome-sync/pkg/log"
"github.com/bakito/adguardhome-sync/version"
"github.com/spf13/cobra"
)
var (
@@ -14,7 +15,7 @@ var (
logger = log.GetLogger("root")
)
// rootCmd represents the base command when called without any subcommands
// rootCmd represents the base command when called without any subcommands.
var rootCmd = &cobra.Command{
Use: "adguardhome-sync",
Short: "Synchronize config from one AdGuardHome instance to another",
@@ -25,7 +26,7 @@ var rootCmd = &cobra.Command{
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
_, _ = fmt.Println(err)
os.Exit(1)
}
}

View File

@@ -1,20 +1,21 @@
package cmd
import (
"github.com/spf13/cobra"
"github.com/bakito/adguardhome-sync/pkg/config"
"github.com/bakito/adguardhome-sync/pkg/log"
"github.com/bakito/adguardhome-sync/pkg/sync"
"github.com/spf13/cobra"
)
// runCmd represents the run command
// runCmd represents the run command.
var doCmd = &cobra.Command{
Use: "run",
Short: "Start a synchronisation from origin to replica",
Short: "Start a synchronization from origin to replica",
Long: `Synchronizes the configuration form an origin instance to a replica`,
RunE: func(cmd *cobra.Command, args []string) error {
logger = log.GetLogger("run")
cfg, usedCfgFile, cfgContent, err := config.Get(cfgFile, cmd.Flags())
cfg, err := config.Get(cfgFile, cmd.Flags())
if err != nil {
logger.Error(err)
return err
@@ -25,9 +26,8 @@ var doCmd = &cobra.Command{
return err
}
if cfg.PrintConfigOnly {
if err := printConfig(cfg, usedCfgFile, cfgContent); err != nil {
if cfg.PrintConfigOnly() {
if err := cfg.Print(); err != nil {
logger.Error(err)
return err
}
@@ -35,7 +35,7 @@ var doCmd = &cobra.Command{
return nil
}
return sync.Sync(cfg)
return sync.Sync(cfg.Get())
},
}
@@ -45,21 +45,21 @@ func init() {
doCmd.PersistentFlags().Bool(config.FlagRunOnStart, true, "Run the sync job on start.")
doCmd.PersistentFlags().Bool(config.FlagPrintConfigOnly, false, "Prints the configuration only and exists. "+
"Can be used to debug the config E.g: when having authentication issues.")
doCmd.PersistentFlags().Bool(config.FlagContinueOnError, false, "If enabled, the synchronisation task "+
doCmd.PersistentFlags().Bool(config.FlagContinueOnError, false, "If enabled, the synchronization task "+
"will not fail on single errors, but will log the errors and continue.")
doCmd.PersistentFlags().
Int(config.FlagApiPort, 8080, "Sync API Port, the API endpoint will be started to enable remote triggering; if 0 port API is disabled.")
doCmd.PersistentFlags().String(config.FlagApiUsername, "", "Sync API username")
doCmd.PersistentFlags().String(config.FlagApiPassword, "", "Sync API password")
doCmd.PersistentFlags().String(config.FlagApiDarkMode, "", "API UI in dark mode")
Int(config.FlagAPIPort, 8080, "Sync API Port, the API endpoint will be started to enable remote triggering; if 0 port API is disabled.")
doCmd.PersistentFlags().String(config.FlagAPIUsername, "", "Sync API username")
doCmd.PersistentFlags().String(config.FlagAPIPassword, "", "Sync API password")
doCmd.PersistentFlags().String(config.FlagAPIDarkMode, "", "API UI in dark mode")
doCmd.PersistentFlags().Bool(config.FlagFeatureDhcpServerConfig, true, "Enable DHCP server config feature")
doCmd.PersistentFlags().Bool(config.FlagFeatureDhcpStaticLeases, true, "Enable DHCP server static leases feature")
doCmd.PersistentFlags().Bool(config.FlagFeatureDnsServerConfig, true, "Enable DNS server config feature")
doCmd.PersistentFlags().Bool(config.FlagFeatureDnsAccessLists, true, "Enable DNS server access lists feature")
doCmd.PersistentFlags().Bool(config.FlagFeatureDnsRewrites, true, "Enable DNS rewrites feature")
doCmd.PersistentFlags().Bool(config.FlagFeatureDNSServerConfig, true, "Enable DNS server config feature")
doCmd.PersistentFlags().Bool(config.FlagFeatureDNSAccessLists, true, "Enable DNS server access lists feature")
doCmd.PersistentFlags().Bool(config.FlagFeatureDNSRewrites, true, "Enable DNS rewrites feature")
doCmd.PersistentFlags().Bool(config.FlagFeatureGeneral, true, "Enable general settings feature")
doCmd.PersistentFlags().Bool(config.FlagFeatureQueryLog, true, "Enable query log config feature")
@@ -71,7 +71,7 @@ func init() {
doCmd.PersistentFlags().String(config.FlagOriginURL, "", "Origin instance url")
doCmd.PersistentFlags().
String(config.FlagOriginWebURL, "", "Origin instance web url used in the web interface (default: <origin-url>)")
doCmd.PersistentFlags().String(config.FlagOriginApiPath, "/control", "Origin instance API path")
doCmd.PersistentFlags().String(config.FlagOriginAPIPath, "/control", "Origin instance API path")
doCmd.PersistentFlags().String(config.FlagOriginUsername, "", "Origin instance username")
doCmd.PersistentFlags().String(config.FlagOriginPassword, "", "Origin instance password")
doCmd.PersistentFlags().String(config.FlagOriginCookie, "", "If Set, uses a cookie for authentication")
@@ -80,7 +80,7 @@ func init() {
doCmd.PersistentFlags().String(config.FlagReplicaURL, "", "Replica instance url")
doCmd.PersistentFlags().
String(config.FlagReplicaWebURL, "", "Replica instance web url used in the web interface (default: <replica-url>)")
doCmd.PersistentFlags().String(config.FlagReplicaApiPath, "/control", "Replica instance API path")
doCmd.PersistentFlags().String(config.FlagReplicaAPIPath, "/control", "Replica instance API path")
doCmd.PersistentFlags().String(config.FlagReplicaUsername, "", "Replica instance username")
doCmd.PersistentFlags().String(config.FlagReplicaPassword, "", "Replica instance password")
doCmd.PersistentFlags().String(config.FlagReplicaCookie, "", "If Set, uses a cookie for authentication")

62
docs/main.go Normal file
View File

@@ -0,0 +1,62 @@
// Print the available environment variables
package main
import (
"fmt"
"reflect"
"strings"
"github.com/bakito/adguardhome-sync/pkg/types"
)
func main() {
_, _ = fmt.Println("| Name | Type | Description |")
_, _ = fmt.Println("| :--- | ---- |:----------- |")
printEnvTags(reflect.TypeOf(types.Config{}), "")
}
// printEnvTags recursively prints all fields with `env` tags.
func printEnvTags(t reflect.Type, prefix string) {
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.Kind() != reflect.Struct {
return
}
for _, field := range reflect.VisibleFields(t) {
if field.PkgPath != "" { // unexported field
continue
}
envTag := field.Tag.Get("env")
if envTag == "" {
switch field.Name {
case "Origin":
envTag = "ORIGIN"
case "Replica":
envTag = "REPLICA#"
}
}
combinedTag := envTag
if prefix != "" && envTag != "" {
combinedTag = prefix + "_" + envTag
} else if prefix != "" {
combinedTag = prefix
}
ft := field.Type
if ft.Kind() == reflect.Ptr {
ft = ft.Elem()
}
if ft.Kind() == reflect.Struct && ft.Name() != "Time" { // skip time.Time
printEnvTags(ft, strings.TrimSuffix(combinedTag, "_"))
} else if envTag != "" {
envVar := strings.Trim(combinedTag, "_") + " (" + ft.Kind().String() + ")"
docs := field.Tag.Get("documentation")
_, _ = fmt.Printf("| %s | %s | %s |\n", envVar, ft.Kind().String(), docs)
}
}
}

71
go.mod
View File

@@ -1,78 +1,77 @@
module github.com/bakito/adguardhome-sync
go 1.24.0
go 1.24.2
require (
github.com/caarlos0/env/v11 v11.3.1
github.com/gin-gonic/gin v1.10.0
github.com/go-faker/faker/v4 v4.6.0
github.com/go-faker/faker/v4 v4.6.1
github.com/go-resty/resty/v2 v2.16.5
github.com/google/uuid v1.6.0
github.com/jinzhu/copier v0.4.0
github.com/oapi-codegen/runtime v1.1.1
github.com/onsi/ginkgo/v2 v2.22.2
github.com/onsi/gomega v1.36.2
github.com/prometheus/client_golang v1.21.0
github.com/onsi/ginkgo/v2 v2.23.4
github.com/onsi/gomega v1.37.0
github.com/prometheus/client_golang v1.22.0
github.com/robfig/cron/v3 v3.0.1
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1
github.com/spf13/cobra v1.9.1
go.uber.org/mock v0.5.0
go.uber.org/mock v0.5.2
go.uber.org/zap v1.27.0
golang.org/x/mod v0.23.0
golang.org/x/mod v0.24.0
gopkg.in/yaml.v3 v3.0.1
k8s.io/apimachinery v0.32.2
k8s.io/utils v0.0.0-20241210054802-24370beab758
k8s.io/apimachinery v0.33.1
k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979
)
require (
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/bytedance/sonic v1.13.2 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/go-playground/validator/v10 v10.26.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.63.0 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.22.0 // indirect
golang.org/x/tools v0.28.0 // indirect
google.golang.org/protobuf v1.36.1 // indirect
golang.org/x/arch v0.16.0 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect
golang.org/x/tools v0.32.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)

161
go.sum
View File

@@ -4,35 +4,34 @@ github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU=
github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-faker/faker/v4 v4.6.0 h1:6aOPzNptRiDwD14HuAnEtlTa+D1IfFuEHO8+vEFwjTs=
github.com/go-faker/faker/v4 v4.6.0/go.mod h1:ZmrHuVtTTm2Em9e0Du6CJ9CADaLEzGXW62z1YqFH0m0=
github.com/go-faker/faker/v4 v4.6.1 h1:xUyVpAjEtB04l6XFY0V/29oR332rOSPWV4lU8RwDt4k=
github.com/go-faker/faker/v4 v4.6.1/go.mod h1:arSdxNCSt7mOhdk8tEolvHeIJ7eX4OX80wXjKKvkKBY=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
@@ -41,24 +40,22 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
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/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a h1:rDA3FfmxwXR+BVKKdz55WwMJ1pD2hJQNW31d+l3mPk4=
github.com/google/pprof v0.0.0-20250501235452-c0086092b71a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@@ -70,11 +67,11 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
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.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -95,27 +92,28 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro=
github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=
github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.21.0 h1:DIsaGmiaBkSangBgMtWdNfxbMNdku5IK6iNhrEqWvdA=
github.com/prometheus/client_golang v1.21.0/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
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.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
@@ -127,14 +125,11 @@ github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKk
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/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
@@ -145,60 +140,60 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U=
golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
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.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
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/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
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.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
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.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
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/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
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/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
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=
@@ -207,17 +202,19 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
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=
k8s.io/apimachinery v0.32.2 h1:yoQBR9ZGkA6Rgmhbp/yuT9/g+4lxtsGYwW6dR6BDPLQ=
k8s.io/apimachinery v0.32.2/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4=
k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0=
k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 h1:jgJW5IePPXLGB8e/1wvd0Ich9QE97RvvF3a8J3fP/Lg=
k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA=
sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI=
sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=

View File

@@ -1,6 +1,7 @@
package main
import (
"context"
"fmt"
"io"
"log"
@@ -20,21 +21,33 @@ func main() {
}
log.Printf("Patching schema version %s\n", version)
resp, err := http.Get(
ctx := context.Background() // Or use context.WithTimeout
req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
fmt.Sprintf("https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/%s/openapi/openapi.yaml", version),
http.NoBody,
)
if err != nil {
log.Fatal(err)
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
log.Fatalln(err)
}
defer func() { _ = resp.Body.Close() }()
data, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatalln(err)
log.Println(err)
return
}
schema := make(map[string]interface{})
schema := make(map[string]any)
err = yaml.Unmarshal(data, &schema)
if err != nil {
log.Fatalln(err)
log.Println(err)
return
}
if requestBodies, ok, _ := unstructured.NestedMap(schema, "components", "requestBodies"); ok {
@@ -47,19 +60,51 @@ func main() {
"paths", "/dns_info", "get", "responses", "200", "content", "application/json", "schema"); ok {
if allOf, ok, _ := unstructured.NestedSlice(dnsInfo, "allOf"); ok && len(allOf) == 2 {
delete(dnsInfo, "allOf")
if err := unstructured.SetNestedMap(schema, allOf[0].(map[string]interface{}),
//nolint:forcetypeassert
if err := unstructured.SetNestedMap(schema, allOf[0].(map[string]any),
"paths", "/dns_info", "get", "responses", "200", "content", "application/json", "schema"); err != nil {
log.Fatalln(err)
log.Println(err)
return
}
}
}
correctEntries(schema)
addFakeTags(schema)
b, err := yaml.Marshal(&schema)
if err != nil {
log.Fatalln(err)
log.Println(err)
return
}
log.Printf("Writing schema file tmp/%s", fileName)
err = os.WriteFile("tmp/"+fileName, b, 0o600)
if err != nil {
log.Println(err)
return
}
}
func correctEntries(schema map[string]any) {
// https://github.com/AdguardTeam/AdGuardHome/pull/7678
if err := unstructured.SetNestedField(schema, "string", "components", "schemas", "QueryLogItem", "properties", "client_proto", "type"); err != nil {
log.Fatalln(err)
}
}
func addFakeTags(schema map[string]any) {
fake := map[string]any{"faker": `slice_len=24`}
if err := unstructured.SetNestedMap(schema, fake, "components", "schemas", "Stats", "properties", "blocked_filtering", "x-oapi-codegen-extra-tags"); err != nil {
log.Fatalln(err)
}
if err := unstructured.SetNestedMap(schema, fake, "components", "schemas", "Stats", "properties", "dns_queries", "x-oapi-codegen-extra-tags"); err != nil {
log.Fatalln(err)
}
if err := unstructured.SetNestedMap(schema, fake, "components", "schemas", "Stats", "properties", "replaced_parental", "x-oapi-codegen-extra-tags"); err != nil {
log.Fatalln(err)
}
if err := unstructured.SetNestedMap(schema, fake, "components", "schemas", "Stats", "properties", "replaced_safebrowsing", "x-oapi-codegen-extra-tags"); err != nil {
log.Fatalln(err)
}
}

View File

@@ -17,14 +17,18 @@ func (cl *client) doGet(req *resty.Request, url string) error {
rl.Debug("do get")
resp, err := req.Get(url)
if err != nil {
if resp != nil && resp.StatusCode() == http.StatusFound {
loc := resp.Header().Get("Location")
if loc == "/install.html" || loc == "/control/install.html" {
return ErrSetupNeeded
l := rl
if resp != nil {
if resp.StatusCode() == http.StatusFound {
loc := resp.Header().Get("Location")
if loc == "/install.html" || loc == "/control/install.html" {
return ErrSetupNeeded
}
}
l = l.With("status", resp.StatusCode(), "body", string(resp.Body()), "error", err)
}
rl.With("status", resp.StatusCode(), "body", string(resp.Body()), "error", err).Debug("error in do get")
l.Debug("error in do get")
return detailedError(resp, err)
}

View File

@@ -11,19 +11,20 @@ import (
"strconv"
"strings"
"github.com/go-resty/resty/v2"
"go.uber.org/zap"
"github.com/bakito/adguardhome-sync/pkg/client/model"
"github.com/bakito/adguardhome-sync/pkg/log"
"github.com/bakito/adguardhome-sync/pkg/types"
"github.com/bakito/adguardhome-sync/pkg/utils"
"github.com/go-resty/resty/v2"
"go.uber.org/zap"
)
const envRedirectPolicyNoOfRedirects = "REDIRECT_POLICY_NO_OF_REDIRECTS"
var (
l = log.GetLogger("client")
// ErrSetupNeeded custom error
// ErrSetupNeeded custom error.
ErrSetupNeeded = errors.New("setup needed")
)
@@ -33,16 +34,16 @@ func detailedError(resp *resty.Response, err error) error {
e += fmt.Sprintf("(%s)", string(resp.Body()))
}
if err != nil {
e += fmt.Sprintf(": %s", err.Error())
e += ": " + err.Error()
}
return errors.New(e)
}
// New create a new client
// New create a new client.
func New(config types.AdGuardInstance) (Client, error) {
var apiURL string
if config.APIPath == "" {
apiURL = fmt.Sprintf("%s/control", config.URL)
apiURL = config.URL + "/control"
} else {
apiURL = fmt.Sprintf("%s/%s", config.URL, config.APIPath)
}
@@ -51,7 +52,7 @@ func New(config types.AdGuardInstance) (Client, error) {
return nil, err
}
u.Path = path.Clean(u.Path)
cl := resty.New().SetBaseURL(u.String()).SetDisableWarn(true)
cl := resty.New().SetBaseURL(u.String()).SetDisableWarn(true).SetHeaders(config.RequestHeaders)
// #nosec G402 has to be explicitly enabled
cl.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: config.InsecureSkipVerify})
@@ -84,7 +85,9 @@ func New(config types.AdGuardInstance) (Client, error) {
}, nil
}
// Client AdguardHome API client interface
// Client AdguardHome API client interface.
//
//nolint:interfacebloat
type Client interface {
Host() string
Status() (*model.ServerStatus, error)
@@ -116,16 +119,16 @@ type Client interface {
UpdateClient(client *model.Client) error
DeleteClient(client *model.Client) error
QueryLogConfig() (*model.QueryLogConfigWithIgnored, error)
SetQueryLogConfig(*model.QueryLogConfigWithIgnored) error
SetQueryLogConfig(ql *model.QueryLogConfigWithIgnored) error
StatsConfig() (*model.GetStatsConfigResponse, error)
SetStatsConfig(sc *model.PutStatsConfigUpdateRequest) error
Setup() error
AccessList() (*model.AccessList, error)
SetAccessList(*model.AccessList) error
SetAccessList(accessList *model.AccessList) error
DNSConfig() (*model.DNSConfig, error)
SetDNSConfig(*model.DNSConfig) error
SetDNSConfig(config *model.DNSConfig) error
DhcpConfig() (*model.DhcpStatus, error)
SetDhcpConfig(*model.DhcpStatus) error
SetDhcpConfig(status *model.DhcpStatus) error
AddDHCPStaticLease(lease model.DhcpStaticLease) error
DeleteDHCPStaticLease(lease model.DhcpStaticLease) error
}
@@ -224,7 +227,7 @@ func (cl *client) toggleStatus(mode string) (bool, error) {
}
func (cl *client) toggleBool(mode string, enable bool) error {
cl.log.With("enable", enable).Info(fmt.Sprintf("Toggle %s", mode))
cl.log.With("enable", enable).Info("Toggle " + mode)
var target string
if enable {
target = "enable"

View File

@@ -8,13 +8,14 @@ import (
"os"
"path/filepath"
"github.com/google/uuid"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/bakito/adguardhome-sync/pkg/client"
"github.com/bakito/adguardhome-sync/pkg/client/model"
"github.com/bakito/adguardhome-sync/pkg/types"
"github.com/bakito/adguardhome-sync/pkg/utils"
"github.com/google/uuid"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var (
@@ -140,7 +141,7 @@ var _ = Describe("Client", func() {
ts, cl = ClientPost(
"/install/configure",
fmt.Sprintf(
`{"web":{"ip":"0.0.0.0","port":3000,"status":"","can_autofix":false},"dns":{"ip":"0.0.0.0","port":53,"status":"","can_autofix":false},"username":"%s","password":"%s"}`,
`{"web":{"ip":"0.0.0.0","port":3000,"status":"","can_autofix":false},"dns":{"ip":"0.0.0.0","port":53,"status":"","can_autofix":false},"username":%q,"password":%q}`,
username,
password,
),
@@ -373,10 +374,10 @@ var _ = Describe("Client", func() {
})
})
func ClientGet(file string, path string) (*httptest.Server, client.Client) {
func ClientGet(file, path string) (*httptest.Server, client.Client) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Ω(r.URL.Path).Should(Equal(types.DefaultAPIPath + path))
b, err := os.ReadFile(filepath.Join("../../testdata", file))
b, err := os.ReadFile(filepath.Join("..", "..", "testdata", file))
Ω(err).ShouldNot(HaveOccurred())
w.Header().Set("Content-Type", "application/json")
_, err = w.Write(b)

View File

@@ -11,19 +11,20 @@ import (
"net/url"
"path"
"go.uber.org/zap"
"github.com/bakito/adguardhome-sync/pkg/client/model"
"github.com/bakito/adguardhome-sync/pkg/log"
"github.com/bakito/adguardhome-sync/pkg/types"
"go.uber.org/zap"
)
var l = log.GetLogger("client")
// New create a new api client
// New create a new api client.
func New(config types.AdGuardInstance) (Client, error) {
var apiURL string
if config.APIPath == "" {
apiURL = fmt.Sprintf("%s/control", config.URL)
apiURL = config.URL + "/control"
} else {
apiURL = fmt.Sprintf("%s/%s", config.URL, config.APIPath)
}
@@ -96,7 +97,7 @@ func (a apiClient) SetFilteringConfig(ctx context.Context, config model.FilterCo
return write(ctx, config, a.client.FilteringConfig)
}
func write[B interface{}](
func write[B any](
ctx context.Context,
body B,
req func(ctx context.Context, body B, reqEditors ...model.RequestEditorFn) (*http.Response, error),
@@ -112,7 +113,7 @@ func write[B interface{}](
return nil
}
func read[I interface{}](
func read[I any](
ctx context.Context,
req func(ctx context.Context, reqEditors ...model.RequestEditorFn) (*http.Response, error),
parse func(rsp *http.Response) (*I, error),

View File

@@ -3,8 +3,9 @@ package client
import (
"net/http"
"github.com/bakito/adguardhome-sync/pkg/client/model"
"github.com/go-resty/resty/v2"
"github.com/bakito/adguardhome-sync/pkg/client/model"
)
var _ model.HttpRequestDoer = &adapter{}

View File

@@ -5,13 +5,14 @@ import (
"sort"
"strings"
"github.com/bakito/adguardhome-sync/pkg/utils"
"github.com/jinzhu/copier"
"go.uber.org/zap"
"k8s.io/utils/ptr"
"github.com/bakito/adguardhome-sync/pkg/utils"
)
// Clone the config
// Clone the config.
func (c *DhcpStatus) Clone() *DhcpStatus {
clone := &DhcpStatus{}
_ = copier.Copy(clone, c)
@@ -27,16 +28,16 @@ func (c *DhcpStatus) cleanV4V6() {
}
}
// CleanAndEquals dhcp server config equal check where V4 and V6 are cleaned in advance
// CleanAndEquals dhcp server config equal check where V4 and V6 are cleaned in advance.
func (c *DhcpStatus) CleanAndEquals(o *DhcpStatus) bool {
c.cleanV4V6()
o.cleanV4V6()
return c.Equals(o)
}
// Equals dhcp server config equal check
// Equals dhcp server config equal check.
func (c *DhcpStatus) Equals(o *DhcpStatus) bool {
return utils.JsonEquals(c, o)
return utils.JSONEquals(c, o)
}
func (c *DhcpStatus) HasConfig() bool {
@@ -56,8 +57,8 @@ func (j DhcpConfigV6) isValid() bool {
type DhcpStaticLeases []DhcpStaticLease
// MergeDhcpStaticLeases the leases
func MergeDhcpStaticLeases(l *[]DhcpStaticLease, other *[]DhcpStaticLease) (DhcpStaticLeases, DhcpStaticLeases) {
// MergeDhcpStaticLeases the leases.
func MergeDhcpStaticLeases(l, other *[]DhcpStaticLease) (adds, removes DhcpStaticLeases) {
var thisLeases []DhcpStaticLease
var otherLeases []DhcpStaticLease
@@ -69,8 +70,6 @@ func MergeDhcpStaticLeases(l *[]DhcpStaticLease, other *[]DhcpStaticLease) (Dhcp
}
current := make(map[string]DhcpStaticLease)
var adds DhcpStaticLeases
var removes DhcpStaticLeases
for _, le := range thisLeases {
current[le.Mac] = le
}
@@ -90,21 +89,21 @@ func MergeDhcpStaticLeases(l *[]DhcpStaticLease, other *[]DhcpStaticLease) (Dhcp
return adds, removes
}
// Equals dns config equal check
// Equals dns config equal check.
func (c *DNSConfig) Equals(o *DNSConfig) bool {
cc := c.Clone()
oo := o.Clone()
cc.Sort()
oo.Sort()
return utils.JsonEquals(cc, oo)
return utils.JSONEquals(cc, oo)
}
func (c *DNSConfig) Clone() *DNSConfig {
return utils.Clone(c, &DNSConfig{})
}
// Sort sort dns config
// Sort dns config.
func (c *DNSConfig) Sort() {
if c.UpstreamDns != nil {
sort.Strings(*c.UpstreamDns)
@@ -119,14 +118,14 @@ func (c *DNSConfig) Sort() {
}
}
// Equals access list equal check
// Equals access list equal check.
func (al *AccessList) Equals(o *AccessList) bool {
return EqualsStringSlice(al.AllowedClients, o.AllowedClients, true) &&
EqualsStringSlice(al.DisallowedClients, o.DisallowedClients, true) &&
EqualsStringSlice(al.BlockedHosts, o.BlockedHosts, true)
}
func EqualsStringSlice(a *[]string, b *[]string, sortIt bool) bool {
func EqualsStringSlice(a, b *[]string, sortIt bool) bool {
if a == nil && b == nil {
return true
}
@@ -152,7 +151,7 @@ func EqualsStringSlice(a *[]string, b *[]string, sortIt bool) bool {
return true
}
// Sort clients
// Sort clients.
func (cl *Client) Sort() {
if cl.Ids != nil {
sort.Strings(*cl.Ids)
@@ -168,28 +167,26 @@ func (cl *Client) Sort() {
}
}
// PrepareDiff timezone BlockedServicesSchedule might differ if all other fields are empty,
// so we skip it in diff
// PrepareDiff so we skip it in diff.
func (cl *Client) PrepareDiff() *string {
var tz *string
bss := cl.BlockedServicesSchedule
if bss != nil && bss.Mon == nil && bss.Tue == nil && bss.Wed == nil &&
bss.Thu == nil && bss.Fri == nil && bss.Sat == nil && bss.Sun == nil {
tz = cl.BlockedServicesSchedule.TimeZone
cl.BlockedServicesSchedule.TimeZone = nil
}
return tz
}
// AfterDiff reset after diff
// AfterDiff reset after diff.
func (cl *Client) AfterDiff(tz *string) {
if cl.BlockedServicesSchedule != nil {
cl.BlockedServicesSchedule.TimeZone = tz
}
}
// Equals Clients equal check
// Equals Clients equal check.
func (cl *Client) Equals(o *Client) bool {
cl.Sort()
o.Sort()
@@ -202,10 +199,10 @@ func (cl *Client) Equals(o *Client) bool {
o.AfterDiff(bssO)
}()
return utils.JsonEquals(cl, o)
return utils.JSONEquals(cl, o)
}
// Add ac client
// Add ac client.
func (clients *Clients) Add(cl Client) {
if clients.Clients == nil {
clients.Clients = &ClientsArray{cl}
@@ -215,8 +212,8 @@ func (clients *Clients) Add(cl Client) {
}
}
// Merge merge Clients
func (clients *Clients) Merge(other *Clients) ([]*Client, []*Client, []*Client) {
// Merge merge Clients.
func (clients *Clients) Merge(other *Clients) (adds, removes, updates []*Client) {
current := make(map[string]*Client)
if clients.Clients != nil {
cc := *clients.Clients
@@ -233,10 +230,6 @@ func (clients *Clients) Merge(other *Clients) ([]*Client, []*Client, []*Client)
}
}
var adds []*Client
var removes []*Client
var updates []*Client
for _, cl := range expected {
if oc, ok := current[*cl.Name]; ok {
if !cl.Equals(oc) {
@@ -255,7 +248,7 @@ func (clients *Clients) Merge(other *Clients) ([]*Client, []*Client, []*Client)
return adds, updates, removes
}
// Key RewriteEntry key
// Key RewriteEntry key.
func (re *RewriteEntry) Key() string {
var d string
var a string
@@ -268,16 +261,13 @@ func (re *RewriteEntry) Key() string {
return fmt.Sprintf("%s#%s", d, a)
}
// RewriteEntries list of RewriteEntry
// RewriteEntries list of RewriteEntry.
type RewriteEntries []RewriteEntry
// Merge RewriteEntries
func (rwe *RewriteEntries) Merge(other *RewriteEntries) (RewriteEntries, RewriteEntries, RewriteEntries) {
// Merge RewriteEntries.
func (rwe *RewriteEntries) Merge(other *RewriteEntries) (adds, removes, duplicates RewriteEntries) {
current := make(map[string]RewriteEntry)
var adds RewriteEntries
var removes RewriteEntries
var duplicates RewriteEntries
processed := make(map[string]bool)
for _, rr := range *rwe {
if _, ok := processed[rr.Key()]; !ok {
@@ -310,16 +300,13 @@ func (rwe *RewriteEntries) Merge(other *RewriteEntries) (RewriteEntries, Rewrite
return adds, removes, duplicates
}
func MergeFilters(this *[]Filter, other *[]Filter) ([]Filter, []Filter, []Filter) {
func MergeFilters(this, other *[]Filter) (adds, updates, removes []Filter) {
if this == nil && other == nil {
return nil, nil, nil
}
current := make(map[string]*Filter)
var adds []Filter
var updates []Filter
var removes []Filter
if this != nil {
for _, fi := range *this {
current[fi.Url] = &fi
@@ -346,7 +333,7 @@ func MergeFilters(this *[]Filter, other *[]Filter) ([]Filter, []Filter, []Filter
return adds, updates, removes
}
// Equals Filter equal check
// Equals Filter equal check.
func (f *Filter) Equals(o *Filter) bool {
return f.Enabled == o.Enabled && f.Url == o.Url && f.Name == o.Name
}
@@ -358,17 +345,17 @@ type QueryLogConfigWithIgnored struct {
Ignored []string `json:"ignored,omitempty"`
}
// Equals QueryLogConfig equal check
// Equals QueryLogConfig equal check.
func (qlc *QueryLogConfigWithIgnored) Equals(o *QueryLogConfigWithIgnored) bool {
return utils.JsonEquals(qlc, o)
return utils.JSONEquals(qlc, o)
}
// Equals QueryLogConfigInterval equal check
// Equals QueryLogConfigInterval equal check.
func (qlc *QueryLogConfigInterval) Equals(o *QueryLogConfigInterval) bool {
return ptrEquals(qlc, o)
}
func ptrEquals[T comparable](a *T, b *T) bool {
func ptrEquals[T comparable](a, b *T) bool {
if a == nil && b == nil {
return true
}
@@ -384,7 +371,7 @@ func ptrEquals[T comparable](a *T, b *T) bool {
return aa == bb
}
// EnableConfig API struct
// EnableConfig API struct.
type EnableConfig struct {
Enabled bool `json:"enabled"`
}
@@ -421,7 +408,7 @@ func (pi *ProfileInfo) ShouldSyncFor(o *ProfileInfo, withTheme bool) *ProfileInf
}
func (bss *BlockedServicesSchedule) Equals(o *BlockedServicesSchedule) bool {
return utils.JsonEquals(bss, o)
return utils.JSONEquals(bss, o)
}
func (bss *BlockedServicesSchedule) ServicesString() string {
@@ -449,9 +436,9 @@ func (c *DNSConfig) Sanitize(l *zap.SugaredLogger) {
}
}
// Equals GetStatsConfigResponse equal check
// Equals GetStatsConfigResponse equal check.
func (sc *GetStatsConfigResponse) Equals(o *GetStatsConfigResponse) bool {
return utils.JsonEquals(sc, o)
return utils.JSONEquals(sc, o)
}
func NewStats() *Stats {
@@ -482,19 +469,19 @@ func (s *Stats) Add(other *Stats) {
s.ReplacedSafebrowsing = sumUp(s.ReplacedSafebrowsing, other.ReplacedSafebrowsing)
}
func addInt(t *int, add *int) *int {
func addInt(t, add *int) *int {
if add != nil {
return ptr.To(*t + *add)
}
return t
}
func sumUp(t *[]int, o *[]int) *[]int {
func sumUp(t, o *[]int) *[]int {
if o != nil {
tt := *t
oo := *o
var sum []int
for i := 0; i < len(tt); i++ {
for i := range tt {
if len(oo) >= i {
sum = append(sum, tt[i]+oo[i])
}

View File

@@ -91,6 +91,15 @@ const (
QueryLogConfigIntervalN90 QueryLogConfigInterval = 90
)
// Defines values for QueryLogItemClientProto.
const (
Dnscrypt QueryLogItemClientProto = "dnscrypt"
Doh QueryLogItemClientProto = "doh"
Doq QueryLogItemClientProto = "doq"
Dot QueryLogItemClientProto = "dot"
Empty QueryLogItemClientProto = ""
)
// Defines values for QueryLogItemReason.
const (
QueryLogItemReasonFilteredBlackList QueryLogItemReason = "FilteredBlackList"
@@ -812,8 +821,8 @@ type QueryLogItem struct {
ClientId *string `json:"client_id,omitempty"`
// ClientInfo Client information for a query log item.
ClientInfo *QueryLogItemClient `json:"client_info,omitempty"`
ClientProto *interface{} `json:"client_proto,omitempty"`
ClientInfo *QueryLogItemClient `json:"client_info,omitempty"`
ClientProto *QueryLogItemClientProto `json:"client_proto,omitempty"`
// Ecs The IP network defined by an EDNS Client-Subnet option in the request message if any.
Ecs *string `json:"ecs,omitempty"`
@@ -854,6 +863,9 @@ type QueryLogItem struct {
Upstream *string `json:"upstream,omitempty"`
}
// QueryLogItemClientProto defines model for QueryLogItem.ClientProto.
type QueryLogItemClientProto string
// QueryLogItemReason Request filtering status.
type QueryLogItemReason string
@@ -989,8 +1001,8 @@ type SetRulesRequest struct {
type Stats struct {
// AvgProcessingTime Average time in seconds on processing a DNS request
AvgProcessingTime *float32 `json:"avg_processing_time,omitempty"`
BlockedFiltering *[]int `json:"blocked_filtering,omitempty"`
DnsQueries *[]int `json:"dns_queries,omitempty"`
BlockedFiltering *[]int `faker:"slice_len=24" json:"blocked_filtering,omitempty"`
DnsQueries *[]int `faker:"slice_len=24" json:"dns_queries,omitempty"`
// NumBlockedFiltering Number of requests blocked by filtering rules
NumBlockedFiltering *int `json:"num_blocked_filtering,omitempty"`
@@ -1006,8 +1018,8 @@ type Stats struct {
// NumReplacedSafesearch Number of requests blocked by safesearch module
NumReplacedSafesearch *int `json:"num_replaced_safesearch,omitempty"`
ReplacedParental *[]int `json:"replaced_parental,omitempty"`
ReplacedSafebrowsing *[]int `json:"replaced_safebrowsing,omitempty"`
ReplacedParental *[]int `faker:"slice_len=24" json:"replaced_parental,omitempty"`
ReplacedSafebrowsing *[]int `faker:"slice_len=24" json:"replaced_safebrowsing,omitempty"`
// TimeUnits Time units
TimeUnits *StatsTimeUnits `json:"time_units,omitempty"`
@@ -1187,7 +1199,13 @@ type ClientsFindParams struct {
// FilteringCheckHostParams defines parameters for FilteringCheckHost.
type FilteringCheckHostParams struct {
// Name Filter by host name
Name *string `form:"name,omitempty" json:"name,omitempty"`
Name string `form:"name" json:"name"`
// Client Optional ClientID or client IP address
Client *string `form:"client,omitempty" json:"client,omitempty"`
// Qtype Optional DNS type
Qtype *string `form:"qtype,omitempty" json:"qtype,omitempty"`
}
// QueryLogParams defines parameters for QueryLog.
@@ -4244,9 +4262,37 @@ func NewFilteringCheckHostRequest(server string, params *FilteringCheckHostParam
if params != nil {
queryValues := queryURL.Query()
if params.Name != nil {
if queryFrag, err := runtime.StyleParamWithLocation("form", true, "name", runtime.ParamLocationQuery, params.Name); err != nil {
return nil, err
} else if parsed, err := url.ParseQuery(queryFrag); err != nil {
return nil, err
} else {
for k, v := range parsed {
for _, v2 := range v {
queryValues.Add(k, v2)
}
}
}
if queryFrag, err := runtime.StyleParamWithLocation("form", true, "name", runtime.ParamLocationQuery, *params.Name); err != nil {
if params.Client != nil {
if queryFrag, err := runtime.StyleParamWithLocation("form", true, "client", runtime.ParamLocationQuery, *params.Client); err != nil {
return nil, err
} else if parsed, err := url.ParseQuery(queryFrag); err != nil {
return nil, err
} else {
for k, v := range parsed {
for _, v2 := range v {
queryValues.Add(k, v2)
}
}
}
}
if params.Qtype != nil {
if queryFrag, err := runtime.StyleParamWithLocation("form", true, "qtype", runtime.ParamLocationQuery, *params.Qtype); err != nil {
return nil, err
} else if parsed, err := url.ParseQuery(queryFrag); err != nil {
return nil, err

View File

@@ -1,11 +1,12 @@
package model
import (
"github.com/bakito/adguardhome-sync/pkg/log"
"github.com/bakito/adguardhome-sync/pkg/utils"
. "github.com/onsi/ginkgo/v2"
"github.com/onsi/gomega"
"go.uber.org/zap"
"github.com/bakito/adguardhome-sync/pkg/log"
"github.com/bakito/adguardhome-sync/pkg/utils"
)
var _ = Describe("Types", func() {

View File

@@ -4,12 +4,13 @@ import (
"encoding/json"
"os"
"github.com/bakito/adguardhome-sync/pkg/client/model"
"github.com/bakito/adguardhome-sync/pkg/types"
"github.com/bakito/adguardhome-sync/pkg/utils"
"github.com/google/uuid"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/bakito/adguardhome-sync/pkg/client/model"
"github.com/bakito/adguardhome-sync/pkg/types"
"github.com/bakito/adguardhome-sync/pkg/utils"
)
var _ = Describe("Types", func() {

View File

@@ -36,6 +36,9 @@
"webURL": {
"format": "uri",
"type": "string"
},
"requestHeaders": {
"type": "object"
}
},
"type": "object"

View File

@@ -4,9 +4,10 @@ import (
"errors"
"regexp"
"github.com/caarlos0/env/v11"
"github.com/bakito/adguardhome-sync/pkg/log"
"github.com/bakito/adguardhome-sync/pkg/types"
"github.com/caarlos0/env/v11"
)
var (
@@ -14,14 +15,32 @@ var (
logger = log.GetLogger("config")
)
func Get(configFile string, flags Flags) (*types.Config, string, string, error) {
type AppConfig struct {
cfg *types.Config
filePath string
content string
}
func (ac *AppConfig) PrintConfigOnly() bool {
return ac.cfg.PrintConfigOnly
}
func (ac *AppConfig) Get() *types.Config {
return ac.cfg
}
func (ac *AppConfig) Init() error {
return ac.cfg.Init()
}
func Get(configFile string, flags Flags) (*AppConfig, error) {
path, err := configFilePath(configFile)
if err != nil {
return nil, "", "", err
return nil, err
}
if err = validateSchema(path); err != nil {
return nil, "", "", err
if err := validateSchema(path); err != nil {
return nil, err
}
cfg := initialConfig()
@@ -29,12 +48,12 @@ func Get(configFile string, flags Flags) (*types.Config, string, string, error)
// read yaml config
var content string
if content, err = readFile(cfg, path); err != nil {
return nil, "", "", err
return nil, err
}
// overwrite from command flags
if err := readFlags(cfg, flags); err != nil {
return nil, "", "", err
return nil, err
}
// *bool field creates issues when already not nil
@@ -44,18 +63,27 @@ func Get(configFile string, flags Flags) (*types.Config, string, string, error)
replicaDhcpServer := cfg.Replica.DHCPServerEnabled
cfg.Replica.DHCPServerEnabled = nil
// ignore replicas form env parsing as they are handled separately
// ignore origin and replicas form env parsing as they are handled separately
replicas := cfg.Replicas
cfg.Replicas = nil
replica := cfg.Replica
cfg.Replica = nil
origin := cfg.Origin
cfg.Origin = nil
// overwrite from env vars
if err = env.Parse(cfg); err != nil {
return nil, "", "", err
if err := env.Parse(cfg); err != nil {
return nil, err
}
if err = env.ParseWithOptions(cfg.Replica, env.Options{Prefix: "REPLICA_"}); err != nil {
return nil, "", "", err
if err := env.ParseWithOptions(origin, env.Options{Prefix: "ORIGIN_"}); err != nil {
return nil, err
}
// restore the replica
if err := env.ParseWithOptions(replica, env.Options{Prefix: "REPLICA_"}); err != nil {
return nil, err
}
// restore origin and replica
cfg.Origin = origin
cfg.Replica = replica
cfg.Replicas = replicas
// if not set from env, use previous value
@@ -63,10 +91,6 @@ func Get(configFile string, flags Flags) (*types.Config, string, string, error)
cfg.Replica.DHCPServerEnabled = replicaDhcpServer
}
if err = env.ParseWithOptions(&cfg.Origin, env.Options{Prefix: "ORIGIN_"}); err != nil {
return nil, "", "", err
}
if cfg.Replica != nil &&
cfg.Replica.URL == "" &&
cfg.Replica.Username == "" {
@@ -74,7 +98,7 @@ func Get(configFile string, flags Flags) (*types.Config, string, string, error)
}
if len(cfg.Replicas) > 0 && cfg.Replica != nil {
return nil, "", "", errors.New("mixed replica config in use. " +
return nil, errors.New("mixed replica config in use. " +
"Do not use single replica and numbered (list) replica config combined")
}
@@ -87,13 +111,13 @@ func Get(configFile string, flags Flags) (*types.Config, string, string, error)
cfg.Replicas, err = enrichReplicasFromEnv(cfg.Replicas)
return cfg, path, content, err
return &AppConfig{cfg: cfg, filePath: path, content: content}, err
}
func initialConfig() *types.Config {
return &types.Config{
RunOnStart: true,
Origin: types.AdGuardInstance{
Origin: &types.AdGuardInstance{
APIPath: "/control",
},
Replica: &types.AdGuardInstance{

View File

@@ -5,9 +5,11 @@ import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/format"
)
func TestCmd(t *testing.T) {
format.TruncatedDiff = false
RegisterFailHandler(Fail)
RunSpecs(t, "Config Suite")
}

View File

@@ -3,11 +3,12 @@ package config_test
import (
"os"
"github.com/bakito/adguardhome-sync/pkg/config"
flagsmock "github.com/bakito/adguardhome-sync/pkg/mocks/flags"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
gm "go.uber.org/mock/gomock"
"github.com/bakito/adguardhome-sync/pkg/config"
flagsmock "github.com/bakito/adguardhome-sync/pkg/mocks/flags"
)
var _ = Describe("Config", func() {
@@ -16,7 +17,7 @@ var _ = Describe("Config", func() {
flags *flagsmock.MockFlags
mockCtrl *gm.Controller
changedEnvVars []string
setEnv = func(name string, value string) {
setEnv = func(name, value string) {
_ = os.Setenv(name, value)
changedEnvVars = append(changedEnvVars, name)
}
@@ -38,29 +39,39 @@ var _ = Describe("Config", func() {
It("should have the origin URL from the config file", func() {
flags.EXPECT().Changed(gm.Any()).Return(false).AnyTimes()
_, _, _, err := config.Get("../../testdata/config_test_replicas_and_replica.yaml", flags)
_, err := config.Get("../../testdata/config_test_replicas_and_replica.yaml", flags)
Ω(err).Should(HaveOccurred())
Ω(err.Error()).Should(ContainSubstring("mixed replica config in use"))
})
})
Context("Env Var Clash", func() {
It("should not use USERNAME env variable if it is defined (#570)", func() {
incorrect := "ThisIsNotTheCorrectUsername"
setEnv("USERNAME", incorrect)
flags.EXPECT().Changed(gm.Any()).Return(false).AnyTimes()
c, err := config.Get("../../testdata/config_test_replica.yaml", flags)
Ω(err).ShouldNot(HaveOccurred())
Ω(c.Get().Origin.Username).ShouldNot(Equal(incorrect))
Ω(c.Get().Replicas[0].Username).ShouldNot(Equal(incorrect))
})
})
Context("Origin Url", func() {
It("should have the origin URL from the config file", func() {
flags.EXPECT().Changed(gm.Any()).Return(false).AnyTimes()
cfg, path, content, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
cfg, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Origin.URL).Should(Equal("https://origin-file:443"))
Ω(path).Should(Equal("../../testdata/config_test_replicas.yaml"))
Ω(content).ShouldNot(BeEmpty())
Ω(cfg.Get().Origin.URL).Should(Equal("https://origin-file:443"))
})
It("should have the origin URL from the config flags", func() {
flags.EXPECT().Changed(config.FlagOriginURL).Return(true).AnyTimes()
flags.EXPECT().Changed(gm.Any()).Return(false).AnyTimes()
flags.EXPECT().GetString(config.FlagOriginURL).Return("https://origin-flag:443", nil).AnyTimes()
cfg, _, _, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
cfg, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Origin.URL).Should(Equal("https://origin-flag:443"))
Ω(cfg.Get().Origin.URL).Should(Equal("https://origin-flag:443"))
})
It("should have the origin URL from the config env var", func() {
setEnv("ORIGIN_URL", "https://origin-env:443")
@@ -68,27 +79,27 @@ var _ = Describe("Config", func() {
flags.EXPECT().Changed(gm.Any()).Return(false).AnyTimes()
flags.EXPECT().GetString(config.FlagOriginURL).Return("https://origin-flag:443", nil).AnyTimes()
cfg, _, _, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
cfg, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Origin.URL).Should(Equal("https://origin-env:443"))
Ω(cfg.Get().Origin.URL).Should(Equal("https://origin-env:443"))
})
})
Context("Replica insecure skip verify", func() {
It("should have the insecure skip verify from the config file", func() {
flags.EXPECT().Changed(gm.Any()).Return(false).AnyTimes()
cfg, _, _, err := config.Get("../../testdata/config_test_replica.yaml", flags)
cfg, err := config.Get("../../testdata/config_test_replica.yaml", flags)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Replicas[0].InsecureSkipVerify).Should(BeFalse())
Ω(cfg.Get().Replicas[0].InsecureSkipVerify).Should(BeFalse())
})
It("should have the insecure skip verify from the config flags", func() {
flags.EXPECT().Changed(config.FlagReplicaISV).Return(true).AnyTimes()
flags.EXPECT().Changed(gm.Any()).Return(false).AnyTimes()
flags.EXPECT().GetBool(config.FlagReplicaISV).Return(true, nil).AnyTimes()
cfg, _, _, err := config.Get("../../testdata/config_test_replica.yaml", flags)
cfg, err := config.Get("../../testdata/config_test_replica.yaml", flags)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Replicas[0].InsecureSkipVerify).Should(BeTrue())
Ω(cfg.Get().Replicas[0].InsecureSkipVerify).Should(BeTrue())
})
It("should have the insecure skip verify from the config env var", func() {
setEnv("REPLICA_INSECURE_SKIP_VERIFY", "false")
@@ -96,9 +107,9 @@ var _ = Describe("Config", func() {
flags.EXPECT().Changed(gm.Any()).Return(false).AnyTimes()
flags.EXPECT().GetBool(config.FlagReplicaISV).Return(true, nil).AnyTimes()
cfg, _, _, err := config.Get("../../testdata/config_test_replica.yaml", flags)
cfg, err := config.Get("../../testdata/config_test_replica.yaml", flags)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Replicas[0].InsecureSkipVerify).Should(BeFalse())
Ω(cfg.Get().Replicas[0].InsecureSkipVerify).Should(BeFalse())
})
})
@@ -106,44 +117,44 @@ var _ = Describe("Config", func() {
It("should have the insecure skip verify from the config file", func() {
flags.EXPECT().Changed(gm.Any()).Return(false).AnyTimes()
cfg, _, _, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
cfg, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Replicas[0].InsecureSkipVerify).Should(BeFalse())
Ω(cfg.Get().Replicas[0].InsecureSkipVerify).Should(BeFalse())
})
It("should have the insecure skip verify from the config env var", func() {
setEnv("REPLICA1_INSECURE_SKIP_VERIFY", "true")
flags.EXPECT().Changed(gm.Any()).Return(false).AnyTimes()
cfg, _, _, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
cfg, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Replicas[0].InsecureSkipVerify).Should(BeTrue())
Ω(cfg.Get().Replicas[0].InsecureSkipVerify).Should(BeTrue())
})
})
Context("API Port", func() {
It("should have the api port from the config file", func() {
flags.EXPECT().Changed(gm.Any()).Return(false).AnyTimes()
cfg, _, _, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
cfg, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.API.Port).Should(Equal(9090))
Ω(cfg.Get().API.Port).Should(Equal(9090))
})
It("should have the api port from the config flags", func() {
flags.EXPECT().Changed(config.FlagApiPort).Return(true).AnyTimes()
flags.EXPECT().Changed(config.FlagAPIPort).Return(true).AnyTimes()
flags.EXPECT().Changed(gm.Any()).Return(false).AnyTimes()
flags.EXPECT().GetInt(config.FlagApiPort).Return(9990, nil).AnyTimes()
flags.EXPECT().GetInt(config.FlagAPIPort).Return(9990, nil).AnyTimes()
cfg, _, _, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
cfg, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.API.Port).Should(Equal(9990))
Ω(cfg.Get().API.Port).Should(Equal(9990))
})
It("should have the api port from the config env var", func() {
setEnv("API_PORT", "9999")
flags.EXPECT().Changed(config.FlagApiPort).Return(true).AnyTimes()
flags.EXPECT().Changed(config.FlagAPIPort).Return(true).AnyTimes()
flags.EXPECT().Changed(gm.Any()).Return(false).AnyTimes()
flags.EXPECT().GetInt(config.FlagApiPort).Return(9990, nil).AnyTimes()
flags.EXPECT().GetInt(config.FlagAPIPort).Return(9990, nil).AnyTimes()
cfg, _, _, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
cfg, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.API.Port).Should(Equal(9999))
Ω(cfg.Get().API.Port).Should(Equal(9999))
})
})
@@ -151,10 +162,10 @@ var _ = Describe("Config", func() {
It("should have the dhcp server enabled from the config file", func() {
flags.EXPECT().Changed(gm.Any()).Return(false).AnyTimes()
cfg, _, _, err := config.Get("../../testdata/config_test_replica.yaml", flags)
cfg, err := config.Get("../../testdata/config_test_replica.yaml", flags)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Replicas[0].DHCPServerEnabled).ShouldNot(BeNil())
Ω(*cfg.Replicas[0].DHCPServerEnabled).Should(BeFalse())
Ω(cfg.Get().Replicas[0].DHCPServerEnabled).ShouldNot(BeNil())
Ω(*cfg.Get().Replicas[0].DHCPServerEnabled).Should(BeFalse())
})
})
@@ -162,75 +173,96 @@ var _ = Describe("Config", func() {
It("should have the dhcp server enabled from the config file", func() {
flags.EXPECT().Changed(gm.Any()).Return(false).AnyTimes()
cfg, _, _, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
cfg, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Replicas[0].DHCPServerEnabled).ShouldNot(BeNil())
Ω(*cfg.Replicas[0].DHCPServerEnabled).Should(BeFalse())
Ω(cfg.Get().Replicas[0].DHCPServerEnabled).ShouldNot(BeNil())
Ω(*cfg.Get().Replicas[0].DHCPServerEnabled).Should(BeFalse())
})
})
Context("API Port", func() {
It("should have the api port from the config file", func() {
flags.EXPECT().Changed(gm.Any()).Return(false).AnyTimes()
cfg, _, _, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
cfg, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.API.Port).Should(Equal(9090))
Ω(cfg.Get().API.Port).Should(Equal(9090))
})
It("should have the api port from the config flags", func() {
flags.EXPECT().Changed(config.FlagApiPort).Return(true).AnyTimes()
flags.EXPECT().Changed(config.FlagAPIPort).Return(true).AnyTimes()
flags.EXPECT().Changed(gm.Any()).Return(false).AnyTimes()
flags.EXPECT().GetInt(config.FlagApiPort).Return(9990, nil).AnyTimes()
flags.EXPECT().GetInt(config.FlagAPIPort).Return(9990, nil).AnyTimes()
cfg, _, _, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
cfg, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.API.Port).Should(Equal(9990))
Ω(cfg.Get().API.Port).Should(Equal(9990))
})
It("should have the api port from the config env var", func() {
setEnv("API_PORT", "9999")
flags.EXPECT().Changed(config.FlagApiPort).Return(true).AnyTimes()
flags.EXPECT().Changed(config.FlagAPIPort).Return(true).AnyTimes()
flags.EXPECT().Changed(gm.Any()).Return(false).AnyTimes()
flags.EXPECT().GetInt(config.FlagApiPort).Return(9990, nil).AnyTimes()
flags.EXPECT().GetInt(config.FlagAPIPort).Return(9990, nil).AnyTimes()
cfg, _, _, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
cfg, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.API.Port).Should(Equal(9999))
Ω(cfg.Get().API.Port).Should(Equal(9999))
})
})
Context("Feature DNS Server Config", func() {
It("should have the feature dns server config from the config file", func() {
flags.EXPECT().Changed(gm.Any()).Return(false).AnyTimes()
cfg, _, _, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
cfg, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Features.DNS.ServerConfig).Should(BeFalse())
Ω(cfg.Get().Features.DNS.ServerConfig).Should(BeFalse())
})
It("should have the feature dns server config from the config flags", func() {
flags.EXPECT().Changed(config.FlagFeatureDnsServerConfig).Return(true).AnyTimes()
flags.EXPECT().Changed(config.FlagFeatureDNSServerConfig).Return(true).AnyTimes()
flags.EXPECT().Changed(gm.Any()).Return(false).AnyTimes()
flags.EXPECT().GetBool(config.FlagFeatureDnsServerConfig).Return(true, nil).AnyTimes()
flags.EXPECT().GetBool(config.FlagFeatureDNSServerConfig).Return(true, nil).AnyTimes()
cfg, _, _, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
cfg, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Features.DNS.ServerConfig).Should(BeTrue())
Ω(cfg.Get().Features.DNS.ServerConfig).Should(BeTrue())
})
It("should have the feature dns server config from the config env var", func() {
setEnv("FEATURES_DNS_SERVER_CONFIG", "false")
flags.EXPECT().Changed(config.FlagFeatureDnsServerConfig).Return(true).AnyTimes()
flags.EXPECT().Changed(config.FlagFeatureDNSServerConfig).Return(true).AnyTimes()
flags.EXPECT().Changed(gm.Any()).Return(false).AnyTimes()
flags.EXPECT().GetBool(config.FlagFeatureDnsServerConfig).Return(true, nil).AnyTimes()
flags.EXPECT().GetBool(config.FlagFeatureDNSServerConfig).Return(true, nil).AnyTimes()
cfg, _, _, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
cfg, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Features.DNS.ServerConfig).Should(BeFalse())
Ω(cfg.Get().Features.DNS.ServerConfig).Should(BeFalse())
})
It("should have the feature dns server config from the config DEPRECATED env var", func() {
setEnv("FEATURES_DNS_SERVERCONFIG", "false")
flags.EXPECT().Changed(config.FlagFeatureDnsServerConfig).Return(true).AnyTimes()
flags.EXPECT().Changed(config.FlagFeatureDNSServerConfig).Return(true).AnyTimes()
flags.EXPECT().Changed(gm.Any()).Return(false).AnyTimes()
flags.EXPECT().GetBool(config.FlagFeatureDnsServerConfig).Return(true, nil).AnyTimes()
flags.EXPECT().GetBool(config.FlagFeatureDNSServerConfig).Return(true, nil).AnyTimes()
cfg, _, _, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
cfg, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Features.DNS.ServerConfig).Should(BeFalse())
Ω(cfg.Get().Features.DNS.ServerConfig).Should(BeFalse())
})
})
Context("Headers", func() {
It("have headers from the config file", func() {
flags.EXPECT().Changed(gm.Any()).Return(false).AnyTimes()
cfg, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Get().Replicas[0].RequestHeaders).Should(HaveLen(2))
Ω(cfg.Get().Replicas[0].RequestHeaders["FOO"]).Should(Equal("bar"))
Ω(cfg.Get().Replicas[0].RequestHeaders["Client-ID"]).Should(Equal("xxxx"))
})
It("have headers from the config file will be replaced when defined as ENV", func() {
setEnv("REPLICA1_REQUEST_HEADERS", "AAA:bbb")
flags.EXPECT().Changed(gm.Any()).Return(false).AnyTimes()
cfg, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Get().Replicas[0].RequestHeaders).Should(HaveLen(1))
Ω(cfg.Get().Replicas[0].RequestHeaders["AAA"]).Should(Equal("bbb"))
})
})
})

View File

@@ -4,10 +4,10 @@ import (
"fmt"
"os"
"github.com/bakito/adguardhome-sync/pkg/config"
"github.com/bakito/adguardhome-sync/pkg/types"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/bakito/adguardhome-sync/pkg/config"
)
var envVars = []string{
@@ -57,7 +57,7 @@ var _ = Describe("Config", func() {
})
Context("Get", func() {
It("features should be false", func() {
cfg, _, _, err := config.Get("", nil)
cfg, err := config.Get("", nil)
Ω(err).ShouldNot(HaveOccurred())
verifyFeatures(cfg, false)
})
@@ -76,12 +76,12 @@ var _ = Describe("Config", func() {
})
Context("Get", func() {
It("features should be true by default", func() {
cfg, _, _, err := config.Get("", nil)
cfg, err := config.Get("", nil)
Ω(err).ShouldNot(HaveOccurred())
verifyFeatures(cfg, true)
})
It("features should be true by default", func() {
cfg, _, _, err := config.Get("", nil)
cfg, err := config.Get("", nil)
Ω(err).ShouldNot(HaveOccurred())
verifyFeatures(cfg, true)
})
@@ -89,7 +89,7 @@ var _ = Describe("Config", func() {
for _, envVar := range envVars {
Ω(os.Setenv(envVar, "false")).ShouldNot(HaveOccurred())
}
cfg, _, _, err := config.Get("", nil)
cfg, err := config.Get("", nil)
Ω(err).ShouldNot(HaveOccurred())
verifyFeatures(cfg, false)
})
@@ -97,43 +97,43 @@ var _ = Describe("Config", func() {
It("should set interface name of replica 1", func() {
Ω(os.Setenv("REPLICA1_URL", "https://foo.bar")).ShouldNot(HaveOccurred())
Ω(os.Setenv(fmt.Sprintf("REPLICA%s_INTERFACE_NAME", "1"), "eth0")).ShouldNot(HaveOccurred())
cfg, _, _, err := config.Get("", nil)
cfg, err := config.Get("", nil)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Replicas[0].InterfaceName).Should(Equal("eth0"))
Ω(cfg.Get().Replicas[0].InterfaceName).Should(Equal("eth0"))
})
})
Context("dhcp server", func() {
It("should enable the dhcp server of replica 1", func() {
Ω(os.Setenv("REPLICA1_URL", "https://foo.bar")).ShouldNot(HaveOccurred())
Ω(os.Setenv(fmt.Sprintf("REPLICA%s_DHCPSERVERENABLED", "1"), "true")).ShouldNot(HaveOccurred())
cfg, _, _, err := config.Get("", nil)
cfg, err := config.Get("", nil)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Replicas[0].DHCPServerEnabled).ShouldNot(BeNil())
Ω(*cfg.Replicas[0].DHCPServerEnabled).Should(BeTrue())
Ω(cfg.Get().Replicas[0].DHCPServerEnabled).ShouldNot(BeNil())
Ω(*cfg.Get().Replicas[0].DHCPServerEnabled).Should(BeTrue())
})
It("should disable the dhcp server of replica 1", func() {
Ω(os.Setenv("REPLICA1_URL", "https://foo.bar")).ShouldNot(HaveOccurred())
Ω(os.Setenv(fmt.Sprintf("REPLICA%s_DHCPSERVERENABLED", "1"), "false")).ShouldNot(HaveOccurred())
cfg, _, _, err := config.Get("", nil)
cfg, err := config.Get("", nil)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Replicas[0].DHCPServerEnabled).ShouldNot(BeNil())
Ω(*cfg.Replicas[0].DHCPServerEnabled).Should(BeFalse())
Ω(cfg.Get().Replicas[0].DHCPServerEnabled).ShouldNot(BeNil())
Ω(*cfg.Get().Replicas[0].DHCPServerEnabled).Should(BeFalse())
})
})
})
})
})
func verifyFeatures(cfg *types.Config, value bool) {
Ω(cfg.Features.GeneralSettings).Should(Equal(value))
Ω(cfg.Features.QueryLogConfig).Should(Equal(value))
Ω(cfg.Features.StatsConfig).Should(Equal(value))
Ω(cfg.Features.ClientSettings).Should(Equal(value))
Ω(cfg.Features.Services).Should(Equal(value))
Ω(cfg.Features.Filters).Should(Equal(value))
Ω(cfg.Features.DHCP.ServerConfig).Should(Equal(value))
Ω(cfg.Features.DHCP.StaticLeases).Should(Equal(value))
Ω(cfg.Features.DNS.ServerConfig).Should(Equal(value))
Ω(cfg.Features.DNS.AccessLists).Should(Equal(value))
Ω(cfg.Features.DNS.Rewrites).Should(Equal(value))
func verifyFeatures(cfg *config.AppConfig, value bool) {
Ω(cfg.Get().Features.GeneralSettings).Should(Equal(value))
Ω(cfg.Get().Features.QueryLogConfig).Should(Equal(value))
Ω(cfg.Get().Features.StatsConfig).Should(Equal(value))
Ω(cfg.Get().Features.ClientSettings).Should(Equal(value))
Ω(cfg.Get().Features.Services).Should(Equal(value))
Ω(cfg.Get().Features.Filters).Should(Equal(value))
Ω(cfg.Get().Features.DHCP.ServerConfig).Should(Equal(value))
Ω(cfg.Get().Features.DHCP.StaticLeases).Should(Equal(value))
Ω(cfg.Get().Features.DNS.ServerConfig).Should(Equal(value))
Ω(cfg.Get().Features.DNS.AccessLists).Should(Equal(value))
Ω(cfg.Get().Features.DNS.Rewrites).Should(Equal(value))
}

View File

@@ -6,9 +6,10 @@ import (
"strconv"
"strings"
"github.com/caarlos0/env/v11"
"github.com/bakito/adguardhome-sync/pkg/types"
"github.com/bakito/adguardhome-sync/pkg/utils"
"github.com/caarlos0/env/v11"
)
func handleDeprecatedEnvVars(cfg *types.Config) {
@@ -61,20 +62,20 @@ func handleDeprecatedEnvVars(cfg *types.Config) {
}
}
func checkDeprecatedEnvVar(oldName string, newName string) (string, bool) {
func checkDeprecatedEnvVar(oldName, newName string) (string, bool) {
old, oldOK := os.LookupEnv(oldName)
if oldOK {
logger.With("deprecated", oldName, "replacement", newName).
Warn("Deprecated env variable is used, please use the correct one")
}
new, newOK := os.LookupEnv(newName)
newVal, newOK := os.LookupEnv(newName)
if newOK {
return new, true
return newVal, true
}
return old, oldOK
}
func checkDeprecatedReplicaEnvVar(oldPattern string, newPattern string, replicaID int) (string, bool) {
func checkDeprecatedReplicaEnvVar(oldPattern, newPattern string, replicaID int) (string, bool) {
return checkDeprecatedEnvVar(fmt.Sprintf(oldPattern, replicaID), fmt.Sprintf(newPattern, replicaID))
}

View File

@@ -4,8 +4,9 @@ import (
"os"
"path/filepath"
"github.com/bakito/adguardhome-sync/pkg/types"
"gopkg.in/yaml.v3"
"github.com/bakito/adguardhome-sync/pkg/types"
)
func readFile(cfg *types.Config, path string) (string, error) {

View File

@@ -10,9 +10,6 @@ import (
)
var _ = Describe("Config", func() {
var ()
BeforeEach(func() {
})
Context("configFilePath", func() {
It("should return the same value", func() {
path := uuid.NewString()
@@ -22,11 +19,12 @@ var _ = Describe("Config", func() {
Ω(result).Should(Equal(path))
})
It("should the file in HOME dir", func() {
home := os.Getenv("HOME")
home, err := os.UserHomeDir()
Ω(err).ShouldNot(HaveOccurred())
result, err := configFilePath("")
Ω(err).ShouldNot(HaveOccurred())
Ω(result).Should(Equal(filepath.Join(home, "/.adguardhome-sync.yaml")))
Ω(result).Should(Equal(filepath.Join(home, ".adguardhome-sync.yaml")))
})
})
})

View File

@@ -6,16 +6,16 @@ const (
FlagPrintConfigOnly = "printConfigOnly"
FlagContinueOnError = "continueOnError"
FlagApiPort = "api-port"
FlagApiUsername = "api-username"
FlagApiPassword = "api-password"
FlagApiDarkMode = "api-dark-mode"
FlagAPIPort = "api-port"
FlagAPIUsername = "api-username"
FlagAPIPassword = "api-password"
FlagAPIDarkMode = "api-dark-mode"
FlagFeatureDhcpServerConfig = "feature-dhcp-server-config"
FlagFeatureDhcpStaticLeases = "feature-dhcp-static-leases"
FlagFeatureDnsServerConfig = "feature-dns-server-config"
FlagFeatureDnsAccessLists = "feature-dns-access-lists"
FlagFeatureDnsRewrites = "feature-dns-rewrites"
FlagFeatureDNSServerConfig = "feature-dns-server-config"
FlagFeatureDNSAccessLists = "feature-dns-access-lists"
FlagFeatureDNSRewrites = "feature-dns-rewrites"
FlagFeatureGeneral = "feature-general-settings"
FlagFeatureQueryLog = "feature-query-log-config"
FlagFeatureStats = "feature-stats-config"
@@ -25,7 +25,7 @@ const (
FlagOriginURL = "origin-url"
FlagOriginWebURL = "origin-web-url"
FlagOriginApiPath = "origin-api-path"
FlagOriginAPIPath = "origin-api-path"
FlagOriginUsername = "origin-username"
FlagOriginPassword = "origin-password"
@@ -34,7 +34,7 @@ const (
FlagReplicaURL = "replica-url"
FlagReplicaWebURL = "replica-web-url"
FlagReplicaApiPath = "replica-api-path"
FlagReplicaAPIPath = "replica-api-path"
FlagReplicaUsername = "replica-username"
FlagReplicaPassword = "replica-password"
FlagReplicaCookie = "replica-cookie"

View File

@@ -18,7 +18,7 @@ func readFlags(cfg *types.Config, flags Flags) error {
return err
}
if err := fr.readApiFlags(); err != nil {
if err := fr.readAPIFlags(); err != nil {
return err
}
@@ -30,11 +30,7 @@ func readFlags(cfg *types.Config, flags Flags) error {
return err
}
if err := fr.readReplicaFlags(); err != nil {
return err
}
return nil
return fr.readReplicaFlags()
}
type flagReader struct {
@@ -53,7 +49,7 @@ func (fr *flagReader) readReplicaFlags() error {
}); err != nil {
return err
}
if err := fr.setStringFlag(FlagReplicaApiPath, func(cgf *types.Config, value string) {
if err := fr.setStringFlag(FlagReplicaAPIPath, func(cgf *types.Config, value string) {
fr.cfg.Replica.APIPath = value
}); err != nil {
return err
@@ -83,12 +79,9 @@ func (fr *flagReader) readReplicaFlags() error {
}); err != nil {
return err
}
if err := fr.setStringFlag(FlagReplicaInterfaceName, func(cgf *types.Config, value string) {
return fr.setStringFlag(FlagReplicaInterfaceName, func(cgf *types.Config, value string) {
fr.cfg.Replica.InterfaceName = value
}); err != nil {
return err
}
return nil
})
}
func (fr *flagReader) readOriginFlags() error {
@@ -102,7 +95,7 @@ func (fr *flagReader) readOriginFlags() error {
}); err != nil {
return err
}
if err := fr.setStringFlag(FlagOriginApiPath, func(cgf *types.Config, value string) {
if err := fr.setStringFlag(FlagOriginAPIPath, func(cgf *types.Config, value string) {
fr.cfg.Origin.APIPath = value
}); err != nil {
return err
@@ -122,12 +115,9 @@ func (fr *flagReader) readOriginFlags() error {
}); err != nil {
return err
}
if err := fr.setBoolFlag(FlagOriginISV, func(cgf *types.Config, value bool) {
return fr.setBoolFlag(FlagOriginISV, func(cgf *types.Config, value bool) {
fr.cfg.Origin.InsecureSkipVerify = value
}); err != nil {
return err
}
return nil
})
}
func (fr *flagReader) readFeatureFlags() error {
@@ -142,17 +132,17 @@ func (fr *flagReader) readFeatureFlags() error {
return err
}
if err := fr.setBoolFlag(FlagFeatureDnsServerConfig, func(cgf *types.Config, value bool) {
if err := fr.setBoolFlag(FlagFeatureDNSServerConfig, func(cgf *types.Config, value bool) {
fr.cfg.Features.DNS.ServerConfig = value
}); err != nil {
return err
}
if err := fr.setBoolFlag(FlagFeatureDnsAccessLists, func(cgf *types.Config, value bool) {
if err := fr.setBoolFlag(FlagFeatureDNSAccessLists, func(cgf *types.Config, value bool) {
fr.cfg.Features.DNS.AccessLists = value
}); err != nil {
return err
}
if err := fr.setBoolFlag(FlagFeatureDnsRewrites, func(cgf *types.Config, value bool) {
if err := fr.setBoolFlag(FlagFeatureDNSRewrites, func(cgf *types.Config, value bool) {
fr.cfg.Features.DNS.Rewrites = value
}); err != nil {
return err
@@ -183,61 +173,51 @@ func (fr *flagReader) readFeatureFlags() error {
}); err != nil {
return err
}
if err := fr.setBoolFlag(FlagFeatureFilters, func(cgf *types.Config, value bool) {
return fr.setBoolFlag(FlagFeatureFilters, func(cgf *types.Config, value bool) {
fr.cfg.Features.Filters = value
})
}
func (fr *flagReader) readAPIFlags() error {
if err := fr.setIntFlag(FlagAPIPort, func(cgf *types.Config, value int) {
fr.cfg.API.Port = value
}); err != nil {
return err
}
return nil
}
func (fr *flagReader) readApiFlags() (err error) {
if err = fr.setIntFlag(FlagApiPort, func(cgf *types.Config, value int) {
fr.cfg.API.Port = value
}); err != nil {
return
}
if err = fr.setStringFlag(FlagApiUsername, func(cgf *types.Config, value string) {
if err := fr.setStringFlag(FlagAPIUsername, func(cgf *types.Config, value string) {
fr.cfg.API.Username = value
}); err != nil {
return
return err
}
if err = fr.setStringFlag(FlagApiPassword, func(cgf *types.Config, value string) {
if err := fr.setStringFlag(FlagAPIPassword, func(cgf *types.Config, value string) {
fr.cfg.API.Password = value
}); err != nil {
return
return err
}
if err = fr.setBoolFlag(FlagApiDarkMode, func(cgf *types.Config, value bool) {
return fr.setBoolFlag(FlagAPIDarkMode, func(cgf *types.Config, value bool) {
fr.cfg.API.DarkMode = value
}); err != nil {
return
}
return
})
}
func (fr *flagReader) readRootFlags() (err error) {
if err = fr.setStringFlag(FlagCron, func(cgf *types.Config, value string) {
func (fr *flagReader) readRootFlags() error {
if err := fr.setStringFlag(FlagCron, func(cgf *types.Config, value string) {
fr.cfg.Cron = value
}); err != nil {
return
return err
}
if err = fr.setBoolFlag(FlagRunOnStart, func(cgf *types.Config, value bool) {
if err := fr.setBoolFlag(FlagRunOnStart, func(cgf *types.Config, value bool) {
fr.cfg.RunOnStart = value
}); err != nil {
return
return err
}
if err = fr.setBoolFlag(FlagPrintConfigOnly, func(cgf *types.Config, value bool) {
if err := fr.setBoolFlag(FlagPrintConfigOnly, func(cgf *types.Config, value bool) {
fr.cfg.PrintConfigOnly = value
}); err != nil {
return
return err
}
if err = fr.setBoolFlag(FlagContinueOnError, func(cgf *types.Config, value bool) {
return fr.setBoolFlag(FlagContinueOnError, func(cgf *types.Config, value bool) {
fr.cfg.ContinueOnError = value
}); err != nil {
return
}
return
})
}
type Flags interface {
@@ -249,33 +229,33 @@ type Flags interface {
func (fr *flagReader) setStringFlag(name string, cb callback[string]) (err error) {
if fr.flags.Changed(name) {
if value, err := fr.flags.GetString(name); err != nil {
var value string
if value, err = fr.flags.GetString(name); err != nil {
return err
} else {
cb(fr.cfg, value)
}
cb(fr.cfg, value)
}
return nil
}
func (fr *flagReader) setBoolFlag(name string, cb callback[bool]) (err error) {
if fr.flags.Changed(name) {
if value, err := fr.flags.GetBool(name); err != nil {
var value bool
if value, err = fr.flags.GetBool(name); err != nil {
return err
} else {
cb(fr.cfg, value)
}
cb(fr.cfg, value)
}
return nil
}
func (fr *flagReader) setIntFlag(name string, cb callback[int]) (err error) {
if fr.flags.Changed(name) {
if value, err := fr.flags.GetInt(name); err != nil {
var value int
if value, err = fr.flags.GetInt(name); err != nil {
return err
} else {
cb(fr.cfg, value)
}
cb(fr.cfg, value)
}
return nil
}

View File

@@ -3,11 +3,12 @@ package config
import (
"strings"
flagsmock "github.com/bakito/adguardhome-sync/pkg/mocks/flags"
"github.com/bakito/adguardhome-sync/pkg/types"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
gm "go.uber.org/mock/gomock"
flagsmock "github.com/bakito/adguardhome-sync/pkg/mocks/flags"
"github.com/bakito/adguardhome-sync/pkg/types"
)
var _ = Describe("Config", func() {
@@ -18,6 +19,7 @@ var _ = Describe("Config", func() {
)
BeforeEach(func() {
cfg = &types.Config{
Origin: &types.AdGuardInstance{},
Replica: &types.AdGuardInstance{},
Features: types.Features{
DNS: types.DNS{
@@ -88,7 +90,7 @@ var _ = Describe("Config", func() {
}))
})
})
Context("readApiFlags", func() {
Context("readAPIFlags", func() {
It("should change all values", func() {
cfg.API = types.API{
Port: 1111,
@@ -99,10 +101,10 @@ var _ = Describe("Config", func() {
flags.EXPECT().Changed(gm.Any()).DoAndReturn(func(name string) bool {
return strings.HasPrefix(name, "api")
}).AnyTimes()
flags.EXPECT().GetInt(FlagApiPort).Return(9999, nil)
flags.EXPECT().GetString(FlagApiUsername).Return("aaaa", nil)
flags.EXPECT().GetString(FlagApiPassword).Return("bbbb", nil)
flags.EXPECT().GetBool(FlagApiDarkMode).Return(true, nil)
flags.EXPECT().GetInt(FlagAPIPort).Return(9999, nil)
flags.EXPECT().GetString(FlagAPIUsername).Return("aaaa", nil)
flags.EXPECT().GetString(FlagAPIPassword).Return("bbbb", nil)
flags.EXPECT().GetBool(FlagAPIDarkMode).Return(true, nil)
err := readFlags(cfg, flags)
Ω(err).ShouldNot(HaveOccurred())
@@ -142,7 +144,7 @@ var _ = Describe("Config", func() {
})
Context("readOriginFlags", func() {
It("should change all values", func() {
cfg.Origin = types.AdGuardInstance{
cfg.Origin = &types.AdGuardInstance{
URL: "1",
WebURL: "2",
APIPath: "3",
@@ -154,7 +156,7 @@ var _ = Describe("Config", func() {
flags.EXPECT().Changed(FlagOriginURL).Return(true)
flags.EXPECT().Changed(FlagOriginWebURL).Return(true)
flags.EXPECT().Changed(FlagOriginApiPath).Return(true)
flags.EXPECT().Changed(FlagOriginAPIPath).Return(true)
flags.EXPECT().Changed(FlagOriginUsername).Return(true)
flags.EXPECT().Changed(FlagOriginPassword).Return(true)
flags.EXPECT().Changed(FlagOriginCookie).Return(true)
@@ -163,7 +165,7 @@ var _ = Describe("Config", func() {
flags.EXPECT().GetString(FlagOriginURL).Return("a", nil)
flags.EXPECT().GetString(FlagOriginWebURL).Return("b", nil)
flags.EXPECT().GetString(FlagOriginApiPath).Return("c", nil)
flags.EXPECT().GetString(FlagOriginAPIPath).Return("c", nil)
flags.EXPECT().GetString(FlagOriginUsername).Return("d", nil)
flags.EXPECT().GetString(FlagOriginPassword).Return("e", nil)
flags.EXPECT().GetString(FlagOriginCookie).Return("f", nil)
@@ -171,7 +173,7 @@ var _ = Describe("Config", func() {
err := readFlags(cfg, flags)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Origin).Should(Equal(types.AdGuardInstance{
Ω(cfg.Origin).Should(Equal(&types.AdGuardInstance{
URL: "a",
WebURL: "b",
APIPath: "c",
@@ -198,7 +200,7 @@ var _ = Describe("Config", func() {
flags.EXPECT().Changed(FlagReplicaURL).Return(true)
flags.EXPECT().Changed(FlagReplicaWebURL).Return(true)
flags.EXPECT().Changed(FlagReplicaApiPath).Return(true)
flags.EXPECT().Changed(FlagReplicaAPIPath).Return(true)
flags.EXPECT().Changed(FlagReplicaUsername).Return(true)
flags.EXPECT().Changed(FlagReplicaPassword).Return(true)
flags.EXPECT().Changed(FlagReplicaCookie).Return(true)
@@ -209,7 +211,7 @@ var _ = Describe("Config", func() {
flags.EXPECT().GetString(FlagReplicaURL).Return("a", nil)
flags.EXPECT().GetString(FlagReplicaWebURL).Return("b", nil)
flags.EXPECT().GetString(FlagReplicaApiPath).Return("c", nil)
flags.EXPECT().GetString(FlagReplicaAPIPath).Return("c", nil)
flags.EXPECT().GetString(FlagReplicaUsername).Return("d", nil)
flags.EXPECT().GetString(FlagReplicaPassword).Return("e", nil)
flags.EXPECT().GetString(FlagReplicaCookie).Return("f", nil)

View File

@@ -0,0 +1,89 @@
package config
import (
"bytes"
_ "embed"
"os"
"runtime"
"sort"
"strings"
"text/template"
"gopkg.in/yaml.v3"
"github.com/bakito/adguardhome-sync/pkg/client"
"github.com/bakito/adguardhome-sync/pkg/types"
"github.com/bakito/adguardhome-sync/version"
)
//go:embed print-config.md
var printConfigTemplate string
func (ac *AppConfig) Print() error {
originVersion := aghVersion(*ac.cfg.Origin)
var replicaVersions []string
for _, replica := range ac.cfg.Replicas {
replicaVersions = append(replicaVersions, aghVersion(replica))
}
out, err := ac.printInternal(os.Environ(), originVersion, replicaVersions)
if err != nil {
return err
}
logger.Infof(
"Printing adguardhome-sync aggregated config (THE APPLICATION WILL NOT START IN THIS MODE):\n%s",
out,
)
return nil
}
func aghVersion(i types.AdGuardInstance) string {
cl, err := client.New(i)
if err != nil {
return "N/A"
}
stats, err := cl.Status()
if err != nil {
return "N/A"
}
return stats.Version
}
func (ac *AppConfig) printInternal(env []string, originVersion string, replicaVersions []string) (string, error) {
config, err := yaml.Marshal(ac.Get())
if err != nil {
return "", err
}
funcMap := template.FuncMap{
// The name "inc" is what the function will be called in the template text.
"inc": func(i int) int {
return i + 1
},
}
t, err := template.New("printConfigTemplate").Funcs(funcMap).Parse(printConfigTemplate)
if err != nil {
return "", err
}
sort.Strings(env)
var buf bytes.Buffer
err = t.Execute(&buf, map[string]any{
"Version": version.Version,
"Build": version.Build,
"OperatingSystem": runtime.GOOS,
"Architecture": runtime.GOARCH,
"AggregatedConfig": string(config),
"ConfigFilePath": ac.filePath,
"ConfigFileContent": ac.content,
"EnvironmentVariables": strings.Join(env, "\n"),
"OriginVersion": originVersion,
"ReplicaVersions": replicaVersions,
})
return buf.String(), err
}

View File

@@ -0,0 +1,36 @@
<!-- PLEASE COPY THE FOLLOWING OUTPUT AS IS INTO THE GITHUB ISSUE (Don't forget to mask your usernames, passwords, IPs and other sensitive information when using this in an issue ) -->
### Runtime
AdguardHome-Sync Version: {{ .Version }}
Build: {{ .Build }}
OperatingSystem: {{ .OperatingSystem }}
Architecture: {{ .Architecture }}
OriginVersion: {{ .OriginVersion }}
ReplicaVersions:
{{- range $i,$rep := .ReplicaVersions }}
- Replica {{ inc $i }}: {{ $rep }}
{{- end }}
### AdGuardHome sync aggregated config
```yaml
{{ .AggregatedConfig }}
```
{{- if .ConfigFilePath }}
### AdGuardHome sync unmodified config file
Config file path: {{ .ConfigFilePath }}
```yaml
{{ .ConfigFileContent }}
```
{{- end }}
### Environment Variables
```ini
{{ .EnvironmentVariables }}
```
<!-- END OF GITHUB ISSUE CONTENT -->

View File

@@ -0,0 +1,60 @@
package config
import (
_ "embed"
"fmt"
"os"
"path/filepath"
"runtime"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/bakito/adguardhome-sync/pkg/test/matchers"
"github.com/bakito/adguardhome-sync/pkg/types"
"github.com/bakito/adguardhome-sync/version"
)
var _ = Describe("AppConfig", func() {
var (
ac *AppConfig
env []string
)
BeforeEach(func() {
ac = &AppConfig{
cfg: &types.Config{
Origin: &types.AdGuardInstance{
URL: "https://ha.xxxx.net:3000",
},
},
content: `
origin:
url: https://ha.xxxx.net:3000
`,
}
env = []string{"FOO=foo", "BAR=bar"}
})
Context("printInternal", func() {
It("should printInternal config without file", func() {
out, err := ac.printInternal(env, "v0.0.1", []string{"v0.0.2"})
Ω(err).ShouldNot(HaveOccurred())
Ω(out).
Should(matchers.EqualIgnoringLineEndings(fmt.Sprintf(expected(1), version.Version, version.Build, runtime.GOOS, runtime.GOARCH)))
})
It("should printInternal config with file", func() {
ac.filePath = "config.yaml"
out, err := ac.printInternal(env, "v0.0.1", []string{"v0.0.2"})
Ω(err).ShouldNot(HaveOccurred())
Ω(out).
Should(matchers.EqualIgnoringLineEndings(fmt.Sprintf(expected(2), version.Version, version.Build, runtime.GOOS, runtime.GOARCH)))
})
})
})
func expected(id int) string {
b, err := os.ReadFile(
filepath.Join("..", "..", "testdata", "config", fmt.Sprintf("print-config_test_expected%d.md", id)),
)
Ω(err).ShouldNot(HaveOccurred())
return string(b)
}

View File

@@ -2,6 +2,7 @@ package config
import (
_ "embed"
"fmt"
"os"
"strings"
@@ -17,20 +18,26 @@ var schemaData string
func validateSchema(cfgFile string) error {
// ignore if file not exists
if _, err := os.Stat(cfgFile); err != nil {
// Config file does not exist or is not readable - ignore it
//nolint:nilerr
return nil
}
// Load YAML file
yamlContent, err := os.ReadFile(cfgFile)
if err != nil {
return err
return fmt.Errorf("config file %q is invalid: %w", cfgFile, err)
}
return validateYAML(yamlContent)
}
func validateYAML(yamlContent []byte) error {
if yamlContent == nil || strings.TrimSpace(string(yamlContent)) == "" {
return nil
}
// Convert YAML to JSON
var yamlData interface{}
var yamlData any
err := yaml.Unmarshal(yamlContent, &yamlData)
if err != nil {
return err

View File

@@ -1,11 +1,12 @@
package config
import (
"github.com/bakito/adguardhome-sync/pkg/types"
"github.com/go-faker/faker/v4"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"gopkg.in/yaml.v3"
"github.com/bakito/adguardhome-sync/pkg/types"
)
var _ = Describe("Config", func() {
@@ -35,5 +36,10 @@ var _ = Describe("Config", func() {
err = validateYAML(data)
Ω(err).ShouldNot(HaveOccurred())
})
It("validate config with empty file", func() {
var data []byte
err := validateYAML(data)
Ω(err).ShouldNot(HaveOccurred())
})
})
})

View File

@@ -18,7 +18,7 @@ var (
logs []string
)
// GetLogger returns a named logger
// GetLogger returns a named logger.
func GetLogger(name string) *zap.SugaredLogger {
return rootLogger.Named(name).Sugar()
}
@@ -101,12 +101,12 @@ func (l *logList) Sync() error {
return nil
}
// Logs get the current logs
// Logs get the current logs.
func Logs() []string {
return logs
}
// Clear the current logs
// Clear the current logs.
func Clear() {
logs = nil
}

View File

@@ -1,9 +1,10 @@
package metrics
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/bakito/adguardhome-sync/pkg/client/model"
"github.com/bakito/adguardhome-sync/pkg/log"
"github.com/prometheus/client_golang/prometheus"
)
const StatsTotal = "total"
@@ -11,7 +12,7 @@ const StatsTotal = "total"
var (
l = log.GetLogger("metrics")
// avgProcessingTime - Average processing time for a DNS query
// avgProcessingTime - Average processing time for a DNS query.
avgProcessingTime = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "avg_processing_time",
@@ -21,7 +22,7 @@ var (
[]string{"hostname"},
)
// dnsQueries - Number of DNS queries
// dnsQueries - Number of DNS queries.
dnsQueries = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "num_dns_queries",
@@ -31,7 +32,7 @@ var (
[]string{"hostname"},
)
// blockedFiltering - Number of DNS queries blocked
// blockedFiltering - Number of DNS queries blocked.
blockedFiltering = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "num_blocked_filtering",
@@ -41,7 +42,7 @@ var (
[]string{"hostname"},
)
// parentalFiltering - Number of DNS queries replaced by parental control
// parentalFiltering - Number of DNS queries replaced by parental control.
parentalFiltering = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "num_replaced_parental",
@@ -51,7 +52,7 @@ var (
[]string{"hostname"},
)
// safeBrowsingFiltering - Number of DNS queries replaced by safe browsing
// safeBrowsingFiltering - Number of DNS queries replaced by safe browsing.
safeBrowsingFiltering = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "num_replaced_safebrowsing",
@@ -61,7 +62,7 @@ var (
[]string{"hostname"},
)
// safeSearchFiltering - Number of DNS queries replaced by safe search
// safeSearchFiltering - Number of DNS queries replaced by safe search.
safeSearchFiltering = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "num_replaced_safesearch",
@@ -71,7 +72,7 @@ var (
[]string{"hostname"},
)
// topQueries - The number of top queries
// topQueries - The number of top queries.
topQueries = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "top_queried_domains",
@@ -81,7 +82,7 @@ var (
[]string{"hostname", "domain"},
)
// topBlocked - The number of top domains blocked
// topBlocked - The number of top domains blocked.
topBlocked = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "top_blocked_domains",
@@ -91,7 +92,7 @@ var (
[]string{"hostname", "domain"},
)
// topClients - The number of top clients
// topClients - The number of top clients.
topClients = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "top_clients",
@@ -111,7 +112,7 @@ var (
[]string{"hostname", "type"},
)
// running - If Adguard is running
// running - If Adguard is running.
running = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "running",
@@ -121,7 +122,7 @@ var (
[]string{"hostname"},
)
// protectionEnabled - If Adguard protection is enabled
// protectionEnabled - If Adguard protection is enabled.
protectionEnabled = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "protection_enabled",
@@ -130,7 +131,24 @@ var (
},
[]string{"hostname"},
)
// aghsSyncDuration - the sync curation in seconds.
aghsSyncDuration = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "sync_duration_seconds",
Namespace: "adguard_home_sync",
Help: "This represents the duration of the last sync in seconds",
},
[]string{"hostname"},
)
// aghsSyncSuccessful - the sync result.
aghsSyncSuccessful = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "sync_successful",
Namespace: "adguard_home_sync",
Help: "This represents the whether the last sync was successful",
},
[]string{"hostname"},
)
stats = OverallStats{}
)
@@ -148,6 +166,8 @@ func Init() {
initMetric("query_types", queryTypes)
initMetric("running", running)
initMetric("protection_enabled", protectionEnabled)
initMetric("sync_duration_seconds", aghsSyncDuration)
initMetric("sync_successful", aghsSyncSuccessful)
}
func initMetric(name string, metric *prometheus.GaugeVec) {
@@ -155,24 +175,33 @@ func initMetric(name string, metric *prometheus.GaugeVec) {
l.With("name", name).Info("New Prometheus metric registered")
}
func Update(ims ...InstanceMetrics) {
for _, im := range ims {
update(im)
func UpdateInstances(iml InstanceMetricsList) {
for _, im := range iml.Metrics {
updateMetrics(im)
stats[im.HostName] = im.Stats
}
l.Debug("updated")
}
func update(im InstanceMetrics) {
func UpdateResult(host string, ok bool, duration float64) {
if ok {
aghsSyncSuccessful.WithLabelValues(host).Set(1)
} else {
aghsSyncSuccessful.WithLabelValues(host).Set(0)
}
aghsSyncDuration.WithLabelValues(host).Set(duration)
}
func updateMetrics(im InstanceMetrics) {
// Status
var isRunning int = 0
isRunning := 0
if im.Status.Running {
isRunning = 1
}
running.WithLabelValues(im.HostName).Set(float64(isRunning))
var isProtected int = 0
isProtected := 0
if im.Status.ProtectionEnabled {
isProtected = 1
}
@@ -218,7 +247,7 @@ func update(im InstanceMetrics) {
if len(dnsanswer) > 0 {
for _, dnsa := range dnsanswer {
dnsType := *dnsa.Type
m[dnsType] += 1
m[dnsType]++
}
}
}
@@ -230,6 +259,10 @@ func update(im InstanceMetrics) {
}
}
type InstanceMetricsList struct {
Metrics []InstanceMetrics `faker:"slice_len=5"`
}
type InstanceMetrics struct {
HostName string
Status *model.ServerStatus
@@ -255,7 +288,7 @@ func safeMetric[T int | float64 | float32](v *T) float64 {
return float64(*v)
}
func GetStats() OverallStats {
func getStats() OverallStats {
return stats.consolidate()
}

View File

@@ -0,0 +1,13 @@
package metrics_test
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestSync(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Metrics Suite")
}

102
pkg/metrics/metrics_test.go Normal file
View File

@@ -0,0 +1,102 @@
package metrics
import (
"github.com/go-faker/faker/v4"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"k8s.io/utils/ptr"
"github.com/bakito/adguardhome-sync/pkg/client/model"
)
var _ = Describe("Metrics", func() {
BeforeEach(func() {
stats = make(OverallStats)
})
Context("UpdateInstances / getStats", func() {
It("generate correct stats", func() {
UpdateInstances(InstanceMetricsList{[]InstanceMetrics{
{HostName: "foo", Status: &model.ServerStatus{}, Stats: &model.Stats{
NumDnsQueries: ptr.To(100),
DnsQueries: ptr.To(
[]int{10, 20, 30, 40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
),
}},
{HostName: "bar", Status: &model.ServerStatus{}, Stats: &model.Stats{
NumDnsQueries: ptr.To(200),
DnsQueries: ptr.To(
[]int{20, 40, 60, 80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
),
}},
{HostName: "aaa", Status: &model.ServerStatus{}, Stats: &model.Stats{
NumDnsQueries: ptr.To(300),
DnsQueries: ptr.To(
[]int{30, 60, 90, 120, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
),
}},
}})
Ω(stats).Should(HaveKey("foo"))
Ω(stats["foo"].NumDnsQueries).Should(Equal(ptr.To(100)))
Ω(stats).Should(HaveKey("bar"))
Ω(stats["bar"].NumDnsQueries).Should(Equal(ptr.To(200)))
Ω(stats).Should(HaveKey("aaa"))
Ω(stats["aaa"].NumDnsQueries).Should(Equal(ptr.To(300)))
os := getStats()
tot := os.Total()
Ω(*tot.NumDnsQueries).Should(Equal(600))
Ω(
*tot.DnsQueries,
).Should(Equal([]int{60, 120, 180, 240, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}))
foo := os["foo"]
bar := os["bar"]
aaa := os["aaa"]
Ω(*foo.NumDnsQueries).Should(Equal(100))
Ω(*bar.NumDnsQueries).Should(Equal(200))
Ω(*aaa.NumDnsQueries).Should(Equal(300))
Ω(
*foo.DnsQueries,
).Should(Equal([]int{10, 20, 30, 40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}))
Ω(
*bar.DnsQueries,
).Should(Equal([]int{20, 40, 60, 80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}))
Ω(
*aaa.DnsQueries,
).Should(Equal([]int{30, 60, 90, 120, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}))
})
})
Context("StatsGraph", func() {
var metrics InstanceMetricsList
BeforeEach(func() {
err := faker.FakeData(&metrics)
Ω(err).ShouldNot(HaveOccurred())
})
It("should provide correct results with faked values", func() {
UpdateInstances(metrics)
_, dns, blocked, malware, adult := StatsGraph()
verifyStats(dns)
verifyStats(blocked)
verifyStats(malware)
verifyStats(adult)
})
})
})
func verifyStats(lines []Line) {
var total Line
sum := make([]int, len(lines[0].Data))
for _, l := range lines {
if l.Title == labelTotal {
total = l
} else {
for i, d := range l.Data {
sum[i] += d
}
}
}
Ω(sum).Should(Equal(total.Data))
}

View File

@@ -1,13 +1,14 @@
package sync
package metrics
import (
"slices"
"strings"
"github.com/bakito/adguardhome-sync/pkg/client/model"
"github.com/bakito/adguardhome-sync/pkg/metrics"
)
const labelTotal = "Total"
var (
blue = []int{78, 141, 245}
blueAlternatives = [][]int{
@@ -46,19 +47,19 @@ var (
}
)
func statsGraph() (*model.Stats, []line, []line, []line, []line) {
s := metrics.GetStats()
t := s.Total()
dns := graphLines(t, s, blue, blueAlternatives, func(s *model.Stats) []int {
func StatsGraph() (t *model.Stats, dns, blocked, malware, adult []Line) {
s := getStats()
t = s.Total()
dns = graphLines(t, s, blue, blueAlternatives, func(s *model.Stats) []int {
return safeStats(s.DnsQueries)
})
blocked := graphLines(t, s, red, redAlternatives, func(s *model.Stats) []int {
blocked = graphLines(t, s, red, redAlternatives, func(s *model.Stats) []int {
return safeStats(s.BlockedFiltering)
})
malware := graphLines(t, s, green, greenAlternatives, func(s *model.Stats) []int {
malware = graphLines(t, s, green, greenAlternatives, func(s *model.Stats) []int {
return safeStats(s.ReplacedSafebrowsing)
})
adult := graphLines(t, s, yellow, yellowAlternatives, func(s *model.Stats) []int {
adult = graphLines(t, s, yellow, yellowAlternatives, func(s *model.Stats) []int {
return safeStats(s.ReplacedParental)
})
@@ -74,15 +75,15 @@ func safeStats(stats *[]int) []int {
func graphLines(
t *model.Stats,
s metrics.OverallStats,
s OverallStats,
baseColor []int,
altColors [][]int,
dataCB func(s *model.Stats) []int,
) []line {
) []Line {
g := &graph{
total: line{
total: Line{
Fill: true,
Title: "Total",
Title: labelTotal,
Data: dataCB(t),
R: baseColor[0],
G: baseColor[1],
@@ -92,8 +93,8 @@ func graphLines(
var i int
for name, data := range s {
if name != metrics.StatsTotal {
g.replicas = append(g.replicas, line{
if name != StatsTotal {
g.replicas = append(g.replicas, Line{
Fill: false,
Title: name,
Data: dataCB(data),
@@ -105,9 +106,9 @@ func graphLines(
}
}
lines := []line{g.total}
lines := []Line{g.total}
slices.SortFunc(g.replicas, func(a, b line) int {
slices.SortFunc(g.replicas, func(a, b Line) int {
return strings.Compare(a.Title, b.Title)
})
lines = append(lines, g.replicas...)
@@ -115,11 +116,11 @@ func graphLines(
}
type graph struct {
total line
replicas []line
total Line
replicas []Line
}
type line struct {
type Line struct {
Data []int `json:"data"`
R int `json:"r"`
G int `json:"g"`

View File

@@ -384,17 +384,17 @@ func (mr *MockClientMockRecorder) SafeSearchConfig() *gomock.Call {
}
// SetAccessList mocks base method.
func (m *MockClient) SetAccessList(arg0 *model.AccessList) error {
func (m *MockClient) SetAccessList(accessList *model.AccessList) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetAccessList", arg0)
ret := m.ctrl.Call(m, "SetAccessList", accessList)
ret0, _ := ret[0].(error)
return ret0
}
// SetAccessList indicates an expected call of SetAccessList.
func (mr *MockClientMockRecorder) SetAccessList(arg0 any) *gomock.Call {
func (mr *MockClientMockRecorder) SetAccessList(accessList any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetAccessList", reflect.TypeOf((*MockClient)(nil).SetAccessList), arg0)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetAccessList", reflect.TypeOf((*MockClient)(nil).SetAccessList), accessList)
}
// SetBlockedServicesSchedule mocks base method.
@@ -426,31 +426,31 @@ func (mr *MockClientMockRecorder) SetCustomRules(rules any) *gomock.Call {
}
// SetDNSConfig mocks base method.
func (m *MockClient) SetDNSConfig(arg0 *model.DNSConfig) error {
func (m *MockClient) SetDNSConfig(config *model.DNSConfig) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetDNSConfig", arg0)
ret := m.ctrl.Call(m, "SetDNSConfig", config)
ret0, _ := ret[0].(error)
return ret0
}
// SetDNSConfig indicates an expected call of SetDNSConfig.
func (mr *MockClientMockRecorder) SetDNSConfig(arg0 any) *gomock.Call {
func (mr *MockClientMockRecorder) SetDNSConfig(config any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDNSConfig", reflect.TypeOf((*MockClient)(nil).SetDNSConfig), arg0)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDNSConfig", reflect.TypeOf((*MockClient)(nil).SetDNSConfig), config)
}
// SetDhcpConfig mocks base method.
func (m *MockClient) SetDhcpConfig(arg0 *model.DhcpStatus) error {
func (m *MockClient) SetDhcpConfig(status *model.DhcpStatus) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetDhcpConfig", arg0)
ret := m.ctrl.Call(m, "SetDhcpConfig", status)
ret0, _ := ret[0].(error)
return ret0
}
// SetDhcpConfig indicates an expected call of SetDhcpConfig.
func (mr *MockClientMockRecorder) SetDhcpConfig(arg0 any) *gomock.Call {
func (mr *MockClientMockRecorder) SetDhcpConfig(status any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDhcpConfig", reflect.TypeOf((*MockClient)(nil).SetDhcpConfig), arg0)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDhcpConfig", reflect.TypeOf((*MockClient)(nil).SetDhcpConfig), status)
}
// SetProfileInfo mocks base method.
@@ -468,17 +468,17 @@ func (mr *MockClientMockRecorder) SetProfileInfo(settings any) *gomock.Call {
}
// SetQueryLogConfig mocks base method.
func (m *MockClient) SetQueryLogConfig(arg0 *model.QueryLogConfigWithIgnored) error {
func (m *MockClient) SetQueryLogConfig(ql *model.QueryLogConfigWithIgnored) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetQueryLogConfig", arg0)
ret := m.ctrl.Call(m, "SetQueryLogConfig", ql)
ret0, _ := ret[0].(error)
return ret0
}
// SetQueryLogConfig indicates an expected call of SetQueryLogConfig.
func (mr *MockClientMockRecorder) SetQueryLogConfig(arg0 any) *gomock.Call {
func (mr *MockClientMockRecorder) SetQueryLogConfig(ql any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetQueryLogConfig", reflect.TypeOf((*MockClient)(nil).SetQueryLogConfig), arg0)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetQueryLogConfig", reflect.TypeOf((*MockClient)(nil).SetQueryLogConfig), ql)
}
// SetSafeSearchConfig mocks base method.
@@ -496,7 +496,7 @@ func (mr *MockClientMockRecorder) SetSafeSearchConfig(settings any) *gomock.Call
}
// SetStatsConfig mocks base method.
func (m *MockClient) SetStatsConfig(sc *model.GetStatsConfigResponse) error {
func (m *MockClient) SetStatsConfig(sc *model.PutStatsConfigUpdateRequest) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetStatsConfig", sc)
ret0, _ := ret[0].(error)

View File

@@ -1,10 +1,11 @@
package sync
import (
"go.uber.org/zap"
"github.com/bakito/adguardhome-sync/pkg/client"
"github.com/bakito/adguardhome-sync/pkg/client/model"
"github.com/bakito/adguardhome-sync/pkg/utils"
"go.uber.org/zap"
)
var (

View File

@@ -1,10 +1,11 @@
package sync
import (
"go.uber.org/zap"
"github.com/bakito/adguardhome-sync/pkg/client"
"github.com/bakito/adguardhome-sync/pkg/client/model"
"github.com/bakito/adguardhome-sync/pkg/types"
"go.uber.org/zap"
)
func setupActions(cfg *types.Config) (actions []syncAction) {

View File

@@ -14,10 +14,11 @@ import (
"syscall"
"time"
"github.com/gin-gonic/gin"
"github.com/bakito/adguardhome-sync/pkg/log"
"github.com/bakito/adguardhome-sync/pkg/metrics"
"github.com/bakito/adguardhome-sync/version"
"github.com/gin-gonic/gin"
)
var (
@@ -33,15 +34,15 @@ func (w *worker) handleSync(c *gin.Context) {
}
func (w *worker) handleRoot(c *gin.Context) {
total, dns, blocked, malware, adult := statsGraph()
total, dns, blocked, malware, adult := metrics.StatsGraph()
c.HTML(http.StatusOK, "index.html", map[string]interface{}{
c.HTML(http.StatusOK, "index.html", map[string]any{
"DarkMode": w.cfg.API.DarkMode,
"Metrics": w.cfg.API.Metrics.Enabled,
"Version": version.Version,
"Build": version.Build,
"SyncStatus": w.status(),
"Stats": map[string]interface{}{
"Stats": map[string]any{
"Labels": getLast24Hours(),
"DNS": dns,
"Blocked": blocked,
@@ -169,8 +170,6 @@ func (w *worker) listenAndServe() {
// manually cancel context if not using httpServer.RegisterOnShutdown(cancel)
cancel()
defer os.Exit(0)
}
type syncStatus struct {
@@ -192,7 +191,7 @@ func getLast24Hours() []string {
currentTime := time.Now()
// Loop to get the last 24 hours
for i := 0; i < 24; i++ {
for i := range 24 {
// Calculate the time for the current hour in the loop
timeInstance := currentTime.Add(time.Duration(-i) * time.Hour)
timeInstance = timeInstance.Truncate(time.Hour)

View File

@@ -26,25 +26,26 @@ func (w *worker) startScraping() {
}
func (w *worker) scrape() {
var ims []metrics.InstanceMetrics
var iml metrics.InstanceMetricsList
ims = append(ims, w.getMetrics(w.cfg.Origin))
iml.Metrics = append(iml.Metrics, w.getMetrics(*w.cfg.Origin))
for _, replica := range w.cfg.Replicas {
ims = append(ims, w.getMetrics(replica))
iml.Metrics = append(iml.Metrics, w.getMetrics(replica))
}
metrics.Update(ims...)
metrics.UpdateInstances(iml)
}
func (w *worker) getMetrics(inst types.AdGuardInstance) (im metrics.InstanceMetrics) {
func (w *worker) getMetrics(inst types.AdGuardInstance) metrics.InstanceMetrics {
var im metrics.InstanceMetrics
client, err := w.createClient(inst)
if err != nil {
l.With("error", err, "url", w.cfg.Origin.URL).Error("Error creating origin client")
return
return im
}
im.HostName = inst.Host
im.Status, _ = client.Status()
im.Stats, _ = client.Stats()
im.QueryLog, _ = client.QueryLog(w.cfg.API.Metrics.QueryLogLimit)
return
return im
}

View File

@@ -7,27 +7,29 @@ import (
"sort"
"time"
"github.com/robfig/cron/v3"
"go.uber.org/zap"
"github.com/bakito/adguardhome-sync/pkg/client"
"github.com/bakito/adguardhome-sync/pkg/client/model"
"github.com/bakito/adguardhome-sync/pkg/log"
"github.com/bakito/adguardhome-sync/pkg/metrics"
"github.com/bakito/adguardhome-sync/pkg/types"
"github.com/bakito/adguardhome-sync/pkg/utils"
"github.com/bakito/adguardhome-sync/pkg/versions"
"github.com/bakito/adguardhome-sync/version"
"github.com/robfig/cron/v3"
"go.uber.org/zap"
)
var l = log.GetLogger("sync")
// Sync config from origin to replica
// Sync config from origin to replica.
func Sync(cfg *types.Config) error {
if cfg.Origin.URL == "" {
return fmt.Errorf("origin URL is required")
return errors.New("origin URL is required")
}
if len(cfg.UniqueReplicas()) == 0 {
return fmt.Errorf("no replicas configured")
return errors.New("no replicas configured")
}
l.With(
@@ -41,10 +43,8 @@ func Sync(cfg *types.Config) error {
cfg.Origin.AutoSetup = false
w := &worker{
cfg: cfg,
createClient: func(ai types.AdGuardInstance) (client.Client, error) {
return client.New(ai)
},
cfg: cfg,
createClient: client.New,
}
if cfg.Cron != "" {
w.cron = cron.New()
@@ -100,7 +100,7 @@ type worker struct {
func (w *worker) status() *syncStatus {
syncStatus := &syncStatus{
Origin: w.getStatus(w.cfg.Origin),
Origin: w.getStatus(*w.cfg.Origin),
}
for _, replica := range w.cfg.Replicas {
@@ -120,15 +120,15 @@ func (w *worker) status() *syncStatus {
return syncStatus
}
func (w *worker) getStatus(inst types.AdGuardInstance) (st replicaStatus) {
st = replicaStatus{Host: inst.WebHost, URL: inst.WebURL}
func (w *worker) getStatus(inst types.AdGuardInstance) replicaStatus {
st := replicaStatus{Host: inst.WebHost, URL: inst.WebURL}
oc, err := w.createClient(inst)
if err != nil {
l.With("error", err, "url", w.cfg.Origin.URL).Error("Error creating origin client")
st.Status = "danger"
st.Error = err.Error()
return
return st
}
sl := l.With("from", inst.WebHost)
status, err := oc.Status()
@@ -136,16 +136,16 @@ func (w *worker) getStatus(inst types.AdGuardInstance) (st replicaStatus) {
if errors.Is(err, client.ErrSetupNeeded) {
st.Status = "warning"
st.Error = err.Error()
return
return st
}
sl.With("error", err).Error("Error getting origin status")
st.Status = "danger"
st.Error = err.Error()
return
return st
}
st.Status = "success"
st.ProtectionEnabled = utils.Ptr(status.ProtectionEnabled)
return
return st
}
func (w *worker) sync() {
@@ -154,9 +154,11 @@ func (w *worker) sync() {
return
}
w.running = true
defer func() { w.running = false }()
defer func() {
w.running = false
}()
oc, err := w.createClient(w.cfg.Origin)
oc, err := w.createClient(*w.cfg.Origin)
if err != nil {
l.With("error", err, "url", w.cfg.Origin.URL).Error("Error creating origin client")
return
@@ -271,10 +273,23 @@ func (w *worker) syncTo(l *zap.SugaredLogger, o *origin, replica types.AdGuardIn
rl := l.With("to", rc.Host())
rl.Info("Start sync")
start := time.Now()
withError := false
delta := time.Since(start).Seconds()
defer func() {
metrics.UpdateResult(rc.Host(), !withError, delta)
doneLog := rl.With("duration", fmt.Sprintf("%vs", delta))
if withError {
doneLog.Error("Sync done")
} else {
doneLog.Info("Sync done")
}
}()
replicaStatus, err := w.statusWithSetup(rl, replica, rc)
if err != nil {
rl.With("error", err).Error("Error getting replica status")
withError = true
return
}
@@ -283,6 +298,7 @@ func (w *worker) syncTo(l *zap.SugaredLogger, o *origin, replica types.AdGuardIn
if versions.IsNewerThan(versions.MinAgh, replicaStatus.Version) {
rl.With("error", err, "version", replicaStatus.Version).
Errorf("Replica AdGuard Home version must be >= %s", versions.MinAgh)
withError = true
return
}
@@ -299,16 +315,16 @@ func (w *worker) syncTo(l *zap.SugaredLogger, o *origin, replica types.AdGuardIn
client: rc,
replica: replica,
}
for _, action := range w.actions {
if err := action.sync(ac); err != nil {
rl.With("error", err).Errorf("Error syncing %s", action.name())
withError = true
if !w.cfg.ContinueOnError {
return
}
}
}
rl.Info("Sync done")
}
func (w *worker) statusWithSetup(

View File

@@ -3,16 +3,17 @@ package sync
import (
"errors"
"github.com/google/uuid"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
gm "go.uber.org/mock/gomock"
"github.com/bakito/adguardhome-sync/pkg/client"
"github.com/bakito/adguardhome-sync/pkg/client/model"
clientmock "github.com/bakito/adguardhome-sync/pkg/mocks/client"
"github.com/bakito/adguardhome-sync/pkg/types"
"github.com/bakito/adguardhome-sync/pkg/utils"
"github.com/bakito/adguardhome-sync/pkg/versions"
"github.com/google/uuid"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
gm "go.uber.org/mock/gomock"
)
var _ = Describe("Sync", func() {
@@ -575,7 +576,7 @@ var _ = Describe("Sync", func() {
Context("sync", func() {
BeforeEach(func() {
w.cfg = &types.Config{
Origin: types.AdGuardInstance{},
Origin: &types.AdGuardInstance{},
Replica: &types.AdGuardInstance{URL: "foo"},
Features: types.Features{
DHCP: types.DHCP{
@@ -598,7 +599,7 @@ var _ = Describe("Sync", func() {
})
It("should have no changes", func() {
// origin
cl.EXPECT().Host()
cl.EXPECT().Host().Times(2)
cl.EXPECT().Status().Return(&model.ServerStatus{Version: versions.MinAgh}, nil)
cl.EXPECT().ProfileInfo().Return(&model.ProfileInfo{}, nil)
cl.EXPECT().Parental()
@@ -638,7 +639,7 @@ var _ = Describe("Sync", func() {
w.cfg.Features.DHCP.ServerConfig = false
w.cfg.Features.DHCP.StaticLeases = false
// origin
cl.EXPECT().Host()
cl.EXPECT().Host().Times(2)
cl.EXPECT().Status().Return(&model.ServerStatus{Version: versions.MinAgh}, nil)
cl.EXPECT().ProfileInfo().Return(&model.ProfileInfo{}, nil)
cl.EXPECT().Parental()
@@ -697,7 +698,7 @@ var _ = Describe("Sync", func() {
cl.EXPECT().DhcpConfig().Return(&model.DhcpStatus{}, nil)
// replica
cl.EXPECT().Host()
cl.EXPECT().Host().Times(2)
cl.EXPECT().Status().Return(&model.ServerStatus{Version: "v0.106.9"}, nil)
w.sync()
})

View File

@@ -0,0 +1,44 @@
package matchers
import (
"strings"
"github.com/onsi/gomega/format"
"github.com/onsi/gomega/types"
)
type equalIgnoringLineEndingsMatcher struct {
expected string
}
func (matcher *equalIgnoringLineEndingsMatcher) Match(actual any) (success bool, err error) {
actualStr, ok := actual.(string)
if !ok {
return false, nil
}
normalizedActual := strings.ReplaceAll(actualStr, "\r\n", "\n")
normalizedExpected := strings.ReplaceAll(matcher.expected, "\r\n", "\n")
return normalizedActual == normalizedExpected, nil
}
func (matcher *equalIgnoringLineEndingsMatcher) FailureMessage(actual any) (message string) {
actualString, actualOK := actual.(string)
if actualOK {
return format.MessageWithDiff(actualString, "to equal", matcher.expected)
}
return format.Message(actual, "to equal", matcher.expected)
}
func (matcher *equalIgnoringLineEndingsMatcher) NegatedFailureMessage(actual any) (message string) {
return format.Message(actual, "not to equal", matcher.expected)
}
// EqualIgnoringLineEndings returns a new matcher.
func EqualIgnoringLineEndings(expected string) types.GomegaMatcher {
return &equalIgnoringLineEndingsMatcher{
expected: expected,
}
}

View File

@@ -1,58 +0,0 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
// Code generated by deepcopy-gen. DO NOT EDIT.
package types
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AdGuardInstance) DeepCopyInto(out *AdGuardInstance) {
*out = *in
if in.DHCPServerEnabled != nil {
in, out := &in.DHCPServerEnabled, &out.DHCPServerEnabled
*out = new(bool)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdGuardInstance.
func (in *AdGuardInstance) DeepCopy() *AdGuardInstance {
if in == nil {
return nil
}
out := new(AdGuardInstance)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Config) DeepCopyInto(out *Config) {
*out = *in
in.Origin.DeepCopyInto(&out.Origin)
if in.Replica != nil {
in, out := &in.Replica, &out.Replica
*out = new(AdGuardInstance)
(*in).DeepCopyInto(*out)
}
if in.Replicas != nil {
in, out := &in.Replicas, &out.Replicas
*out = make([]AdGuardInstance, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
out.API = in.API
out.Features = in.Features
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Config.
func (in *Config) DeepCopy() *Config {
if in == nil {
return nil
}
out := new(Config)
in.DeepCopyInto(out)
return out
}

View File

@@ -25,33 +25,33 @@ func NewFeatures(enabled bool) Features {
}
}
// Features feature flags
// Features feature flags.
type Features struct {
DNS DNS `json:"dns" yaml:"dns"`
DHCP DHCP `json:"dhcp" yaml:"dhcp"`
GeneralSettings bool `json:"generalSettings" yaml:"generalSettings" env:"FEATURES_GENERAL_SETTINGS"`
QueryLogConfig bool `json:"queryLogConfig" yaml:"queryLogConfig" env:"FEATURES_QUERY_LOG_CONFIG"`
StatsConfig bool `json:"statsConfig" yaml:"statsConfig" env:"FEATURES_STATS_CONFIG"`
ClientSettings bool `json:"clientSettings" yaml:"clientSettings" env:"FEATURES_CLIENT_SETTINGS"`
Services bool `json:"services" yaml:"services" env:"FEATURES_SERVICES"`
Filters bool `json:"filters" yaml:"filters" env:"FEATURES_FILTERS"`
Theme bool `json:"theme" yaml:"theme" env:"FEATURES_THEME"`
GeneralSettings bool `json:"generalSettings" yaml:"generalSettings" documentation:"Sync general settings" env:"FEATURES_GENERAL_SETTINGS"`
QueryLogConfig bool `json:"queryLogConfig" yaml:"queryLogConfig" documentation:"Sync query log config" env:"FEATURES_QUERY_LOG_CONFIG"`
StatsConfig bool `json:"statsConfig" yaml:"statsConfig" documentation:"Sync stats config" env:"FEATURES_STATS_CONFIG"`
ClientSettings bool `json:"clientSettings" yaml:"clientSettings" documentation:"Sync client settings" env:"FEATURES_CLIENT_SETTINGS"`
Services bool `json:"services" yaml:"services" documentation:"Sync services" env:"FEATURES_SERVICES"`
Filters bool `json:"filters" yaml:"filters" documentation:"Sync filters" env:"FEATURES_FILTERS"`
Theme bool `json:"theme" yaml:"theme" documentation:"Sync the weg UI theme" env:"FEATURES_THEME"`
}
// DHCP features
// DHCP features.
type DHCP struct {
ServerConfig bool `json:"serverConfig" yaml:"serverConfig" env:"FEATURES_DHCP_SERVER_CONFIG"`
StaticLeases bool `json:"staticLeases" yaml:"staticLeases" env:"FEATURES_DHCP_STATIC_LEASES"`
ServerConfig bool `documentation:"Sync DHCP server config" env:"FEATURES_DHCP_SERVER_CONFIG" json:"serverConfig" yaml:"serverConfig"`
StaticLeases bool `documentation:"Sync DHCP static leases" env:"FEATURES_DHCP_STATIC_LEASES" json:"staticLeases" yaml:"staticLeases"`
}
// DNS features
// DNS features.
type DNS struct {
AccessLists bool `json:"accessLists" yaml:"accessLists" env:"FEATURES_DNS_ACCESS_LISTS"`
ServerConfig bool `json:"serverConfig" yaml:"serverConfig" env:"FEATURES_DNS_SERVER_CONFIG"`
Rewrites bool `json:"rewrites" yaml:"rewrites" env:"FEATURES_DNS_REWRITES"`
AccessLists bool `documentation:"Sync DNS access lists" env:"FEATURES_DNS_ACCESS_LISTS" json:"accessLists" yaml:"accessLists"`
ServerConfig bool `documentation:"Sync DNS server config" env:"FEATURES_DNS_SERVER_CONFIG" json:"serverConfig" yaml:"serverConfig"`
Rewrites bool `documentation:"Sync DNS rewrites" env:"FEATURES_DNS_REWRITES" json:"rewrites" yaml:"rewrites"`
}
// LogDisabled log all disabled features
// LogDisabled log all disabled features.
func (f *Features) LogDisabled(l *zap.SugaredLogger) {
features := f.collectDisabled()

View File

@@ -1,3 +1,5 @@
// Package types
// +kubebuilder:object:generate=true
package types
import (
@@ -11,72 +13,75 @@ import (
)
const (
// DefaultAPIPath default api path
// DefaultAPIPath default api path.
DefaultAPIPath = "/control"
)
// Config application configuration struct
// +k8s:deepcopy-gen=true
type Config struct {
Origin AdGuardInstance `json:"origin" yaml:"origin" env:"ORIGIN"`
Replica *AdGuardInstance `json:"replica,omitempty" yaml:"replica,omitempty" env:"REPLICA"`
Replicas []AdGuardInstance `json:"replicas,omitempty" yaml:"replicas,omitempty" faker:"slice_len=2"`
Cron string `json:"cron,omitempty" yaml:"cron,omitempty" env:"CRON"`
RunOnStart bool `json:"runOnStart,omitempty" yaml:"runOnStart,omitempty" env:"RUN_ON_START"`
PrintConfigOnly bool `json:"printConfigOnly,omitempty" yaml:"printConfigOnly,omitempty" env:"PRINT_CONFIG_ONLY"`
ContinueOnError bool `json:"continueOnError,omitempty" yaml:"continueOnError,omitempty" env:"CONTINUE_ON_ERROR"`
API API `json:"api,omitempty" yaml:"api,omitempty" env:"API"`
Features Features `json:"features,omitempty" yaml:"features,omitempty" env:"FEATURES_"`
// Origin adguardhome instance
Origin *AdGuardInstance `json:"origin" yaml:"origin"`
// One single replica adguardhome instance
Replica *AdGuardInstance `json:"replica,omitempty" yaml:"replica,omitempty"`
// Multiple replica instances
Replicas []AdGuardInstance `json:"replicas,omitempty" yaml:"replicas,omitempty" faker:"slice_len=2"`
Cron string `json:"cron,omitempty" yaml:"cron,omitempty" documentation:"Cron expression for the sync interval" env:"CRON"`
RunOnStart bool `json:"runOnStart,omitempty" yaml:"runOnStart,omitempty" documentation:"Run the sung on startup" env:"RUN_ON_START"`
PrintConfigOnly bool `json:"printConfigOnly,omitempty" yaml:"printConfigOnly,omitempty" documentation:"Print current config only and stop the application" env:"PRINT_CONFIG_ONLY"`
ContinueOnError bool `json:"continueOnError,omitempty" yaml:"continueOnError,omitempty" documentation:"Continue sync on errors" env:"CONTINUE_ON_ERROR"`
API API `json:"api,omitempty" yaml:"api,omitempty"`
Features Features `json:"features,omitempty" yaml:"features,omitempty"`
}
// API configuration
// API configuration.
type API struct {
Port int `json:"port,omitempty" yaml:"port,omitempty" env:"API_PORT"`
Username string `json:"username,omitempty" yaml:"username,omitempty" env:"API_USERNAME"`
Password string `json:"password,omitempty" yaml:"password,omitempty" env:"API_PASSWORD"`
DarkMode bool `json:"darkMode,omitempty" yaml:"darkMode,omitempty" env:"API_DARK_MODE"`
Metrics Metrics `json:"metrics,omitempty" yaml:"metrics,omitempty" env:"API_METRICS"`
TLS TLS `json:"tls,omitempty" yaml:"tls,omitempty" env:"API_TLS"`
Port int `documentation:"API port" env:"API_PORT" json:"port,omitempty" yaml:"port,omitempty"`
Username string `documentation:"API username" env:"API_USERNAME" json:"username,omitempty" yaml:"username,omitempty"`
Password string `documentation:"API password" env:"API_PASSWORD" json:"password,omitempty" yaml:"password,omitempty"`
DarkMode bool `documentation:"API dark mode" env:"API_DARK_MODE" json:"darkMode,omitempty" yaml:"darkMode,omitempty"`
Metrics Metrics ` json:"metrics,omitempty" yaml:"metrics,omitempty"`
TLS TLS ` json:"tls,omitempty" yaml:"tls,omitempty"`
}
// Metrics configuration
// Metrics configuration.
type Metrics struct {
Enabled bool `json:"enabled,omitempty" yaml:"enabled,omitempty" env:"API_METRICS_ENABLED"`
ScrapeInterval time.Duration `json:"scrapeInterval,omitempty" yaml:"scrapeInterval,omitempty" env:"API_METRICS_SCRAPE_INTERVAL"`
QueryLogLimit int `json:"queryLogLimit,omitempty" yaml:"queryLogLimit,omitempty" env:"API_METRICS_QUERY_LOG_LIMIT"`
Enabled bool `documentation:"Enable metrics" env:"API_METRICS_ENABLED" json:"enabled,omitempty" yaml:"enabled,omitempty"`
ScrapeInterval time.Duration `documentation:"Interval for metrics scraping" env:"API_METRICS_SCRAPE_INTERVAL" json:"scrapeInterval,omitempty" yaml:"scrapeInterval,omitempty"`
QueryLogLimit int `documentation:"Metrics log query limit" env:"API_METRICS_QUERY_LOG_LIMIT" json:"queryLogLimit,omitempty" yaml:"queryLogLimit,omitempty"`
}
// TLS configuration
// TLS configuration.
type TLS struct {
CertDir string `json:"certDir,omitempty" yaml:"certDir,omitempty" env:"API_TLS_CERT_DIR"`
CertName string `json:"certName,omitempty" yaml:"certName,omitempty" env:"API_TLS_CERT_NAME"`
KeyName string `json:"keyName,omitempty" yaml:"keyName,omitempty" env:"API_TLS_KEY_NAME"`
CertDir string `documentation:"API TLS certificate directory" env:"API_TLS_CERT_DIR" json:"certDir,omitempty" yaml:"certDir,omitempty"`
CertName string `documentation:"API TLS certificate file name" env:"API_TLS_CERT_NAME" json:"certName,omitempty" yaml:"certName,omitempty"`
KeyName string `documentation:"API TLS key file name" env:"API_TLS_KEY_NAME" json:"keyName,omitempty" yaml:"keyName,omitempty"`
}
func (t TLS) Enabled() bool {
return strings.TrimSpace(t.CertDir) != ""
}
func (t TLS) Certs() (cert string, key string) {
func (t TLS) Certs() (cert, key string) {
cert = filepath.Join(t.CertDir, defaultIfEmpty(t.CertName, "tls.crt"))
key = filepath.Join(t.CertDir, defaultIfEmpty(t.KeyName, "tls.key"))
return
return cert, key
}
func defaultIfEmpty(val string, fallback string) string {
func defaultIfEmpty(val, fallback string) string {
if strings.TrimSpace(val) == "" {
return fallback
}
return val
}
// Mask maks username and password
// Mask maks username and password.
func (a *API) Mask() {
a.Username = mask(a.Username)
a.Password = mask(a.Password)
}
// UniqueReplicas get unique replication instances
// UniqueReplicas get unique replication instances.
func (cfg *Config) UniqueReplicas() []AdGuardInstance {
dedup := make(map[string]AdGuardInstance)
if cfg.Replica != nil && cfg.Replica.URL != "" {
@@ -101,7 +106,7 @@ func (cfg *Config) UniqueReplicas() []AdGuardInstance {
return r
}
// Log the current config
// Log the current config.
func (cfg *Config) Log(l *zap.SugaredLogger) {
c := cfg.mask()
l.With("config", c).Debug("Using config")
@@ -140,27 +145,28 @@ func (cfg *Config) Init() error {
// AdGuardInstance AdguardHome config instance
// +k8s:deepcopy-gen=true
type AdGuardInstance struct {
URL string `json:"url" yaml:"url" env:"URL" faker:"url"`
WebURL string `json:"webURL" yaml:"webURL" env:"WEB_URL" faker:"url"`
APIPath string `json:"apiPath,omitempty" yaml:"apiPath,omitempty" env:"API_PATH"`
Username string `json:"username,omitempty" yaml:"username,omitempty" env:"USERNAME"`
Password string `json:"password,omitempty" yaml:"password,omitempty" env:"PASSWORD"`
Cookie string `json:"cookie,omitempty" yaml:"cookie,omitempty" env:"COOKIE"`
InsecureSkipVerify bool `json:"insecureSkipVerify" yaml:"insecureSkipVerify" env:"INSECURE_SKIP_VERIFY"`
AutoSetup bool `json:"autoSetup" yaml:"autoSetup" env:"AUTO_SETUP"`
InterfaceName string `json:"interfaceName,omitempty" yaml:"interfaceName,omitempty" env:"INTERFACE_NAME"`
DHCPServerEnabled *bool `json:"dhcpServerEnabled,omitempty" yaml:"dhcpServerEnabled,omitempty" env:"DHCP_SERVER_ENABLED"`
URL string `documentation:"URL of adguardhome instance" env:"URL" faker:"url" json:"url" yaml:"url"`
WebURL string `documentation:"Web URL of adguardhome instance" env:"WEB_URL" faker:"url" json:"webURL" yaml:"webURL"`
APIPath string `documentation:"API Path" env:"API_PATH" json:"apiPath,omitempty" yaml:"apiPath,omitempty"`
Username string `documentation:"Adguardhome username" env:"USERNAME" json:"username,omitempty" yaml:"username,omitempty"`
Password string `documentation:"Adguardhome password" env:"PASSWORD" json:"password,omitempty" yaml:"password,omitempty"`
Cookie string `documentation:"Adguardhome cookie" env:"COOKIE" json:"cookie,omitempty" yaml:"cookie,omitempty"`
RequestHeaders map[string]string `documentation:"Request Headers 'key1:value1,key2:value2'" env:"REQUEST_HEADERS" json:"requestHeaders,omitempty" yaml:"requestHeaders,omitempty"`
InsecureSkipVerify bool `documentation:"Skip TLS verification" env:"INSECURE_SKIP_VERIFY" json:"insecureSkipVerify" yaml:"insecureSkipVerify"`
AutoSetup bool `documentation:"Automatically setup the instance if it is not initialized" env:"AUTO_SETUP" json:"autoSetup" yaml:"autoSetup"`
InterfaceName string `documentation:"Network interface name" env:"INTERFACE_NAME" json:"interfaceName,omitempty" yaml:"interfaceName,omitempty"`
DHCPServerEnabled *bool `documentation:"Enable DHCP server" env:"DHCP_SERVER_ENABLED" json:"dhcpServerEnabled,omitempty" yaml:"dhcpServerEnabled,omitempty"`
Host string `json:"-" yaml:"-"`
WebHost string `json:"-" yaml:"-"`
}
// Key AdGuardInstance key
// Key AdGuardInstance key.
func (i *AdGuardInstance) Key() string {
return fmt.Sprintf("%s#%s", i.URL, i.APIPath)
}
// Mask maks username and password
// Mask maks username and password.
func (i *AdGuardInstance) Mask() {
i.Username = mask(i.Username)
i.Password = mask(i.Password)
@@ -187,19 +193,19 @@ func (i *AdGuardInstance) Init() error {
}
func mask(s string) string {
if s == "" {
return "***"
if len(s) < 3 {
return strings.Repeat("*", len(s))
}
mask := strings.Repeat("*", len(s)-2)
return fmt.Sprintf("%v%s%v", string(s[0]), mask, string(s[len(s)-1]))
}
// Protection API struct
// Protection API struct.
type Protection struct {
ProtectionEnabled bool `json:"protection_enabled"`
}
// InstallConfig AdguardHome install config
// InstallConfig AdguardHome install config.
type InstallConfig struct {
Web InstallPort `json:"web"`
DNS InstallPort `json:"dns"`
@@ -207,7 +213,7 @@ type InstallConfig struct {
Password string `json:"password"`
}
// InstallPort AdguardHome install config port
// InstallPort AdguardHome install config port.
type InstallPort struct {
IP string `json:"ip"`
Port int `json:"port"`

View File

@@ -0,0 +1,15 @@
package types
import (
"testing"
)
func FuzzMask(f *testing.F) {
testcases := []string{"", "a", "ab", "abc", "abcd"}
for _, tc := range testcases {
f.Add(tc)
}
f.Fuzz(func(t *testing.T, value string) {
_ = mask(value)
})
}

View File

@@ -1,6 +1,8 @@
package types
import (
"strings"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
@@ -39,6 +41,7 @@ var _ = Describe("Types", func() {
Context("Config", func() {
Context("init", func() {
cfg := Config{
Origin: &AdGuardInstance{},
Replicas: []AdGuardInstance{
{URL: "https://localhost:3000"},
},
@@ -53,6 +56,7 @@ var _ = Describe("Types", func() {
Context("UniqueReplicas", func() {
It("should return unique replicas in the array", func() {
cfg := Config{
Origin: &AdGuardInstance{},
Replicas: []AdGuardInstance{
{URL: "a"},
{URL: "a", APIPath: DefaultAPIPath},
@@ -68,6 +72,7 @@ var _ = Describe("Types", func() {
Context("mask", func() {
It("should mask all names and passwords", func() {
cfg := Config{
Origin: &AdGuardInstance{},
Replicas: []AdGuardInstance{
{URL: "a", Username: "user", Password: "pass"},
},
@@ -83,6 +88,15 @@ var _ = Describe("Types", func() {
Ω(masked.API.Password).Should(Equal("p**s"))
})
})
DescribeTable("mask should work correctly",
func(value, expected string) {
Ω(mask(value)).Should(Equal(expected))
},
Entry(`Empty password`, "", ""),
Entry(`1 char password`, "a", "*"),
Entry(`2 char password`, "ab", "**"),
Entry(`3 char password`, "abc", "a*c"),
)
})
Context("Feature", func() {
Context("LogDisabled", func() {
@@ -115,6 +129,8 @@ var _ = Describe("Types", func() {
Context("Certs", func() {
It("should use default crt and key", func() {
crt, key := t.Certs()
crt = normalizePath(crt)
key = normalizePath(key)
Ω(crt).Should(Equal("/path/to/certs/tls.crt"))
Ω(key).Should(Equal("/path/to/certs/tls.key"))
})
@@ -122,9 +138,15 @@ var _ = Describe("Types", func() {
t.CertName = "foo.crt"
t.KeyName = "bar.key"
crt, key := t.Certs()
crt = normalizePath(crt)
key = normalizePath(key)
Ω(crt).Should(Equal("/path/to/certs/foo.crt"))
Ω(key).Should(Equal("/path/to/certs/bar.key"))
})
})
})
})
func normalizePath(path string) string {
return strings.ReplaceAll(path, "\\", "/")
}

View File

@@ -0,0 +1,209 @@
//go:build !ignore_autogenerated
// Code generated by controller-gen. DO NOT EDIT.
package types
import ()
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *API) DeepCopyInto(out *API) {
*out = *in
out.Metrics = in.Metrics
out.TLS = in.TLS
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new API.
func (in *API) DeepCopy() *API {
if in == nil {
return nil
}
out := new(API)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AdGuardInstance) DeepCopyInto(out *AdGuardInstance) {
*out = *in
if in.RequestHeaders != nil {
in, out := &in.RequestHeaders, &out.RequestHeaders
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.DHCPServerEnabled != nil {
in, out := &in.DHCPServerEnabled, &out.DHCPServerEnabled
*out = new(bool)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdGuardInstance.
func (in *AdGuardInstance) DeepCopy() *AdGuardInstance {
if in == nil {
return nil
}
out := new(AdGuardInstance)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Config) DeepCopyInto(out *Config) {
*out = *in
if in.Origin != nil {
in, out := &in.Origin, &out.Origin
*out = new(AdGuardInstance)
(*in).DeepCopyInto(*out)
}
if in.Replica != nil {
in, out := &in.Replica, &out.Replica
*out = new(AdGuardInstance)
(*in).DeepCopyInto(*out)
}
if in.Replicas != nil {
in, out := &in.Replicas, &out.Replicas
*out = make([]AdGuardInstance, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
out.API = in.API
out.Features = in.Features
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Config.
func (in *Config) DeepCopy() *Config {
if in == nil {
return nil
}
out := new(Config)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DHCP) DeepCopyInto(out *DHCP) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DHCP.
func (in *DHCP) DeepCopy() *DHCP {
if in == nil {
return nil
}
out := new(DHCP)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DNS) DeepCopyInto(out *DNS) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNS.
func (in *DNS) DeepCopy() *DNS {
if in == nil {
return nil
}
out := new(DNS)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Features) DeepCopyInto(out *Features) {
*out = *in
out.DNS = in.DNS
out.DHCP = in.DHCP
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Features.
func (in *Features) DeepCopy() *Features {
if in == nil {
return nil
}
out := new(Features)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *InstallConfig) DeepCopyInto(out *InstallConfig) {
*out = *in
out.Web = in.Web
out.DNS = in.DNS
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InstallConfig.
func (in *InstallConfig) DeepCopy() *InstallConfig {
if in == nil {
return nil
}
out := new(InstallConfig)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *InstallPort) DeepCopyInto(out *InstallPort) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InstallPort.
func (in *InstallPort) DeepCopy() *InstallPort {
if in == nil {
return nil
}
out := new(InstallPort)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Metrics) DeepCopyInto(out *Metrics) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Metrics.
func (in *Metrics) DeepCopy() *Metrics {
if in == nil {
return nil
}
out := new(Metrics)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Protection) DeepCopyInto(out *Protection) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Protection.
func (in *Protection) DeepCopy() *Protection {
if in == nil {
return nil
}
out := new(Protection)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TLS) DeepCopyInto(out *TLS) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLS.
func (in *TLS) DeepCopy() *TLS {
if in == nil {
return nil
}
out := new(TLS)
in.DeepCopyInto(out)
return out
}

View File

@@ -1,15 +1,18 @@
package utils
import "encoding/json"
import (
"bytes"
"encoding/json"
)
func Clone[I interface{}](in I, out I) I {
func Clone[I any](in, out I) I {
b, _ := json.Marshal(in)
_ = json.Unmarshal(b, out)
return out
}
func JsonEquals(a interface{}, b interface{}) bool {
func JSONEquals(a, b any) bool {
ja, _ := json.Marshal(a)
jb, _ := json.Marshal(b)
return string(ja) == string(jb)
return bytes.Equal(ja, jb)
}

View File

@@ -2,18 +2,18 @@ package utils
import "fmt"
func Ptr[I interface{}](i I) *I {
func Ptr[I any](i I) *I {
return &i
}
func PtrToString[I interface{}](i *I) string {
func PtrToString[I any](i *I) string {
if i == nil {
return ""
}
return fmt.Sprintf("%v", i)
}
func PtrEquals[I comparable](a *I, b *I) bool {
func PtrEquals[I comparable](a, b *I) bool {
if a == nil && b == nil {
return true
}

View File

@@ -3,15 +3,15 @@ package versions
import "golang.org/x/mod/semver"
const (
// MinAgh minimal adguardhome version
// MinAgh minimal adguardhome version.
MinAgh = "v0.107.40"
)
func IsNewerThan(v1 string, v2 string) bool {
func IsNewerThan(v1, v2 string) bool {
return semver.Compare(sanitize(v1), sanitize(v2)) == 1
}
func IsSame(v1 string, v2 string) bool {
func IsSame(v1, v2 string) bool {
return semver.Compare(sanitize(v1), sanitize(v2)) == 0
}

View File

@@ -1,9 +1,10 @@
package versions_test
import (
"github.com/bakito/adguardhome-sync/pkg/versions"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/bakito/adguardhome-sync/pkg/versions"
)
var _ = Describe("Versions", func() {

View File

@@ -5,8 +5,8 @@
"customType": "regex",
"datasourceTemplate": "go",
"description": "Update toolbox tools in .toolbox.mk",
"fileMatch": [
"^\\.toolbox\\.mk$"
"managerFilePatterns": [
".toolbox.mk"
],
"matchStrings": [
"# renovate: packageName=(?<packageName>.+?)\\s+.+?_VERSION \\?= (?<currentValue>.+?)\\s"
@@ -16,8 +16,8 @@
"customType": "regex",
"datasourceTemplate": "github-releases",
"description": "Update github _VERSION Makefile",
"fileMatch": [
"^Makefile$"
"managerFilePatterns": [
"Makefile"
],
"matchStrings": [
"# renovate: packageName=(?<packageName>.+?)\\s+.+?_VERSION \\?= (?<currentValue>.+?)\\s"

View File

@@ -0,0 +1,31 @@
<!-- PLEASE COPY THE FOLLOWING OUTPUT AS IS INTO THE GITHUB ISSUE (Don't forget to mask your usernames, passwords, IPs and other sensitive information when using this in an issue ) -->
### Runtime
AdguardHome-Sync Version: %s
Build: %s
OperatingSystem: %s
Architecture: %s
OriginVersion: v0.0.1
ReplicaVersions:
- Replica 1: v0.0.2
### AdGuardHome sync aggregated config
```yaml
origin:
url: https://ha.xxxx.net:3000
webURL: ""
insecureSkipVerify: false
autoSetup: false
```
### Environment Variables
```ini
BAR=bar
FOO=foo
```
<!-- END OF GITHUB ISSUE CONTENT -->

View File

@@ -0,0 +1,41 @@
<!-- PLEASE COPY THE FOLLOWING OUTPUT AS IS INTO THE GITHUB ISSUE (Don't forget to mask your usernames, passwords, IPs and other sensitive information when using this in an issue ) -->
### Runtime
AdguardHome-Sync Version: %s
Build: %s
OperatingSystem: %s
Architecture: %s
OriginVersion: v0.0.1
ReplicaVersions:
- Replica 1: v0.0.2
### AdGuardHome sync aggregated config
```yaml
origin:
url: https://ha.xxxx.net:3000
webURL: ""
insecureSkipVerify: false
autoSetup: false
```
### AdGuardHome sync unmodified config file
Config file path: config.yaml
```yaml
origin:
url: https://ha.xxxx.net:3000
```
### Environment Variables
```ini
BAR=bar
FOO=foo
```
<!-- END OF GITHUB ISSUE CONTENT -->

View File

@@ -16,6 +16,9 @@ replicas:
autoSetup: false
interfaceName: eth3
dhcpServerEnabled: false
requestHeaders:
FOO: bar
Client-ID: xxxx
cron: '*/15 * * * *'
runOnStart: true
printConfigOnly: true

16
testdata/test-layout.kdl vendored Normal file
View File

@@ -0,0 +1,16 @@
layout {
pane split_direction="vertical" {
pane name="main"
pane {
pane {
command "make"
args "start-replica"
}
pane {
command "make"
args "start-replica2"
}
}
}
}

View File

@@ -1,8 +1,8 @@
package version
var (
// Version the module version
// Version the module version.
Version = "devel"
// Build the build time
// Build the build time.
Build = "N/A"
)