Compare commits

...

45 Commits

Author SHA1 Message Date
Marc Brugger
3c0115b71c Dhcp server enabled handling (#282)
* switch to go.uber.org/mock

* #281 special handling for dhcpServerEnabled
2024-01-17 19:29:25 +01:00
Marc Brugger
c401c790bc switch to go.uber.org/mock (#280) 2024-01-16 16:46:13 +01:00
dependabot[bot]
271dcacf19 Bump github.com/go-git/go-git/v5 from 5.7.0 to 5.11.0 (#279)
Bumps [github.com/go-git/go-git/v5](https://github.com/go-git/go-git) from 5.7.0 to 5.11.0.
- [Release notes](https://github.com/go-git/go-git/releases)
- [Commits](https://github.com/go-git/go-git/compare/v5.7.0...v5.11.0)

---
updated-dependencies:
- dependency-name: github.com/go-git/go-git/v5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-15 22:06:42 +01:00
dependabot[bot]
983d29d033 Bump github.com/cloudflare/circl from 1.3.5 to 1.3.7 (#278)
Bumps [github.com/cloudflare/circl](https://github.com/cloudflare/circl) from 1.3.5 to 1.3.7.
- [Release notes](https://github.com/cloudflare/circl/releases)
- [Commits](https://github.com/cloudflare/circl/compare/v1.3.5...v1.3.7)

---
updated-dependencies:
- dependency-name: github.com/cloudflare/circl
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-15 22:06:31 +01:00
Marc Brugger
078f6e1cc4 add dependencies to tools.go (#277) 2024-01-15 21:07:56 +01:00
Marc Brugger
9f8f6bc814 run tests with ginkgo cli (#276) 2024-01-15 18:24:10 +01:00
bakito
ef35178396 move deprecations to wiki 2024-01-15 08:13:52 +01:00
dependabot[bot]
b3781c6d8f Bump github.com/onsi/ginkgo/v2 from 2.13.2 to 2.14.0 (#274)
Bumps [github.com/onsi/ginkgo/v2](https://github.com/onsi/ginkgo) from 2.13.2 to 2.14.0.
- [Release notes](https://github.com/onsi/ginkgo/releases)
- [Changelog](https://github.com/onsi/ginkgo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/ginkgo/compare/v2.13.2...v2.14.0)

---
updated-dependencies:
- dependency-name: github.com/onsi/ginkgo/v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-15 07:07:45 +01:00
bakito
2319739f6d ignore wiki from 2024-01-14 21:21:19 +01:00
Marc Brugger
f9ac1ca698 Add e2e status to Readme 2024-01-14 21:02:19 +01:00
bakito
9802bf2f37 add tests #272 2024-01-14 16:38:58 +01:00
bakito
843433e35d extend dhcp checks #272 2024-01-14 16:13:45 +01:00
bakito
15c651ca96 #272 do not sync dhcp if empty IP 2024-01-14 14:03:04 +01:00
Marc Brugger
4c1e56ccce Fix config issues with new env library (#273)
* correct config issues #271 #272

* rename type tags

* replace env lib

* move to config module

* read flags

* show e2e logs on error

* extract env

* replace deprecated env var

* increment index

* check replica numbers do not start with 0

* remove test suite

* error handling

* refactor flags

* flags test

* file test

* file test

* config tests

* extend tests

* test mixed mode

* simplify

* simplify

* test mask

* correct uniqe replicas

* Update types_test.go

* e2e test with file mode
2024-01-14 13:29:36 +01:00
bakito
906dfc680a extend bug issue template wiht 'applied-config' 2024-01-14 09:07:54 +01:00
Marc Brugger
583197f1c8 add deprecated env var table 2024-01-10 20:12:08 +01:00
Marc Brugger
5fca3b1002 better readable env vars (#270)
* better readable env vars
2024-01-08 19:39:47 +01:00
bakito
1edf5ae52a add filter test cases 2024-01-08 19:20:31 +01:00
bakito
39f7f41e6d update readme 2024-01-07 22:44:32 +01:00
Marc Brugger
3139ad605f Refactor sync into separate action functions (#268)
* sync-actions

* dns rewrites and filters

* continue on filter error

* servides

* client settings

* dns

* dhcp

* remove deprecated env var

* fix client tests

* tests

* copy replica config

* map continue on error

* map env var with underscore
2024-01-07 22:03:21 +01:00
bakito
a9de069f6b update dependencies 2024-01-07 10:05:13 +01:00
Marc Brugger
4a8e2aab51 allow definig web URL (#267) 2024-01-07 09:55:21 +01:00
Marc Brugger
749c5f178c add option to print full config for debugging (#266)
* add option to print full config for debugging

* print link to FAQ in auth errors
2024-01-07 03:05:44 +01:00
Marc Brugger
680989bc2e add tzdata to image (#265) 2024-01-05 09:27:36 +01:00
dependabot[bot]
418989989b Bump github.com/go-resty/resty/v2 from 2.9.1 to 2.11.0 (#263)
Bumps [github.com/go-resty/resty/v2](https://github.com/go-resty/resty) from 2.9.1 to 2.11.0.
- [Release notes](https://github.com/go-resty/resty/releases)
- [Commits](https://github.com/go-resty/resty/compare/v2.9.1...v2.11.0)

---
updated-dependencies:
- dependency-name: github.com/go-resty/resty/v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-28 08:07:19 +01:00
dependabot[bot]
9a06a6ac10 Bump github.com/spf13/viper from 1.18.1 to 1.18.2 (#261)
Bumps [github.com/spf13/viper](https://github.com/spf13/viper) from 1.18.1 to 1.18.2.
- [Release notes](https://github.com/spf13/viper/releases)
- [Commits](https://github.com/spf13/viper/compare/v1.18.1...v1.18.2)

---
updated-dependencies:
- dependency-name: github.com/spf13/viper
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-25 07:36:08 +01:00
bakito
ff7bae0bba build main image only once a day if needed 2023-12-22 22:45:50 +01:00
Marc Brugger
215ee946dd add protection flag to api status (#260) 2023-12-22 21:03:46 +01:00
bakito
cc13b9318d correct api status host property from 'origin' to 'host' 2023-12-22 17:26:29 +01:00
bakito
9d26eec6b0 asure cacerts are installed in build image 2023-12-20 20:41:34 +01:00
Marc Brugger
bb969a0005 upgrade agh schema to v0.107.43 (#257) 2023-12-20 07:57:41 +01:00
bakito
37b8fda889 extend issue template description 2023-12-19 21:34:02 +01:00
bakito
0c9487a53d logs are required in template 2023-12-19 19:38:37 +01:00
Marc Brugger
a039704f1b update issue templates (#256) 2023-12-19 19:36:53 +01:00
Marc Brugger
ec9be5aed6 Update general_issue.md (#255) 2023-12-19 17:52:48 +01:00
Marc Brugger
542aacb002 Create general_issue.md (#254)
* Create general_issue.md

* Add bug label
2023-12-19 17:27:03 +01:00
Marc Brugger
e7e391f85c Create config.yml (#253) 2023-12-19 17:11:34 +01:00
dependabot[bot]
8ccd773b4b Bump golang.org/x/crypto from 0.16.0 to 0.17.0 (#251)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.16.0 to 0.17.0.
- [Commits](https://github.com/golang/crypto/compare/v0.16.0...v0.17.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-19 06:44:47 +01:00
dependabot[bot]
cb52a43940 Bump github/codeql-action from 2 to 3 (#249)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-18 06:40:39 +01:00
dependabot[bot]
2463809356 Bump k8s.io/apimachinery from 0.28.4 to 0.29.0 (#250)
Bumps [k8s.io/apimachinery](https://github.com/kubernetes/apimachinery) from 0.28.4 to 0.29.0.
- [Commits](https://github.com/kubernetes/apimachinery/compare/v0.28.4...v0.29.0)

---
updated-dependencies:
- dependency-name: k8s.io/apimachinery
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-18 06:40:22 +01:00
dependabot[bot]
b71df4d8ed Bump github.com/google/uuid from 1.4.0 to 1.5.0 (#248)
Bumps [github.com/google/uuid](https://github.com/google/uuid) from 1.4.0 to 1.5.0.
- [Release notes](https://github.com/google/uuid/releases)
- [Changelog](https://github.com/google/uuid/blob/master/CHANGELOG.md)
- [Commits](https://github.com/google/uuid/compare/v1.4.0...v1.5.0)

---
updated-dependencies:
- dependency-name: github.com/google/uuid
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-12 23:10:10 +01:00
bakito
4cd60e818a downgrade resty 2023-12-12 22:57:12 +01:00
dependabot[bot]
31aad9471b Bump github.com/spf13/viper from 1.17.0 to 1.18.1 (#245)
Bumps [github.com/spf13/viper](https://github.com/spf13/viper) from 1.17.0 to 1.18.1.
- [Release notes](https://github.com/spf13/viper/releases)
- [Commits](https://github.com/spf13/viper/compare/v1.17.0...v1.18.1)

---
updated-dependencies:
- dependency-name: github.com/spf13/viper
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-11 06:43:00 +01:00
dependabot[bot]
34dac9e091 Bump actions/setup-go from 4 to 5 (#246)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 4 to 5.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-11 06:42:51 +01:00
dependabot[bot]
c1c81bb8f6 Bump github.com/onsi/ginkgo/v2 from 2.13.1 to 2.13.2 (#243)
Bumps [github.com/onsi/ginkgo/v2](https://github.com/onsi/ginkgo) from 2.13.1 to 2.13.2.
- [Release notes](https://github.com/onsi/ginkgo/releases)
- [Changelog](https://github.com/onsi/ginkgo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/onsi/ginkgo/compare/v2.13.1...v2.13.2)

---
updated-dependencies:
- dependency-name: github.com/onsi/ginkgo/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-04 08:02:14 +01:00
61 changed files with 4759 additions and 1410 deletions

View File

@@ -1,28 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. What version of AdGuardHome sync used?
2. What version of AdGuardHome us used?
3. How does the configuration look?
4. What is the error message?
**Expected behavior**
A clear and concise description of what you expected to happen.
**Log Files**
If applicable, add log files or json responses from AdGuardHome to help explain your problem.
Please set the environment variable `LOG_LEVEL=debug` to generate debug logs.
**Additional context**
Add any other context about the problem here.

67
.github/ISSUE_TEMPLATE/bug_report.yaml vendored Normal file
View File

@@ -0,0 +1,67 @@
name: Bug report
description: Create a report to help us improve
labels: ['bug']
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this issue report! If you have usage questions, please try the [FAQ](https://github.com/bakito/adguardhome-sync/wiki/FAQ) first.
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Also tell us, what did you expect to happen?
validations:
required: true
- type: textarea
id: adguardhome-sync-version
attributes:
label: AdguardHome-Sync Version
description: What version of adguardhome-sync was running when you discovered this issue?
validations:
required: true
- type: textarea
id: adguardhome-version
attributes:
label: AdguardHome Version
description: What version of adguardhome was running when you discovered this issue?
validations:
required: true
- type: textarea
id: config
attributes:
label: Configuration
description: |
- How did you configure adguardhome-sync?
- Please provide your configuration
render: shell
validations:
required: true
- type: textarea
id: applied-config
attributes:
label: Current Applied Configuration
description: |
- Run adguardhome-sync with environment variable `PRINT_CONFIG_ONLY=true` and provide the output here
render: shell
- type: textarea
id: logs
attributes:
label: Relevant 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`.
Please also check adguardhome logs and paste any relevant logs/errors to this issue.
render: shell
validations:
required: true
- type: textarea
attributes:
label: Anything else?
description: |
Links? References? Anything that will provide more context about the issue you are encountering!
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: false

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1 @@
blank_issues_enabled: false

View File

@@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -0,0 +1,36 @@
name: Feature request
description: Suggest an idea for this project
labels: ['enhancement']
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this issue report!
- type: textarea
id: relation
attributes:
label: Is your feature request related to a problem? Please describe.
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
validations:
required: true
- type: textarea
id: solution
attributes:
label: Describe the solution you'd like*
description: |
A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Describe alternatives you've considered
description: |
A clear and concise description of any alternative solutions or features you've considered.
- type: textarea
attributes:
label: Anything else?
description: |
Add any other context or screenshots about the feature request here.
validations:
required: false

View File

@@ -0,0 +1,60 @@
name: General Issue
description: Report an issue with your setup
labels: []
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this issue report! If you have usage questions, please try the [FAQ](https://github.com/bakito/adguardhome-sync/wiki/FAQ) first.
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Also tell us, what did you expect to happen?
validations:
required: true
- type: textarea
id: adguardhome-sync-version
attributes:
label: AdguardHome-Sync Version
description: What version of adguardhome-sync was running when you discovered this issue?
validations:
required: true
- type: textarea
id: adguardhome-version
attributes:
label: AdguardHome Version
description: What version of adguardhome was running when you discovered this issue?
validations:
required: true
- type: textarea
id: config
attributes:
label: Configuration
description: |
- How did you configure adguardhome-sync?
- Please provide your configuration
render: shell
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant 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`.
Please also check adguardhome logs and paste any relevant logs/errors to this issue.
render: shell
validations:
required: true
- type: textarea
attributes:
label: Anything else?
description: |
Links? References? Anything that will provide more context about the issue you are encountering!
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: false

View File

@@ -42,7 +42,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v2 uses: github/codeql-action/init@v3
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@@ -53,7 +53,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v2 uses: github/codeql-action/autobuild@v3
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
@@ -67,4 +67,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2 uses: github/codeql-action/analyze@v3

View File

@@ -11,6 +11,11 @@ on:
jobs: jobs:
e2e: e2e:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
build:
- mode: env
- mode: file
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -22,7 +27,7 @@ jobs:
run: ./testdata/e2e/bin/build-image.sh run: ./testdata/e2e/bin/build-image.sh
- name: Install Helm Chart - name: Install Helm Chart
run: ./testdata/e2e/bin/install-chart.sh run: ./testdata/e2e/bin/install-chart.sh ${{ matrix.build.mode }}
- name: Wait for sync to finish - name: Wait for sync to finish
run: ./testdata/e2e/bin/wait-for-sync.sh run: ./testdata/e2e/bin/wait-for-sync.sh
- name: Show origin Logs - name: Show origin Logs

View File

@@ -14,7 +14,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v4 uses: actions/setup-go@v5
with: with:
go-version-file: "go.mod" go-version-file: "go.mod"
@@ -32,7 +32,7 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v4 uses: actions/setup-go@v5
with: with:
go-version-file: "go.mod" go-version-file: "go.mod"
@@ -53,7 +53,7 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v4 uses: actions/setup-go@v5
with: with:
go-version-file: "go.mod" go-version-file: "go.mod"

View File

@@ -1,9 +1,9 @@
name: docker-image name: docker-image
on: on:
push: workflow_dispatch: # allows manual triggering
branches: schedule:
- main - cron: '0 0 * * *'
release: release:
types: types:
- published - published
@@ -39,6 +39,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Modify Dockerfile - name: Modify Dockerfile
run: | run: |
sed -i -e "s|FROM scratch|FROM ${{ matrix.build.fromImage }}|g" Dockerfile sed -i -e "s|FROM scratch|FROM ${{ matrix.build.fromImage }}|g" Dockerfile
@@ -58,10 +59,14 @@ jobs:
VERSION=${{ github.event.release.tag_name }} VERSION=${{ github.event.release.tag_name }}
BUILD=${{ env.curr_date }} BUILD=${{ env.curr_date }}
- name: Check for commits in the last 24 hours
run: echo "NEW_COMMIT_COUNT=$(git log --oneline --since '24 hours ago' | wc -l)" >> $GITHUB_ENV
if: ${{ github.event.release.tag_name == '' }}
- name: Build and push main - name: Build and push main
id: docker_build_main id: docker_build_main
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
if: ${{ github.event.release.tag_name == '' }} if: ${{ github.event.release.tag_name == '' && env.NEW_COMMIT_COUNT > 0 }}
with: with:
context: . context: .
pull: true pull: true

3
.gitignore vendored
View File

@@ -8,5 +8,6 @@ main
.adguardhome-sync.yaml .adguardhome-sync.yaml
tmp tmp
bin bin
config.yaml config*.yaml
*.log *.log
wiki

View File

@@ -3,7 +3,7 @@ FROM golang:1.21-bullseye as builder
WORKDIR /go/src/app WORKDIR /go/src/app
RUN apt-get update && \ RUN apt-get update && \
apt-get install -y upx && \ apt-get install -y upx ca-certificates tzdata && \
apt-get upgrade -y # upgrade to get latest ca-certs apt-get upgrade -y # upgrade to get latest ca-certs
ARG VERSION=main ARG VERSION=main
@@ -26,6 +26,7 @@ LABEL maintainer="bakito <github@bakito.ch>"
EXPOSE 8080 EXPOSE 8080
ENTRYPOINT ["/opt/go/adguardhome-sync"] ENTRYPOINT ["/opt/go/adguardhome-sync"]
CMD ["run", "--config", "/config/adguardhome-sync.yaml"] CMD ["run", "--config", "/config/adguardhome-sync.yaml"]
COPY --from=builder /go/src/app/adguardhome-sync /opt/go/adguardhome-sync
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=builder /usr/share/zoneinfo/ /usr/share/zoneinfo/
COPY --from=builder /go/src/app/adguardhome-sync /opt/go/adguardhome-sync
USER 1001 USER 1001

View File

@@ -15,13 +15,14 @@ generate: deepcopy-gen
test: generate lint test-ci test: generate lint test-ci
# Run ci tests # Run ci tests
test-ci: mocks tidy test-ci: mocks tidy ginkgo
go test ./... -coverprofile=coverage.out.tmp $(GINKGO) --cover --coverprofile coverage.out.tmp ./...
cat coverage.out.tmp | grep -v "_generated.go" > coverage.out cat coverage.out.tmp | grep -v "_generated.go" > coverage.out
go tool cover -func=coverage.out go tool cover -func=coverage.out
mocks: mockgen mocks: mockgen
$(MOCKGEN) -package client -destination pkg/mocks/client/mock.go github.com/bakito/adguardhome-sync/pkg/client Client $(MOCKGEN) -package client -destination pkg/mocks/client/mock.go github.com/bakito/adguardhome-sync/pkg/client Client
$(MOCKGEN) -package client -destination pkg/mocks/flags/mock.go github.com/bakito/adguardhome-sync/pkg/config Flags
release: semver goreleaser release: semver goreleaser
@version=$$($(LOCALBIN)/semver); \ @version=$$($(LOCALBIN)/semver); \
@@ -40,70 +41,65 @@ $(LOCALBIN):
mkdir -p $(LOCALBIN) mkdir -p $(LOCALBIN)
## Tool Binaries ## Tool Binaries
SEMVER ?= $(LOCALBIN)/semver DEEPCOPY_GEN ?= $(LOCALBIN)/deepcopy-gen
OAPI_CODEGEN ?= $(LOCALBIN)/oapi-codegen GINKGO ?= $(LOCALBIN)/ginkgo
MOCKGEN ?= $(LOCALBIN)/mockgen
GOLANGCI_LINT ?= $(LOCALBIN)/golangci-lint GOLANGCI_LINT ?= $(LOCALBIN)/golangci-lint
GORELEASER ?= $(LOCALBIN)/goreleaser GORELEASER ?= $(LOCALBIN)/goreleaser
DEEPCOPY_GEN ?= $(LOCALBIN)/deepcopy-gen MOCKGEN ?= $(LOCALBIN)/mockgen
OAPI_CODEGEN ?= $(LOCALBIN)/oapi-codegen
## Tool Versions SEMVER ?= $(LOCALBIN)/semver
SEMVER_VERSION ?= v1.1.3
OAPI_CODEGEN_VERSION ?= v2.0.0
MOCKGEN_VERSION ?= v1.6.0
GOLANGCI_LINT_VERSION ?= v1.55.2
GORELEASER_VERSION ?= v1.22.1
DEEPCOPY_GEN_VERSION ?= v0.28.4
## Tool Installer ## Tool Installer
.PHONY: semver
semver: $(SEMVER) ## Download semver locally if necessary.
$(SEMVER): $(LOCALBIN)
test -s $(LOCALBIN)/semver || GOBIN=$(LOCALBIN) go install github.com/bakito/semver@$(SEMVER_VERSION)
.PHONY: oapi-codegen
oapi-codegen: $(OAPI_CODEGEN) ## Download oapi-codegen locally if necessary.
$(OAPI_CODEGEN): $(LOCALBIN)
test -s $(LOCALBIN)/oapi-codegen || GOBIN=$(LOCALBIN) go install github.com/deepmap/oapi-codegen/v2/cmd/oapi-codegen@$(OAPI_CODEGEN_VERSION)
.PHONY: mockgen
mockgen: $(MOCKGEN) ## Download mockgen locally if necessary.
$(MOCKGEN): $(LOCALBIN)
test -s $(LOCALBIN)/mockgen || GOBIN=$(LOCALBIN) go install github.com/golang/mock/mockgen@$(MOCKGEN_VERSION)
.PHONY: golangci-lint
golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary.
$(GOLANGCI_LINT): $(LOCALBIN)
test -s $(LOCALBIN)/golangci-lint || GOBIN=$(LOCALBIN) go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION)
.PHONY: goreleaser
goreleaser: $(GORELEASER) ## Download goreleaser locally if necessary.
$(GORELEASER): $(LOCALBIN)
test -s $(LOCALBIN)/goreleaser || GOBIN=$(LOCALBIN) go install github.com/goreleaser/goreleaser@$(GORELEASER_VERSION)
.PHONY: deepcopy-gen .PHONY: deepcopy-gen
deepcopy-gen: $(DEEPCOPY_GEN) ## Download deepcopy-gen locally if necessary. deepcopy-gen: $(DEEPCOPY_GEN) ## Download deepcopy-gen locally if necessary.
$(DEEPCOPY_GEN): $(LOCALBIN) $(DEEPCOPY_GEN): $(LOCALBIN)
test -s $(LOCALBIN)/deepcopy-gen || GOBIN=$(LOCALBIN) go install k8s.io/code-generator/cmd/deepcopy-gen@$(DEEPCOPY_GEN_VERSION) test -s $(LOCALBIN)/deepcopy-gen || GOBIN=$(LOCALBIN) go install k8s.io/code-generator/cmd/deepcopy-gen
.PHONY: ginkgo
ginkgo: $(GINKGO) ## Download ginkgo locally if necessary.
$(GINKGO): $(LOCALBIN)
test -s $(LOCALBIN)/ginkgo || GOBIN=$(LOCALBIN) go install github.com/onsi/ginkgo/v2/ginkgo
.PHONY: golangci-lint
golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary.
$(GOLANGCI_LINT): $(LOCALBIN)
test -s $(LOCALBIN)/golangci-lint || GOBIN=$(LOCALBIN) go install github.com/golangci/golangci-lint/cmd/golangci-lint
.PHONY: goreleaser
goreleaser: $(GORELEASER) ## Download goreleaser locally if necessary.
$(GORELEASER): $(LOCALBIN)
test -s $(LOCALBIN)/goreleaser || GOBIN=$(LOCALBIN) go install github.com/goreleaser/goreleaser
.PHONY: mockgen
mockgen: $(MOCKGEN) ## Download mockgen locally if necessary.
$(MOCKGEN): $(LOCALBIN)
test -s $(LOCALBIN)/mockgen || GOBIN=$(LOCALBIN) go install go.uber.org/mock/mockgen
.PHONY: oapi-codegen
oapi-codegen: $(OAPI_CODEGEN) ## Download oapi-codegen locally if necessary.
$(OAPI_CODEGEN): $(LOCALBIN)
test -s $(LOCALBIN)/oapi-codegen || GOBIN=$(LOCALBIN) go install github.com/deepmap/oapi-codegen/v2/cmd/oapi-codegen
.PHONY: semver
semver: $(SEMVER) ## Download semver locally if necessary.
$(SEMVER): $(LOCALBIN)
test -s $(LOCALBIN)/semver || GOBIN=$(LOCALBIN) go install github.com/bakito/semver
## Update Tools ## Update Tools
.PHONY: update-toolbox-tools .PHONY: update-toolbox-tools
update-toolbox-tools: update-toolbox-tools:
@rm -f \ @rm -f \
$(LOCALBIN)/semver \ $(LOCALBIN)/deepcopy-gen \
$(LOCALBIN)/oapi-codegen \ $(LOCALBIN)/ginkgo \
$(LOCALBIN)/mockgen \
$(LOCALBIN)/golangci-lint \ $(LOCALBIN)/golangci-lint \
$(LOCALBIN)/goreleaser \ $(LOCALBIN)/goreleaser \
$(LOCALBIN)/deepcopy-gen $(LOCALBIN)/mockgen \
toolbox makefile -f $(LOCALDIR)/Makefile \ $(LOCALBIN)/oapi-codegen \
github.com/bakito/semver \ $(LOCALBIN)/semver
github.com/deepmap/oapi-codegen/v2/cmd/oapi-codegen \ toolbox makefile -f $(LOCALDIR)/Makefile
github.com/golang/mock/mockgen \
github.com/golangci/golangci-lint/cmd/golangci-lint \
github.com/goreleaser/goreleaser \
k8s.io/code-generator/cmd/deepcopy-gen@github.com/kubernetes/code-generator
## toolbox - end ## toolbox - end
start-replica: start-replica:
docker run --pull always --name adguardhome-replica -p 9091:3000 --rm adguard/adguardhome:latest 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 # docker run --pull always --name adguardhome-replica -p 9090:80 -p 9091:3000 --rm adguard/adguardhome:v0.107.13
copy-replica-config:
docker cp adguardhome-replica:/opt/adguardhome/conf/AdGuardHome.yaml tmp/AdGuardHome.yaml
start-replica2: start-replica2:
docker run --pull always --name adguardhome-replica2 -p 9093:3000 --rm adguard/adguardhome:latest 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 # docker run --pull always --name adguardhome-replica -p 9090:80 -p 9091:3000 --rm adguard/adguardhome:v0.107.13
@@ -128,10 +124,10 @@ kind-test:
model: oapi-codegen model: oapi-codegen
@mkdir -p tmp @mkdir -p tmp
go run openapi/main.go v0.107.40 go run openapi/main.go v0.107.43
$(OAPI_CODEGEN) -package model -generate types,client -config .oapi-codegen.yaml tmp/schema.yaml > pkg/client/model/model_generated.go $(OAPI_CODEGEN) -package model -generate types,client -config .oapi-codegen.yaml tmp/schema.yaml > pkg/client/model/model_generated.go
model-diff: model-diff:
go run openapi/main.go v0.107.40 go run openapi/main.go v0.107.43
go run openapi/main.go go run openapi/main.go
diff tmp/schema.yaml tmp/schema-master.yaml diff tmp/schema.yaml tmp/schema-master.yaml

View File

@@ -1,4 +1,5 @@
[![Go](https://github.com/bakito/adguardhome-sync/actions/workflows/go.yml/badge.svg)](https://github.com/bakito/adguardhome-sync/actions/workflows/go.yml) [![Go](https://github.com/bakito/adguardhome-sync/actions/workflows/go.yml/badge.svg)](https://github.com/bakito/adguardhome-sync/actions/workflows/go.yml)
[![e2e tests](https://github.com/bakito/adguardhome-sync/actions/workflows/e2e.yaml/badge.svg)](https://github.com/bakito/adguardhome-sync/actions/workflows/e2e.yaml)
[![Go Report Card](https://goreportcard.com/badge/github.com/bakito/adguardhome-sync)](https://goreportcard.com/report/github.com/bakito/adguardhome-sync) [![Go Report Card](https://goreportcard.com/badge/github.com/bakito/adguardhome-sync)](https://goreportcard.com/report/github.com/bakito/adguardhome-sync)
[![Coverage Status](https://coveralls.io/repos/github/bakito/adguardhome-sync/badge.svg?branch=main&service=github)](https://coveralls.io/github/bakito/adguardhome-sync?branch=main) [![Coverage Status](https://coveralls.io/repos/github/bakito/adguardhome-sync/badge.svg?branch=main&service=github)](https://coveralls.io/github/bakito/adguardhome-sync?branch=main)
@@ -6,6 +7,11 @@
Synchronize [AdGuardHome](https://github.com/AdguardTeam/AdGuardHome) config to replica instances. Synchronize [AdGuardHome](https://github.com/AdguardTeam/AdGuardHome) config to replica instances.
## FAQ & Deprecations
Please check the wiki
for [FAQ](https://github.com/bakito/adguardhome-sync/wiki/FAQ) and [Deprecations](https://github.com/bakito/adguardhome-sync/wiki/Deprecations).
## Current sync features ## Current sync features
- General Settings - General Settings
@@ -39,7 +45,9 @@ Both the origin instance must be initially setup via the AdguardHome installatio
## Username / Password vs. Cookie ## Username / Password vs. Cookie
Some instances of AdGuard Home do not support basic authentication. For instance, many routers with built-in Adguard Home support do not. If this is the case, a valid cookie may be provided instead. If the router protects the AdGuard instance behind its own authentication, the cookie from an authenticated request may allow the sync to succeed. Some instances of AdGuard Home do not support basic authentication. For instance, many routers with built-in Adguard
Home support do not. If this is the case, a valid cookie may be provided instead. If the router protects the AdGuard
instance behind its own authentication, the cookie from an authenticated request may allow the sync to succeed.
- This has been tested successfully against GL.Inet routers with AdGuard Home. - This has been tested successfully against GL.Inet routers with AdGuard Home.
- Note: due to the short validity of cookies, this approach is likely only suitable for one-time syncs - Note: due to the short validity of cookies, this approach is likely only suitable for one-time syncs
@@ -140,6 +148,8 @@ services:
environment: environment:
LOG_LEVEL: "info" LOG_LEVEL: "info"
ORIGIN_URL: "https://192.168.1.2:3000" ORIGIN_URL: "https://192.168.1.2:3000"
# ORIGIN_WEB_URL: "https://some-other.url" # used in the web interface (default: <origin-url>
ORIGIN_USERNAME: "username" ORIGIN_USERNAME: "username"
ORIGIN_PASSWORD: "password" ORIGIN_PASSWORD: "password"
REPLICA_URL: "http://192.168.1.3" REPLICA_URL: "http://192.168.1.3"
@@ -148,27 +158,29 @@ services:
REPLICA1_URL: "http://192.168.1.4" REPLICA1_URL: "http://192.168.1.4"
REPLICA1_USERNAME: "username" REPLICA1_USERNAME: "username"
REPLICA1_PASSWORD: "password" REPLICA1_PASSWORD: "password"
REPLICA1_APIPATH: "/some/path/control" REPLICA1_API_PATH: "/some/path/control"
# REPLICA1_AUTOSETUP: true # if true, AdGuardHome is automatically initialized. # REPLICA1_WEB_URL: "https://some-other.url" # used in the web interface (default: <replica-url>
# REPLICA1_INTERFACENAME: 'ens18' # use custom dhcp interface name # REPLICA1_AUTO_SETUP: true # if true, AdGuardHome is automatically initialized.
# REPLICA1_DHCPSERVERENABLED: true/false (optional) enables/disables the dhcp server on the replica # REPLICA1_INTERFACE_NAME: 'ens18' # use custom dhcp interface name
# REPLICA1_DHCP_SERVER_ENABLED: true/false (optional) enables/disables the dhcp server on the replica
CRON: "*/10 * * * *" # run every 10 minutes CRON: "*/10 * * * *" # run every 10 minutes
RUNONSTART: true RUNONSTART: true
# CONTINUE_ON_ERROR: false # If enabled, the synchronisation task will not fail on single errors, but will log the errors and continue
# Configure the sync API server, disabled if api port is 0 # Configure the sync API server, disabled if api port is 0
API_PORT: 8080 API_PORT: 8080
# Configure sync features; by default all features are enabled. # Configure sync features; by default all features are enabled.
# FEATURES_GENERALSETTINGS: true # FEATURES_GENERAL_SETTINGS: true
# FEATURES_QUERYLOGCONFIG: true # FEATURES_QUERY_LOG_CONFIG: true
# FEATURES_STATSCONFIG: true # FEATURES_STATS_CONFIG: true
# FEATURES_CLIENTSETTINGS: true # FEATURES_CLIENT_SETTINGS: true
# FEATURES_SERVICES: true # FEATURES_SERVICES: true
# FEATURES_FILTERS: true # FEATURES_FILTERS: true
# FEATURES_DHCP_SERVERCONFIG: true # FEATURES_DHCP_SERVER_CONFIG: true
# FEATURES_DHCP_STATICLEASES: true # FEATURES_DHCP_STATIC_LEASES: true
# FEATURES_DNS_SERVERCONFIG: true # FEATURES_DNS_SERVER_CONFIG: true
# FEATURES_DNS_ACCESSLISTS: true # FEATURES_DNS_ACCESS_LISTS: true
# FEATURES_DNS_REWRITES: true # FEATURES_DNS_REWRITES: true
ports: ports:
- 8080:8080 - 8080:8080
@@ -186,6 +198,9 @@ cron: "*/10 * * * *"
# runs the synchronisation on startup # runs the synchronisation on startup
runOnStart: true runOnStart: true
# If enabled, the synchronisation task will not fail on single errors, but will log the errors and continue
continueOnError: false
origin: origin:
# url of the origin instance # url of the origin instance
url: https://192.168.1.2:3000 url: https://192.168.1.2:3000
@@ -195,15 +210,7 @@ origin:
password: password password: password
# cookie: Origin-Cookie-Name=CCCOOOKKKIIIEEE # cookie: Origin-Cookie-Name=CCCOOOKKKIIIEEE
# replica instance (optional, if only one) # replicas instances
replica:
# url of the replica instance
url: http://192.168.1.3
username: username
password: password
# cookie: Replica-Cookie-Name=CCCOOOKKKIIIEEE
# replicas instances (optional, if more than one)
replicas: replicas:
# url of the replica instance # url of the replica instance
- url: http://192.168.1.3 - url: http://192.168.1.3
@@ -215,6 +222,7 @@ replicas:
password: password password: password
# cookie: Replica2-Cookie-Name=CCCOOOKKKIIIEEE # cookie: Replica2-Cookie-Name=CCCOOOKKKIIIEEE
# autoSetup: true # if true, AdGuardHome is automatically initialized. # autoSetup: true # if true, AdGuardHome is automatically initialized.
# webURL: "https://some-other.url" # used in the web interface (default: <replica-url>
# Configure the sync API server, disabled if api port is 0 # Configure the sync API server, disabled if api port is 0
api: api:
@@ -223,6 +231,8 @@ api:
# if username and password are defined, basic auth is applied to the sync API # if username and password are defined, basic auth is applied to the sync API
username: username username: username
password: password password: password
# enable api dark mode
darkMode: true
# Configure sync features; by default all features are enabled. # Configure sync features; by default all features are enabled.
features: features:

View File

@@ -3,71 +3,15 @@ package cmd
import ( import (
"fmt" "fmt"
"os" "os"
"regexp"
"strings"
"github.com/bakito/adguardhome-sync/pkg/log" "github.com/bakito/adguardhome-sync/pkg/log"
"github.com/bakito/adguardhome-sync/pkg/types"
"github.com/bakito/adguardhome-sync/version" "github.com/bakito/adguardhome-sync/version"
"github.com/mitchellh/go-homedir"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper"
"go.uber.org/zap"
)
const (
configCron = "cron"
configRunOnStart = "runOnStart"
configAPIPort = "api.port"
configAPIUsername = "api.username"
configAPIPassword = "api.password"
configAPIDarkMode = "api.darkMode"
configFeatureDHCPServerConfig = "features.dhcp.serverConfig"
configFeatureDHCPStaticLeases = "features.dhcp.staticLeases"
configFeatureDNServerConfig = "features.dns.serverConfig"
configFeatureDNSPAccessLists = "features.dns.accessLists"
configFeatureDNSRewrites = "features.dns.rewrites"
configFeatureGeneralSettings = "features.generalSettings"
configFeatureQueryLogConfig = "features.queryLogConfig"
configFeatureStatsConfig = "features.statsConfig"
configFeatureClientSettings = "features.clientSettings"
configFeatureServices = "features.services"
configFeatureFilters = "features.filters"
configOriginURL = "origin.url"
configOriginAPIPath = "origin.apiPath"
configOriginUsername = "origin.username"
configOriginPassword = "origin.password"
configOriginCookie = "origin.cookie"
configOriginInsecureSkipVerify = "origin.insecureSkipVerify"
configReplicaURL = "replica.url"
configReplicaAPIPath = "replica.apiPath"
configReplicaUsername = "replica.username"
configReplicaPassword = "replica.password"
configReplicaCookie = "replica.cookie"
configReplicaInsecureSkipVerify = "replica.insecureSkipVerify"
configReplicaAutoSetup = "replica.autoSetup"
configReplicaInterfaceName = "replica.interfaceName"
envReplicasUsernameFormat = "REPLICA%s_USERNAME" // #nosec G101
envReplicasPasswordFormat = "REPLICA%s_PASSWORD" // #nosec G101
envReplicasCookieFormat = "REPLICA%s_COOKIE" // #nosec G101
envReplicasAPIPathFormat = "REPLICA%s_APIPATH"
envReplicasInsecureSkipVerifyFormat = "REPLICA%s_INSECURESKIPVERIFY"
envReplicasAutoSetup = "REPLICA%s_AUTOSETUP"
envReplicasInterfaceName = "REPLICA%s_INTERFACENAME"
// Deprecated: use envReplicasInterfaceName instead
envReplicasInterfaceNameDeprecated = "REPLICA%s_INTERFACWENAME"
envDHCPServerEnabled = "REPLICA%s_DHCPSERVERENABLED"
) )
var ( var (
cfgFile string cfgFile string
logger = log.GetLogger("root") logger = log.GetLogger("root")
envReplicasURLPattern = regexp.MustCompile(`^REPLICA(\d+)_URL=(.*)`)
) )
// rootCmd represents the base command when called without any subcommands // rootCmd represents the base command when called without any subcommands
@@ -87,8 +31,6 @@ func Execute() {
} }
func init() { func init() {
cobra.OnInitialize(initConfig)
// Here you will define your flags and configuration settings. // Here you will define your flags and configuration settings.
// Cobra supports persistent flags, which, if defined here, // Cobra supports persistent flags, which, if defined here,
// will be global for your application. // will be global for your application.
@@ -99,87 +41,3 @@ func init() {
// when this action is called directly. // when this action is called directly.
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
} }
// initConfig reads in config file and ENV variables if set.
func initConfig() {
if cfgFile != "" {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
} else {
// Find home directory.
home, err := homedir.Dir()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// Search config in home directory with name ".adguardhome-sync" (without extension).
viper.AddConfigPath(home)
viper.SetConfigName(".adguardhome-sync")
}
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_"))
viper.AutomaticEnv() // read in environment variables that match
// If a config file is found, read it in.
if err := viper.ReadInConfig(); err == nil {
logger.Info("Using config file:", viper.ConfigFileUsed())
} else if cfgFile != "" {
fmt.Println(err)
os.Exit(1)
}
}
func getConfig(logger *zap.SugaredLogger) (*types.Config, error) {
cfg := &types.Config{}
if err := viper.Unmarshal(cfg); err != nil {
return nil, err
}
if len(cfg.Replicas) == 0 {
cfg.Replicas = append(cfg.Replicas, collectEnvReplicas(logger)...)
}
return cfg, nil
}
// Manually collect replicas from env.
func collectEnvReplicas(logger *zap.SugaredLogger) []types.AdGuardInstance {
var replicas []types.AdGuardInstance
for _, v := range os.Environ() {
if envReplicasURLPattern.MatchString(v) {
sm := envReplicasURLPattern.FindStringSubmatch(v)
re := types.AdGuardInstance{
URL: sm[2],
Username: os.Getenv(fmt.Sprintf(envReplicasUsernameFormat, sm[1])),
Password: os.Getenv(fmt.Sprintf(envReplicasPasswordFormat, sm[1])),
Cookie: os.Getenv(fmt.Sprintf(envReplicasCookieFormat, sm[1])),
APIPath: os.Getenv(fmt.Sprintf(envReplicasAPIPathFormat, sm[1])),
InsecureSkipVerify: strings.EqualFold(os.Getenv(fmt.Sprintf(envReplicasInsecureSkipVerifyFormat, sm[1])), "true"),
AutoSetup: strings.EqualFold(os.Getenv(fmt.Sprintf(envReplicasAutoSetup, sm[1])), "true"),
InterfaceName: os.Getenv(fmt.Sprintf(envReplicasInterfaceName, sm[1])),
}
if re.InterfaceName == "" {
if in, ok := os.LookupEnv(fmt.Sprintf(envReplicasInterfaceNameDeprecated, sm[1])); ok {
logger.
With("correct", envReplicasInterfaceName, "deprecated", envReplicasInterfaceNameDeprecated).
Warn("Deprecated env variable is used, please use the correct one")
re.InterfaceName = in
}
}
if dhcpEnabled, ok := os.LookupEnv(fmt.Sprintf(envDHCPServerEnabled, sm[1])); ok {
if strings.EqualFold(dhcpEnabled, "true") {
re.DHCPServerEnabled = boolPtr(true)
} else if strings.EqualFold(dhcpEnabled, "false") {
re.DHCPServerEnabled = boolPtr(false)
}
}
replicas = append(replicas, re)
}
}
return replicas
}
func boolPtr(b bool) *bool {
return &b
}

View File

@@ -1,116 +0,0 @@
package cmd
import (
"fmt"
"os"
"github.com/bakito/adguardhome-sync/pkg/log"
"github.com/bakito/adguardhome-sync/pkg/types"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"go.uber.org/zap"
)
var envVars = []string{
"FEATURES_GENERALSETTINGS",
"FEATURES_QUERYLOGCONFIG",
"FEATURES_STATSCONFIG",
"FEATURES_CLIENTSETTINGS",
"FEATURES_SERVICES",
"FEATURES_FILTERS",
"FEATURES_DHCP_SERVERCONFIG",
"FEATURES_DHCP_STATICLEASES",
"FEATURES_DNS_SERVERCONFIG",
"FEATURES_DNS_ACCESSLISTS",
"FEATURES_DNS_REWRITES",
"REPLICA1_INTERFACENAME",
"REPLICA1_INTERFACWENAME",
"REPLICA1_DHCPSERVERENABLED",
}
var _ = Describe("Run", func() {
var logger *zap.SugaredLogger
BeforeEach(func() {
logger = log.GetLogger("root")
for _, envVar := range envVars {
Ω(os.Unsetenv(envVar)).ShouldNot(HaveOccurred())
}
initConfig()
})
AfterEach(func() {
for _, envVar := range envVars {
Ω(os.Unsetenv(envVar)).ShouldNot(HaveOccurred())
}
})
Context("getConfig", func() {
It("features should be true by default", func() {
cfg, err := getConfig(logger)
Ω(err).ShouldNot(HaveOccurred())
verifyFeatures(cfg, true)
})
It("features should be false", func() {
for _, envVar := range envVars {
Ω(os.Setenv(envVar, "false")).ShouldNot(HaveOccurred())
}
cfg, err := getConfig(logger)
Ω(err).ShouldNot(HaveOccurred())
verifyFeatures(cfg, false)
})
Context("interface name", func() {
It("should set interface name of replica 1", func() {
Ω(os.Setenv("REPLICA1_URL", "https://foo.bar")).ShouldNot(HaveOccurred())
Ω(os.Setenv(fmt.Sprintf(envReplicasInterfaceName, "1"), "eth0")).ShouldNot(HaveOccurred())
cfg, err := getConfig(logger)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Replicas[0].InterfaceName).Should(Equal("eth0"))
})
It("should set interface name of replica 1 from deprecated env", func() {
Ω(os.Setenv("REPLICA1_URL", "https://foo.bar")).ShouldNot(HaveOccurred())
Ω(os.Setenv(fmt.Sprintf(envReplicasInterfaceNameDeprecated, "1"), "eth0")).ShouldNot(HaveOccurred())
cfg, err := getConfig(logger)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Replicas[0].InterfaceName).Should(Equal("eth0"))
})
It("deprecated should not overwrite the correct", func() {
Ω(os.Setenv("REPLICA1_URL", "https://foo.bar")).ShouldNot(HaveOccurred())
Ω(os.Setenv(fmt.Sprintf(envReplicasInterfaceNameDeprecated, "1"), "eth1")).ShouldNot(HaveOccurred())
Ω(os.Setenv(fmt.Sprintf(envReplicasInterfaceName, "1"), "eth0")).ShouldNot(HaveOccurred())
cfg, err := getConfig(logger)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.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(envDHCPServerEnabled, "1"), "true")).ShouldNot(HaveOccurred())
cfg, err := getConfig(logger)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Replicas[0].DHCPServerEnabled).ShouldNot(BeNil())
Ω(*cfg.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(envDHCPServerEnabled, "1"), "false")).ShouldNot(HaveOccurred())
cfg, err := getConfig(logger)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Replicas[0].DHCPServerEnabled).ShouldNot(BeNil())
Ω(*cfg.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))
}

View File

@@ -1,10 +1,11 @@
package cmd package cmd
import ( import (
"github.com/bakito/adguardhome-sync/pkg/config"
"github.com/bakito/adguardhome-sync/pkg/log" "github.com/bakito/adguardhome-sync/pkg/log"
"github.com/bakito/adguardhome-sync/pkg/sync" "github.com/bakito/adguardhome-sync/pkg/sync"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "gopkg.in/yaml.v3"
) )
// runCmd represents the run command // runCmd represents the run command
@@ -14,83 +15,75 @@ var doCmd = &cobra.Command{
Long: `Synchronizes the configuration form an origin instance to a replica`, Long: `Synchronizes the configuration form an origin instance to a replica`,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
logger = log.GetLogger("run") logger = log.GetLogger("run")
cfg, err := getConfig(logger) cfg, err := config.Get(cfgFile, cmd.Flags())
if err != nil { if err != nil {
logger.Error(err) logger.Error(err)
return err return err
} }
if err := cfg.Init(); err != nil {
logger.Error(err)
return err
}
if cfg.PrintConfigOnly {
config, err := yaml.Marshal(cfg)
if err != nil {
logger.Error(err)
return err
}
logger.Infof("Printing adguardhome-sync config (THE APPLICATION WILL NOT START IN THIS MODE): \n%s",
string(config))
return nil
}
return sync.Sync(cfg) return sync.Sync(cfg)
}, },
} }
func init() { func init() {
rootCmd.AddCommand(doCmd) rootCmd.AddCommand(doCmd)
doCmd.PersistentFlags().String("cron", "", "The cron expression to run in daemon mode") doCmd.PersistentFlags().String(config.FlagCron, "", "The cron expression to run in daemon mode")
_ = viper.BindPFlag(configCron, doCmd.PersistentFlags().Lookup("cron")) doCmd.PersistentFlags().Bool(config.FlagRunOnStart, true, "Run the sync job on start.")
doCmd.PersistentFlags().Bool("runOnStart", true, "Run the sync job on start.") doCmd.PersistentFlags().Bool(config.FlagPrintConfigOnly, false, "Prints the configuration only and exists. "+
_ = viper.BindPFlag(configRunOnStart, doCmd.PersistentFlags().Lookup("runOnStart")) "Can be used to debug the config E.g: when having authentication issues.")
doCmd.PersistentFlags().Int("api-port", 8080, "Sync API Port, the API endpoint will be started to enable remote triggering; if 0 port API is disabled.") doCmd.PersistentFlags().Bool(config.FlagContinueOnError, false, "If enabled, the synchronisation task "+
_ = viper.BindPFlag(configAPIPort, doCmd.PersistentFlags().Lookup("api-port")) "will not fail on single errors, but will log the errors and continue.")
doCmd.PersistentFlags().String("api-username", "", "Sync API username")
_ = viper.BindPFlag(configAPIUsername, doCmd.PersistentFlags().Lookup("api-username"))
doCmd.PersistentFlags().String("api-password", "", "Sync API password")
_ = viper.BindPFlag(configAPIPassword, doCmd.PersistentFlags().Lookup("api-password"))
doCmd.PersistentFlags().String("api-darkMode", "", "API UI in dark mode")
_ = viper.BindPFlag(configAPIDarkMode, doCmd.PersistentFlags().Lookup("api-darkMode"))
doCmd.PersistentFlags().Bool("feature-dhcp-server-config", true, "Enable DHCP server config feature") 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.")
_ = viper.BindPFlag(configFeatureDHCPServerConfig, doCmd.PersistentFlags().Lookup("feature-dhcp-server-config")) doCmd.PersistentFlags().String(config.FlagApiUsername, "", "Sync API username")
doCmd.PersistentFlags().Bool("feature-dhcp-static-leases", true, "Enable DHCP server static leases feature") doCmd.PersistentFlags().String(config.FlagApiPassword, "", "Sync API password")
_ = viper.BindPFlag(configFeatureDHCPStaticLeases, doCmd.PersistentFlags().Lookup("feature-dhcp-static-leases")) doCmd.PersistentFlags().String(config.FlagApiDarkMode, "", "API UI in dark mode")
doCmd.PersistentFlags().Bool("feature-dns-server-config", true, "Enable DNS server config feature") doCmd.PersistentFlags().Bool(config.FlagFeatureDhcpServerConfig, true, "Enable DHCP server config feature")
_ = viper.BindPFlag(configFeatureDNServerConfig, doCmd.PersistentFlags().Lookup("feature-dns-server-config")) doCmd.PersistentFlags().Bool(config.FlagFeatureDhcpStaticLeases, true, "Enable DHCP server static leases feature")
doCmd.PersistentFlags().Bool("feature-dns-access-lists", true, "Enable DNS server access lists feature")
_ = viper.BindPFlag(configFeatureDNSPAccessLists, doCmd.PersistentFlags().Lookup("feature-dns-access-lists"))
doCmd.PersistentFlags().Bool("feature-dns-rewrites", true, "Enable DNS rewrites feature")
_ = viper.BindPFlag(configFeatureDNSRewrites, doCmd.PersistentFlags().Lookup("feature-dns-rewrites"))
doCmd.PersistentFlags().Bool("feature-general-settings", true, "Enable general settings feature")
_ = viper.BindPFlag(configFeatureGeneralSettings, doCmd.PersistentFlags().Lookup("feature-general-settings"))
_ = viper.BindPFlag("features.generalSettings", doCmd.PersistentFlags().Lookup("feature-general-settings"))
doCmd.PersistentFlags().Bool("feature-query-log-config", true, "Enable query log config feature")
_ = viper.BindPFlag(configFeatureQueryLogConfig, doCmd.PersistentFlags().Lookup("feature-query-log-config"))
doCmd.PersistentFlags().Bool("feature-stats-config", true, "Enable stats config feature")
_ = viper.BindPFlag(configFeatureStatsConfig, doCmd.PersistentFlags().Lookup("feature-stats-config"))
doCmd.PersistentFlags().Bool("feature-client-settings", true, "Enable client settings feature")
_ = viper.BindPFlag(configFeatureClientSettings, doCmd.PersistentFlags().Lookup("feature-client-settings"))
doCmd.PersistentFlags().Bool("feature-services", true, "Enable services sync feature")
_ = viper.BindPFlag(configFeatureServices, doCmd.PersistentFlags().Lookup("feature-services"))
doCmd.PersistentFlags().Bool("feature-filters", true, "Enable filters sync feature")
_ = viper.BindPFlag(configFeatureFilters, doCmd.PersistentFlags().Lookup("feature-filters"))
doCmd.PersistentFlags().String("origin-url", "", "Origin instance url") doCmd.PersistentFlags().Bool(config.FlagFeatureDnsServerConfig, true, "Enable DNS server config feature")
_ = viper.BindPFlag(configOriginURL, doCmd.PersistentFlags().Lookup("origin-url")) doCmd.PersistentFlags().Bool(config.FlagFeatureDnsAccessLists, true, "Enable DNS server access lists feature")
doCmd.PersistentFlags().String("origin-api-path", "/control", "Origin instance API path") doCmd.PersistentFlags().Bool(config.FlagFeatureDnsRewrites, true, "Enable DNS rewrites feature")
_ = viper.BindPFlag(configOriginAPIPath, doCmd.PersistentFlags().Lookup("origin-api-path"))
doCmd.PersistentFlags().String("origin-username", "", "Origin instance username")
_ = viper.BindPFlag(configOriginUsername, doCmd.PersistentFlags().Lookup("origin-username"))
doCmd.PersistentFlags().String("origin-password", "", "Origin instance password")
_ = viper.BindPFlag(configOriginPassword, doCmd.PersistentFlags().Lookup("origin-password"))
doCmd.PersistentFlags().String("origin-cookie", "", "If Set, uses a cookie for authentication")
_ = viper.BindPFlag(configOriginCookie, doCmd.PersistentFlags().Lookup("origin-cookie"))
doCmd.PersistentFlags().Bool("origin-insecure-skip-verify", false, "Enable Origin instance InsecureSkipVerify")
_ = viper.BindPFlag(configOriginInsecureSkipVerify, doCmd.PersistentFlags().Lookup("origin-insecure-skip-verify"))
doCmd.PersistentFlags().String("replica-url", "", "Replica instance url") doCmd.PersistentFlags().Bool(config.FlagFeatureGeneral, true, "Enable general settings feature")
_ = viper.BindPFlag(configReplicaURL, doCmd.PersistentFlags().Lookup("replica-url")) doCmd.PersistentFlags().Bool(config.FlagFeatureQueryLog, true, "Enable query log config feature")
doCmd.PersistentFlags().String("replica-api-path", "/control", "Replica instance API path") doCmd.PersistentFlags().Bool(config.FlagFeatureStats, true, "Enable stats config feature")
_ = viper.BindPFlag(configReplicaAPIPath, doCmd.PersistentFlags().Lookup("replica-api-path")) doCmd.PersistentFlags().Bool(config.FlagFeatureClient, true, "Enable client settings feature")
doCmd.PersistentFlags().String("replica-username", "", "Replica instance username") doCmd.PersistentFlags().Bool(config.FlagFeatureServices, true, "Enable services sync feature")
_ = viper.BindPFlag(configReplicaUsername, doCmd.PersistentFlags().Lookup("replica-username")) doCmd.PersistentFlags().Bool(config.FlagFeatureFilters, true, "Enable filters sync feature")
doCmd.PersistentFlags().String("replica-password", "", "Replica instance password")
_ = viper.BindPFlag(configReplicaPassword, doCmd.PersistentFlags().Lookup("replica-password")) doCmd.PersistentFlags().String(config.FlagOriginURL, "", "Origin instance url")
doCmd.PersistentFlags().String("replica-cookie", "", "If Set, uses a cookie for authentication") doCmd.PersistentFlags().String(config.FlagOriginWebURL, "", "Origin instance web url used in the web interface (default: <origin-url>)")
_ = viper.BindPFlag(configReplicaCookie, doCmd.PersistentFlags().Lookup("replica-cookie")) doCmd.PersistentFlags().String(config.FlagOriginApiPath, "/control", "Origin instance API path")
doCmd.PersistentFlags().Bool("replica-insecure-skip-verify", false, "Enable Replica instance InsecureSkipVerify") doCmd.PersistentFlags().String(config.FlagOriginUsername, "", "Origin instance username")
_ = viper.BindPFlag(configReplicaInsecureSkipVerify, doCmd.PersistentFlags().Lookup("replica-insecure-skip-verify")) doCmd.PersistentFlags().String(config.FlagOriginPassword, "", "Origin instance password")
doCmd.PersistentFlags().Bool("replica-auto-setup", false, "Enable automatic setup of new AdguardHome instances. This replaces the setup wizard.") doCmd.PersistentFlags().String(config.FlagOriginCookie, "", "If Set, uses a cookie for authentication")
_ = viper.BindPFlag(configReplicaAutoSetup, doCmd.PersistentFlags().Lookup("replica-auto-setup")) doCmd.PersistentFlags().Bool(config.FlagOriginISV, false, "Enable Origin instance InsecureSkipVerify")
doCmd.PersistentFlags().String("replica-interface-name", "", "Optional change the interface name of the replica if it differs from the master")
_ = viper.BindPFlag(configReplicaInterfaceName, doCmd.PersistentFlags().Lookup("replica-interface-name")) 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.FlagReplicaUsername, "", "Replica instance username")
doCmd.PersistentFlags().String(config.FlagReplicaPassword, "", "Replica instance password")
doCmd.PersistentFlags().String(config.FlagReplicaCookie, "", "If Set, uses a cookie for authentication")
doCmd.PersistentFlags().Bool(config.FlagReplicaISV, false, "Enable Replica instance InsecureSkipVerify")
doCmd.PersistentFlags().Bool(config.FlagReplicaAutoSetup, false, "Enable automatic setup of new AdguardHome instances. This replaces the setup wizard.")
doCmd.PersistentFlags().String(config.FlagReplicaInterfaceName, "", "Optional change the interface name of the replica if it differs from the master")
} }

425
go.mod
View File

@@ -1,80 +1,441 @@
module github.com/bakito/adguardhome-sync module github.com/bakito/adguardhome-sync
go 1.20 go 1.21
require ( require (
github.com/bakito/semver v1.1.3
github.com/caarlos0/env/v10 v10.0.0
github.com/deepmap/oapi-codegen/v2 v2.0.0
github.com/gin-gonic/gin v1.9.1 github.com/gin-gonic/gin v1.9.1
github.com/go-resty/resty/v2 v2.10.0 github.com/go-resty/resty/v2 v2.9.1
github.com/golang/mock v1.6.0 github.com/golangci/golangci-lint v1.55.2
github.com/google/uuid v1.4.0 github.com/google/uuid v1.5.0
github.com/goreleaser/goreleaser v1.23.0
github.com/jinzhu/copier v0.4.0 github.com/jinzhu/copier v0.4.0
github.com/mitchellh/go-homedir v1.1.0 github.com/oapi-codegen/runtime v1.1.1
github.com/oapi-codegen/runtime v1.1.0 github.com/onsi/ginkgo/v2 v2.14.0
github.com/onsi/ginkgo/v2 v2.13.1
github.com/onsi/gomega v1.30.0 github.com/onsi/gomega v1.30.0
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/spf13/cobra v1.8.0 github.com/spf13/cobra v1.8.0
github.com/spf13/viper v1.17.0 go.uber.org/mock v0.4.0
go.uber.org/zap v1.26.0 go.uber.org/zap v1.26.0
golang.org/x/mod v0.14.0 golang.org/x/mod v0.14.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
k8s.io/apimachinery v0.28.4 k8s.io/apimachinery v0.29.0
k8s.io/code-generator v0.29.0
) )
require ( require (
4d63.com/gocheckcompilerdirectives v1.2.1 // indirect
4d63.com/gochecknoglobals v0.2.1 // indirect
cloud.google.com/go v0.110.10 // indirect
cloud.google.com/go/compute v1.23.3 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v1.1.5 // indirect
cloud.google.com/go/kms v1.15.5 // indirect
cloud.google.com/go/storage v1.35.1 // indirect
code.gitea.io/sdk/gitea v0.17.0 // indirect
dario.cat/mergo v1.0.0 // indirect
github.com/4meepo/tagalign v1.3.3 // indirect
github.com/Abirdcfly/dupword v0.0.13 // indirect
github.com/AlekSi/pointer v1.2.0 // indirect
github.com/Antonboom/errname v0.1.12 // indirect
github.com/Antonboom/nilnil v0.1.7 // indirect
github.com/Antonboom/testifylint v0.2.3 // indirect
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azkeys v0.10.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.0 // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest v0.11.29 // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.23 // indirect
github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 // indirect
github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
github.com/Azure/go-autorest/logger v0.2.1 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.0 // indirect
github.com/BurntSushi/toml v1.3.2 // indirect
github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect
github.com/GaijinEntertainment/go-exhaustruct/v3 v3.1.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Masterminds/semver/v3 v3.2.1 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/OpenPeeDeeP/depguard/v2 v2.1.0 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c // indirect
github.com/alecthomas/go-check-sumtype v0.1.3 // indirect
github.com/alessio/shellescape v1.4.1 // indirect
github.com/alexkohler/nakedret/v2 v2.0.2 // indirect
github.com/alexkohler/prealloc v1.0.0 // indirect
github.com/alingse/asasalint v0.0.11 // indirect
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/bytedance/sonic v1.10.0 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/ashanbrown/forbidigo v1.6.0 // indirect
github.com/ashanbrown/makezero v1.1.1 // indirect
github.com/atc0005/go-teams-notify/v2 v2.8.0 // indirect
github.com/aws/aws-sdk-go v1.48.3 // indirect
github.com/aws/aws-sdk-go-v2 v1.23.1 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.1 // indirect
github.com/aws/aws-sdk-go-v2/config v1.25.5 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.16.4 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.5 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.14.2 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.4 // indirect
github.com/aws/aws-sdk-go-v2/service/ecr v1.20.2 // indirect
github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.18.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.4 // indirect
github.com/aws/aws-sdk-go-v2/service/kms v1.26.3 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.44.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.17.3 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.20.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.25.4 // indirect
github.com/aws/smithy-go v1.17.0 // indirect
github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20231024185945-8841054dbdb8 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bkielbasa/cyclop v1.2.1 // indirect
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect
github.com/blizzy78/varnamelen v0.8.0 // indirect
github.com/bombsimon/wsl/v3 v3.4.0 // indirect
github.com/breml/bidichk v0.2.7 // indirect
github.com/breml/errchkjson v0.3.6 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/butuzov/ireturn v0.2.2 // indirect
github.com/butuzov/mirror v1.1.0 // indirect
github.com/bytedance/sonic v1.10.2 // indirect
github.com/caarlos0/ctrlc v1.2.0 // indirect
github.com/caarlos0/env/v9 v9.0.0 // indirect
github.com/caarlos0/go-reddit/v3 v3.0.1 // indirect
github.com/caarlos0/go-shellwords v1.0.12 // indirect
github.com/caarlos0/go-version v0.1.1 // indirect
github.com/caarlos0/log v0.4.4 // indirect
github.com/catenacyber/perfsprint v0.2.0 // indirect
github.com/cavaliergopher/cpio v1.0.1 // indirect
github.com/ccojocar/zxcvbn-go v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/charithe/durationcheck v0.0.10 // indirect
github.com/charmbracelet/lipgloss v0.9.1 // indirect
github.com/charmbracelet/x/exp/ordered v0.0.0-20231010190216-1cb11efc897d // indirect
github.com/chavacava/garif v0.1.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.0 // indirect github.com/chenzhuoyu/iasm v0.9.1 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/cloudflare/circl v1.3.7 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
github.com/coreos/go-semver v0.3.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
github.com/curioswitch/go-reassign v0.2.0 // indirect
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
github.com/daixiang0/gci v0.11.2 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/denis-tingaikin/go-header v0.4.3 // indirect
github.com/dghubble/go-twitter v0.0.0-20211115160449-93a8679adecb // indirect
github.com/dghubble/oauth1 v0.7.2 // indirect
github.com/dghubble/sling v1.4.0 // indirect
github.com/dimchansky/utfbom v1.1.1 // indirect
github.com/disgoorg/disgo v0.17.0 // indirect
github.com/disgoorg/json v1.1.0 // indirect
github.com/disgoorg/snowflake/v2 v2.0.1 // indirect
github.com/distribution/reference v0.5.0 // indirect
github.com/docker/cli v24.0.7+incompatible // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker v24.0.7+incompatible // indirect
github.com/docker/docker-credential-helpers v0.8.0 // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/elliotchance/orderedmap/v2 v2.2.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/esimonov/ifshort v1.0.4 // indirect
github.com/ettle/strcase v0.1.1 // indirect
github.com/evanphx/json-patch/v5 v5.6.0 // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/fatih/structtag v1.2.0 // indirect
github.com/firefart/nonamedreturns v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/fzipp/gocyclo v0.6.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/getkin/kin-openapi v0.118.0 // indirect
github.com/ghostiam/protogetter v0.2.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-critic/go-critic v0.9.0 // indirect
github.com/go-fed/httpsig v1.1.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.5.0 // indirect
github.com/go-git/go-git/v5 v5.11.0 // indirect
github.com/go-logr/logr v1.3.0 // indirect github.com/go-logr/logr v1.3.0 // indirect
github.com/go-openapi/analysis v0.21.4 // indirect
github.com/go-openapi/errors v0.20.4 // indirect
github.com/go-openapi/jsonpointer v0.20.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/loads v0.21.2 // indirect
github.com/go-openapi/runtime v0.26.0 // indirect
github.com/go-openapi/spec v0.20.9 // indirect
github.com/go-openapi/strfmt v0.21.7 // indirect
github.com/go-openapi/swag v0.22.4 // indirect
github.com/go-openapi/validate v0.22.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.15.1 // indirect github.com/go-playground/validator/v10 v10.16.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible // indirect
github.com/go-toolsmith/astcast v1.1.0 // indirect
github.com/go-toolsmith/astcopy v1.1.0 // indirect
github.com/go-toolsmith/astequal v1.1.0 // indirect
github.com/go-toolsmith/astfmt v1.1.0 // indirect
github.com/go-toolsmith/astp v1.1.0 // indirect
github.com/go-toolsmith/strparse v1.1.0 // indirect
github.com/go-toolsmith/typep v1.1.0 // indirect
github.com/go-xmlfmt/xmlfmt v1.1.2 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-json v0.10.2 // indirect
github.com/gofrs/flock v0.8.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang-jwt/jwt/v5 v5.1.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 // indirect
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a // indirect
github.com/golangci/go-misc v0.0.0-20220329215616-d24fe342adfe // indirect
github.com/golangci/gofmt v0.0.0-20231018234816-f50ced29576e // indirect
github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0 // indirect
github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca // indirect
github.com/golangci/misspell v0.4.1 // indirect
github.com/golangci/revgrep v0.5.2 // indirect
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 // indirect
github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-cmp v0.6.0 // indirect
github.com/google/go-containerregistry v0.17.0 // indirect
github.com/google/go-github/v57 v57.0.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect github.com/google/gofuzz v1.2.0 // indirect
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect github.com/google/ko v0.15.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect github.com/google/pprof v0.0.0-20231023181126-ff6d637d2a7b // indirect
github.com/google/rpmpack v0.5.0 // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2 // indirect
github.com/google/wire v0.5.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
github.com/gordonklaus/ineffassign v0.0.0-20230610083614-0e73809eb601 // indirect
github.com/goreleaser/chglog v0.5.0 // indirect
github.com/goreleaser/fileglob v1.3.0 // indirect
github.com/goreleaser/nfpm/v2 v2.35.1 // indirect
github.com/gorilla/websocket v1.5.1 // indirect
github.com/gostaticanalysis/analysisutil v0.7.1 // indirect
github.com/gostaticanalysis/comment v1.4.2 // indirect
github.com/gostaticanalysis/forcetypeassert v0.1.0 // indirect
github.com/gostaticanalysis/nilerr v0.1.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.4 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/hashicorp/hcl v1.0.1-vault-5 // indirect
github.com/hexops/gotextdiff v1.0.3 // indirect
github.com/huandu/xstrings v1.3.3 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/invopop/jsonschema v0.12.0 // indirect
github.com/invopop/yaml v0.1.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jgautheron/goconst v1.6.0 // indirect
github.com/jingyugao/rowserrcheck v1.1.1 // indirect
github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/julz/importas v0.1.0 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/kisielk/errcheck v1.6.3 // indirect
github.com/kisielk/gotool v1.0.0 // indirect
github.com/kkHAIKE/contextcheck v1.1.4 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/kulti/thelper v0.6.3 // indirect
github.com/kunwardeep/paralleltest v1.0.8 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/kyoh86/exportloopref v0.1.11 // indirect
github.com/ldez/gomoddirectives v0.2.3 // indirect
github.com/ldez/tagliatelle v0.5.0 // indirect
github.com/leodido/go-urn v1.2.4 // indirect github.com/leodido/go-urn v1.2.4 // indirect
github.com/leonklingele/grouper v1.1.1 // indirect
github.com/letsencrypt/boulder v0.0.0-20231026200631-000cd05d5491 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/lufeee/execinquery v1.2.1 // indirect
github.com/macabu/inamedparam v0.1.2 // indirect
github.com/magiconair/properties v1.8.7 // indirect github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/maratori/testableexamples v1.0.0 // indirect
github.com/maratori/testpackage v1.1.1 // indirect
github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-mastodon v0.0.6 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/mbilski/exhaustivestruct v1.2.0 // indirect
github.com/mgechev/revive v1.3.4 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/moricho/tparallel v0.3.1 // indirect
github.com/muesli/mango v0.1.0 // indirect
github.com/muesli/mango-cobra v1.2.0 // indirect
github.com/muesli/mango-pflag v0.1.0 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/roff v0.1.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/nakabonne/nestif v0.3.1 // indirect
github.com/nishanths/exhaustive v0.11.0 // indirect
github.com/nishanths/predeclared v0.2.2 // indirect
github.com/nunnatsa/ginkgolinter v0.14.1 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc5 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/perimeterx/marshmallow v1.1.4 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/polyfloyd/go-errorlint v1.4.5 // indirect
github.com/prometheus/client_golang v1.17.0 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.45.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/quasilyte/go-ruleguard v0.4.0 // indirect
github.com/quasilyte/gogrep v0.5.0 // indirect
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect
github.com/rivo/uniseg v0.4.2 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/ryancurrah/gomodguard v1.3.0 // indirect
github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect
github.com/sagikazarmark/locafero v0.3.0 // indirect github.com/sagikazarmark/locafero v0.3.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sanposhiho/wastedassign/v2 v2.0.7 // indirect
github.com/sasha-s/go-csync v0.0.0-20210812194225-61421b77c44b // indirect
github.com/sashamelentyev/interfacebloat v1.1.0 // indirect
github.com/sashamelentyev/usestdlibvars v1.24.0 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.7.0 // indirect
github.com/securego/gosec/v2 v2.18.2 // indirect
github.com/sergi/go-diff v1.2.0 // indirect
github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/sigstore/cosign/v2 v2.2.1 // indirect
github.com/sigstore/rekor v1.3.3 // indirect
github.com/sigstore/sigstore v1.7.5 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sivchari/containedctx v1.0.3 // indirect
github.com/sivchari/nosnakecase v1.7.0 // indirect
github.com/sivchari/tenv v1.7.1 // indirect
github.com/skeema/knownhosts v1.2.1 // indirect
github.com/slack-go/slack v0.12.3 // indirect
github.com/sonatard/noctx v0.0.2 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect
github.com/sourcegraph/go-diff v0.7.0 // indirect
github.com/spf13/afero v1.10.0 // indirect github.com/spf13/afero v1.10.0 // indirect
github.com/spf13/cast v1.5.1 // indirect github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.17.0 // indirect
github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect
github.com/stbenjam/no-sprintf-host-port v0.1.1 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/stretchr/testify v1.8.4 // indirect
github.com/subosito/gotenv v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect
github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c // indirect
github.com/tdakkota/asciicheck v0.2.0 // indirect
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
github.com/tetafro/godot v1.4.15 // indirect
github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966 // indirect
github.com/timonwong/loggercheck v0.9.4 // indirect
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect
github.com/tomarrell/wrapcheck/v2 v2.8.1 // indirect
github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect github.com/ugorji/go/codec v1.2.12 // indirect
github.com/ulikunitz/xz v0.5.11 // indirect
github.com/ultraware/funlen v0.1.0 // indirect
github.com/ultraware/whitespace v0.0.5 // indirect
github.com/uudashr/gocognit v1.1.2 // indirect
github.com/vbatts/tar-split v0.11.5 // indirect
github.com/withfig/autocomplete-tools/integrations/cobra v1.2.1 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/xanzy/go-gitlab v0.95.2 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xen0n/gosmopolitan v1.2.2 // indirect
github.com/yagipy/maintidx v1.0.0 // indirect
github.com/yeya24/promlinter v0.2.0 // indirect
github.com/ykadowak/zerologlint v0.1.3 // indirect
gitlab.com/bosi/decorder v0.4.1 // indirect
gitlab.com/digitalxero/go-conventional-commit v1.0.7 // indirect
go-simpler.org/sloglint v0.1.2 // indirect
go.mongodb.org/mongo-driver v1.12.1 // indirect
go.opencensus.io v0.24.0 // indirect
go.tmz.dev/musttag v0.7.2 // indirect
go.uber.org/automaxprocs v1.5.3 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
golang.org/x/arch v0.4.0 // indirect gocloud.dev v0.35.0 // indirect
golang.org/x/crypto v0.14.0 // indirect golang.org/x/arch v0.7.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/crypto v0.18.0 // indirect
golang.org/x/net v0.17.0 // indirect golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb // indirect
golang.org/x/sys v0.14.0 // indirect golang.org/x/exp/typeparams v0.0.0-20230307190834-24139beb5833 // indirect
golang.org/x/text v0.13.0 // indirect golang.org/x/net v0.19.0 // indirect
golang.org/x/tools v0.14.0 // indirect golang.org/x/oauth2 v0.15.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect golang.org/x/sync v0.5.0 // indirect
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect golang.org/x/sys v0.16.0 // indirect
golang.org/x/term v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.16.1 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/api v0.151.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto v0.0.0-20231120223509-83a465c0220f // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect
google.golang.org/grpc v1.59.0 // indirect
google.golang.org/protobuf v1.32.0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/go-jose/go-jose.v2 v2.6.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/mail.v2 v2.3.1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/klog/v2 v2.100.1 // indirect honnef.co/go/tools v0.4.6 // indirect
k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01 // indirect
k8s.io/klog/v2 v2.110.1 // indirect
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
mvdan.cc/gofumpt v0.5.0 // indirect
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed // indirect
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b // indirect
mvdan.cc/unparam v0.0.0-20221223090309-7455f1af531d // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.3.0 // indirect sigs.k8s.io/kind v0.20.0 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
) )

1259
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,7 @@ import (
"net/http" "net/http"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
"go.uber.org/zap"
) )
func (cl *client) doGet(req *resty.Request, url string) error { func (cl *client) doGet(req *resty.Request, url string) error {
@@ -22,9 +23,13 @@ func (cl *client) doGet(req *resty.Request, url string) error {
return ErrSetupNeeded return ErrSetupNeeded
} }
} }
rl.With("status", resp.StatusCode(), "body", string(resp.Body()), "error", err).Debug("error in do get") rl.With("status", resp.StatusCode(), "body", string(resp.Body()), "error", err).Debug("error in do get")
return detailedError(resp, err) return detailedError(resp, err)
} }
checkAuthenticationIssue(resp, rl)
rl.With( rl.With(
"status", resp.StatusCode(), "status", resp.StatusCode(),
"body", string(resp.Body()), "body", string(resp.Body()),
@@ -48,6 +53,9 @@ func (cl *client) doPost(req *resty.Request, url string) error {
rl.With("status", resp.StatusCode(), "body", string(resp.Body()), "error", err).Debug("error in do post") rl.With("status", resp.StatusCode(), "body", string(resp.Body()), "error", err).Debug("error in do post")
return detailedError(resp, err) return detailedError(resp, err)
} }
checkAuthenticationIssue(resp, rl)
rl.With( rl.With(
"status", resp.StatusCode(), "status", resp.StatusCode(),
"body", string(resp.Body()), "body", string(resp.Body()),
@@ -71,6 +79,9 @@ func (cl *client) doPut(req *resty.Request, url string) error {
rl.With("status", resp.StatusCode(), "body", string(resp.Body()), "error", err).Debug("error in do put") rl.With("status", resp.StatusCode(), "body", string(resp.Body()), "error", err).Debug("error in do put")
return detailedError(resp, err) return detailedError(resp, err)
} }
checkAuthenticationIssue(resp, rl)
rl.With( rl.With(
"status", resp.StatusCode(), "status", resp.StatusCode(),
"body", string(resp.Body()), "body", string(resp.Body()),
@@ -81,3 +92,10 @@ func (cl *client) doPut(req *resty.Request, url string) error {
} }
return nil return nil
} }
func checkAuthenticationIssue(resp *resty.Response, rl *zap.SugaredLogger) {
if resp != nil && (resp.StatusCode() == http.StatusUnauthorized || resp.StatusCode() == http.StatusForbidden) {
rl.With("status", resp.StatusCode()).Error("there seems to be an authentication issue - " +
"please check https://github.com/bakito/adguardhome-sync/wiki/FAQ")
}
}

View File

@@ -80,9 +80,9 @@ func New(config types.AdGuardInstance) (Client, error) {
} }
return &client{ return &client{
host: u.Host, host: config.Host,
client: cl, client: cl,
log: l.With("host", u.Host), log: l.With("host", config.Host),
}, nil }, nil
} }
@@ -96,9 +96,9 @@ type Client interface {
DeleteRewriteEntries(e ...model.RewriteEntry) error DeleteRewriteEntries(e ...model.RewriteEntry) error
Filtering() (*model.FilterStatus, error) Filtering() (*model.FilterStatus, error)
ToggleFiltering(enabled bool, interval int) error ToggleFiltering(enabled bool, interval int) error
AddFilters(whitelist bool, e ...model.Filter) error AddFilter(whitelist bool, f model.Filter) error
DeleteFilters(whitelist bool, e ...model.Filter) error DeleteFilter(whitelist bool, f model.Filter) error
UpdateFilters(whitelist bool, e ...model.Filter) error UpdateFilter(whitelist bool, f model.Filter) error
RefreshFilters(whitelist bool) error RefreshFilters(whitelist bool) error
SetCustomRules(rules *[]string) error SetCustomRules(rules *[]string) error
SafeBrowsing() (bool, error) SafeBrowsing() (bool, error)
@@ -114,9 +114,9 @@ type Client interface {
SetBlockedServices(services *model.BlockedServicesArray) error SetBlockedServices(services *model.BlockedServicesArray) error
SetBlockedServicesSchedule(schedule *model.BlockedServicesSchedule) error SetBlockedServicesSchedule(schedule *model.BlockedServicesSchedule) error
Clients() (*model.Clients, error) Clients() (*model.Clients, error)
AddClients(client ...*model.Client) error AddClient(client *model.Client) error
UpdateClients(client ...*model.Client) error UpdateClient(client *model.Client) error
DeleteClients(client ...*model.Client) error DeleteClient(client *model.Client) error
QueryLogConfig() (*model.QueryLogConfig, error) QueryLogConfig() (*model.QueryLogConfig, error)
SetQueryLogConfig(*model.QueryLogConfig) error SetQueryLogConfig(*model.QueryLogConfig) error
StatsConfig() (*model.StatsConfig, error) StatsConfig() (*model.StatsConfig, error)
@@ -128,8 +128,8 @@ type Client interface {
SetDNSConfig(*model.DNSConfig) error SetDNSConfig(*model.DNSConfig) error
DhcpConfig() (*model.DhcpStatus, error) DhcpConfig() (*model.DhcpStatus, error)
SetDhcpConfig(*model.DhcpStatus) error SetDhcpConfig(*model.DhcpStatus) error
AddDHCPStaticLeases(leases ...model.DhcpStaticLease) error AddDHCPStaticLease(lease model.DhcpStaticLease) error
DeleteDHCPStaticLeases(leases ...model.DhcpStaticLease) error DeleteDHCPStaticLease(lease model.DhcpStaticLease) error
} }
type client struct { type client struct {
@@ -169,7 +169,7 @@ func (cl *client) RewriteList() (*model.RewriteEntries, error) {
func (cl *client) AddRewriteEntries(entries ...model.RewriteEntry) error { func (cl *client) AddRewriteEntries(entries ...model.RewriteEntry) error {
for i := range entries { for i := range entries {
e := entries[i] e := entries[i]
cl.log.With("domain", e.Domain, "answer", e.Answer).Info("Add rewrite entry") cl.log.With("domain", e.Domain, "answer", e.Answer).Info("Add DNS rewrite entry")
err := cl.doPost(cl.client.R().EnableTrace().SetBody(&e), "/rewrite/add") err := cl.doPost(cl.client.R().EnableTrace().SetBody(&e), "/rewrite/add")
if err != nil { if err != nil {
return err return err
@@ -181,7 +181,7 @@ func (cl *client) AddRewriteEntries(entries ...model.RewriteEntry) error {
func (cl *client) DeleteRewriteEntries(entries ...model.RewriteEntry) error { func (cl *client) DeleteRewriteEntries(entries ...model.RewriteEntry) error {
for i := range entries { for i := range entries {
e := entries[i] e := entries[i]
cl.log.With("domain", e.Domain, "answer", e.Answer).Info("Delete rewrite entry") cl.log.With("domain", e.Domain, "answer", e.Answer).Info("Delete DNS rewrite entry")
err := cl.doPost(cl.client.R().EnableTrace().SetBody(&e), "/rewrite/delete") err := cl.doPost(cl.client.R().EnableTrace().SetBody(&e), "/rewrite/delete")
if err != nil { if err != nil {
return err return err
@@ -229,43 +229,25 @@ func (cl *client) Filtering() (*model.FilterStatus, error) {
return f, err return f, err
} }
func (cl *client) AddFilters(whitelist bool, filters ...model.Filter) error { func (cl *client) AddFilter(whitelist bool, f model.Filter) error {
for _, f := range filters { cl.log.With("url", f.Url, "whitelist", whitelist, "enabled", f.Enabled).Info("Add filter")
cl.log.With("url", f.Url, "whitelist", whitelist, "enabled", f.Enabled).Info("Add filter") ff := &model.AddUrlRequest{Name: utils.Ptr(f.Name), Url: utils.Ptr(f.Url), Whitelist: utils.Ptr(whitelist)}
ff := &model.AddUrlRequest{Name: utils.Ptr(f.Name), Url: utils.Ptr(f.Url), Whitelist: utils.Ptr(whitelist)} return cl.doPost(cl.client.R().EnableTrace().SetBody(ff), "/filtering/add_url")
err := cl.doPost(cl.client.R().EnableTrace().SetBody(ff), "/filtering/add_url")
if err != nil {
return err
}
}
return nil
} }
func (cl *client) DeleteFilters(whitelist bool, filters ...model.Filter) error { func (cl *client) DeleteFilter(whitelist bool, f model.Filter) error {
for _, f := range filters { cl.log.With("url", f.Url, "whitelist", whitelist, "enabled", f.Enabled).Info("Delete filter")
cl.log.With("url", f.Url, "whitelist", whitelist, "enabled", f.Enabled).Info("Delete filter") ff := &model.RemoveUrlRequest{Url: utils.Ptr(f.Url), Whitelist: utils.Ptr(whitelist)}
ff := &model.RemoveUrlRequest{Url: utils.Ptr(f.Url), Whitelist: utils.Ptr(whitelist)} return cl.doPost(cl.client.R().EnableTrace().SetBody(ff), "/filtering/remove_url")
err := cl.doPost(cl.client.R().EnableTrace().SetBody(ff), "/filtering/remove_url")
if err != nil {
return err
}
}
return nil
} }
func (cl *client) UpdateFilters(whitelist bool, filters ...model.Filter) error { func (cl *client) UpdateFilter(whitelist bool, f model.Filter) error {
for _, f := range filters { cl.log.With("url", f.Url, "whitelist", whitelist, "enabled", f.Enabled).Info("Update filter")
cl.log.With("url", f.Url, "whitelist", whitelist, "enabled", f.Enabled).Info("Update filter") fu := &model.FilterSetUrl{
fu := &model.FilterSetUrl{ Whitelist: utils.Ptr(whitelist), Url: utils.Ptr(f.Url),
Whitelist: utils.Ptr(whitelist), Url: utils.Ptr(f.Url), Data: &model.FilterSetUrlData{Name: f.Name, Url: f.Url, Enabled: f.Enabled},
Data: &model.FilterSetUrlData{Name: f.Name, Url: f.Url, Enabled: f.Enabled},
}
err := cl.doPost(cl.client.R().EnableTrace().SetBody(fu), "/filtering/set_url")
if err != nil {
return err
}
} }
return nil return cl.doPost(cl.client.R().EnableTrace().SetBody(fu), "/filtering/set_url")
} }
func (cl *client) RefreshFilters(whitelist bool) error { func (cl *client) RefreshFilters(whitelist bool) error {
@@ -319,39 +301,19 @@ func (cl *client) Clients() (*model.Clients, error) {
return clients, err return clients, err
} }
func (cl *client) AddClients(clients ...*model.Client) error { func (cl *client) AddClient(client *model.Client) error {
for i := range clients { cl.log.With("name", *client.Name).Info("Add client settings")
client := clients[i] return cl.doPost(cl.client.R().EnableTrace().SetBody(client), "/clients/add")
cl.log.With("name", *client.Name).Info("Add client")
err := cl.doPost(cl.client.R().EnableTrace().SetBody(client), "/clients/add")
if err != nil {
return err
}
}
return nil
} }
func (cl *client) UpdateClients(clients ...*model.Client) error { func (cl *client) UpdateClient(client *model.Client) error {
for _, client := range clients { cl.log.With("name", *client.Name).Info("Update client settings")
cl.log.With("name", *client.Name).Info("Update client") return cl.doPost(cl.client.R().EnableTrace().SetBody(&model.ClientUpdate{Name: client.Name, Data: client}), "/clients/update")
err := cl.doPost(cl.client.R().EnableTrace().SetBody(&model.ClientUpdate{Name: client.Name, Data: client}), "/clients/update")
if err != nil {
return err
}
}
return nil
} }
func (cl *client) DeleteClients(clients ...*model.Client) error { func (cl *client) DeleteClient(client *model.Client) error {
for i := range clients { cl.log.With("name", *client.Name).Info("Delete client settings")
client := clients[i] return cl.doPost(cl.client.R().EnableTrace().SetBody(client), "/clients/delete")
cl.log.With("name", *client.Name).Info("Delete client")
err := cl.doPost(cl.client.R().EnableTrace().SetBody(client), "/clients/delete")
if err != nil {
return err
}
}
return nil
} }
func (cl *client) QueryLogConfig() (*model.QueryLogConfig, error) { func (cl *client) QueryLogConfig() (*model.QueryLogConfig, error) {
@@ -435,24 +397,20 @@ func (cl *client) SetDhcpConfig(config *model.DhcpStatus) error {
return cl.doPost(cl.client.R().EnableTrace().SetBody(config), "/dhcp/set_config") return cl.doPost(cl.client.R().EnableTrace().SetBody(config), "/dhcp/set_config")
} }
func (cl *client) AddDHCPStaticLeases(leases ...model.DhcpStaticLease) error { func (cl *client) AddDHCPStaticLease(l model.DhcpStaticLease) error {
for _, l := range leases { cl.log.With("mac", l.Mac, "ip", l.Ip, "hostname", l.Hostname).Info("Add static dhcp lease")
cl.log.With("mac", l.Mac, "ip", l.Ip, "hostname", l.Hostname).Info("Add static dhcp lease") err := cl.doPost(cl.client.R().EnableTrace().SetBody(l), "/dhcp/add_static_lease")
err := cl.doPost(cl.client.R().EnableTrace().SetBody(l), "/dhcp/add_static_lease") if err != nil {
if err != nil { return err
return err
}
} }
return nil return nil
} }
func (cl *client) DeleteDHCPStaticLeases(leases ...model.DhcpStaticLease) error { func (cl *client) DeleteDHCPStaticLease(l model.DhcpStaticLease) error {
for _, l := range leases { cl.log.With("mac", l.Mac, "ip", l.Ip, "hostname", l.Hostname).Info("Delete static dhcp lease")
cl.log.With("mac", l.Mac, "ip", l.Ip, "hostname", l.Hostname).Info("Delete static dhcp lease") err := cl.doPost(cl.client.R().EnableTrace().SetBody(l), "/dhcp/remove_static_lease")
err := cl.doPost(cl.client.R().EnableTrace().SetBody(l), "/dhcp/remove_static_lease") if err != nil {
if err != nil { return err
return err
}
} }
return nil return nil
} }

View File

@@ -35,7 +35,10 @@ var _ = Describe("Client", func() {
Context("Host", func() { Context("Host", func() {
It("should read the current host", func() { It("should read the current host", func() {
cl, _ := client.New(types.AdGuardInstance{URL: "https://foo.bar:3000"}) inst := types.AdGuardInstance{URL: "https://foo.bar:3000"}
err := inst.Init()
Ω(err).ShouldNot(HaveOccurred())
cl, _ := client.New(inst)
host := cl.Host() host := cl.Host()
Ω(host).Should(Equal("foo.bar:3000")) Ω(host).Should(Equal("foo.bar:3000"))
}) })
@@ -69,7 +72,9 @@ var _ = Describe("Client", func() {
`{"name":"","url":"foo","whitelist":true}`, `{"name":"","url":"foo","whitelist":true}`,
`{"name":"","url":"bar","whitelist":true}`, `{"name":"","url":"bar","whitelist":true}`,
) )
err := cl.AddFilters(true, model.Filter{Url: "foo"}, model.Filter{Url: "bar"}) err := cl.AddFilter(true, model.Filter{Url: "foo"})
Ω(err).ShouldNot(HaveOccurred())
err = cl.AddFilter(true, model.Filter{Url: "bar"})
Ω(err).ShouldNot(HaveOccurred()) Ω(err).ShouldNot(HaveOccurred())
}) })
It("should update Filters", func() { It("should update Filters", func() {
@@ -77,7 +82,9 @@ var _ = Describe("Client", func() {
`{"data":{"enabled":false,"name":"","url":"foo"},"url":"foo","whitelist":true}`, `{"data":{"enabled":false,"name":"","url":"foo"},"url":"foo","whitelist":true}`,
`{"data":{"enabled":false,"name":"","url":"bar"},"url":"bar","whitelist":true}`, `{"data":{"enabled":false,"name":"","url":"bar"},"url":"bar","whitelist":true}`,
) )
err := cl.UpdateFilters(true, model.Filter{Url: "foo"}, model.Filter{Url: "bar"}) err := cl.UpdateFilter(true, model.Filter{Url: "foo"})
Ω(err).ShouldNot(HaveOccurred())
err = cl.UpdateFilter(true, model.Filter{Url: "bar"})
Ω(err).ShouldNot(HaveOccurred()) Ω(err).ShouldNot(HaveOccurred())
}) })
It("should delete Filters", func() { It("should delete Filters", func() {
@@ -85,7 +92,9 @@ var _ = Describe("Client", func() {
`{"url":"foo","whitelist":true}`, `{"url":"foo","whitelist":true}`,
`{"url":"bar","whitelist":true}`, `{"url":"bar","whitelist":true}`,
) )
err := cl.DeleteFilters(true, model.Filter{Url: "foo"}, model.Filter{Url: "bar"}) err := cl.DeleteFilter(true, model.Filter{Url: "foo"})
Ω(err).ShouldNot(HaveOccurred())
err = cl.DeleteFilter(true, model.Filter{Url: "bar"})
Ω(err).ShouldNot(HaveOccurred()) Ω(err).ShouldNot(HaveOccurred())
}) })
}) })
@@ -264,21 +273,21 @@ var _ = Describe("Client", func() {
ts, cl = ClientPost("/clients/add", ts, cl = ClientPost("/clients/add",
`{"ids":["id"],"name":"foo"}`, `{"ids":["id"],"name":"foo"}`,
) )
err := cl.AddClients(&model.Client{Name: utils.Ptr("foo"), Ids: utils.Ptr([]string{"id"})}) err := cl.AddClient(&model.Client{Name: utils.Ptr("foo"), Ids: utils.Ptr([]string{"id"})})
Ω(err).ShouldNot(HaveOccurred()) Ω(err).ShouldNot(HaveOccurred())
}) })
It("should update Clients", func() { It("should update Clients", func() {
ts, cl = ClientPost("/clients/update", ts, cl = ClientPost("/clients/update",
`{"data":{"ids":["id"],"name":"foo"},"name":"foo"}`, `{"data":{"ids":["id"],"name":"foo"},"name":"foo"}`,
) )
err := cl.UpdateClients(&model.Client{Name: utils.Ptr("foo"), Ids: utils.Ptr([]string{"id"})}) err := cl.UpdateClient(&model.Client{Name: utils.Ptr("foo"), Ids: utils.Ptr([]string{"id"})})
Ω(err).ShouldNot(HaveOccurred()) Ω(err).ShouldNot(HaveOccurred())
}) })
It("should delete Clients", func() { It("should delete Clients", func() {
ts, cl = ClientPost("/clients/delete", ts, cl = ClientPost("/clients/delete",
`{"ids":["id"],"name":"foo"}`, `{"ids":["id"],"name":"foo"}`,
) )
err := cl.DeleteClients(&model.Client{Name: utils.Ptr("foo"), Ids: utils.Ptr([]string{"id"})}) err := cl.DeleteClient(&model.Client{Name: utils.Ptr("foo"), Ids: utils.Ptr([]string{"id"})})
Ω(err).ShouldNot(HaveOccurred()) Ω(err).ShouldNot(HaveOccurred())
}) })
}) })

View File

@@ -16,6 +16,22 @@ func (c *DhcpStatus) Clone() *DhcpStatus {
return clone return clone
} }
func (c *DhcpStatus) cleanV4V6() {
if c.V4 != nil && !c.V4.isValid() {
c.V4 = nil
}
if c.V6 != nil && !c.V6.isValid() {
c.V6 = nil
}
}
// 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 { func (c *DhcpStatus) Equals(o *DhcpStatus) bool {
return utils.JsonEquals(c, o) return utils.JsonEquals(c, o)
@@ -26,11 +42,14 @@ func (c *DhcpStatus) HasConfig() bool {
} }
func (j DhcpConfigV4) isValid() bool { func (j DhcpConfigV4) isValid() bool {
return j.GatewayIp != nil && j.SubnetMask != nil && j.RangeStart != nil && j.RangeEnd != nil return j.GatewayIp != nil && *j.GatewayIp != "" &&
j.SubnetMask != nil && *j.SubnetMask != "" &&
j.RangeStart != nil && *j.RangeStart != "" &&
j.RangeEnd != nil && *j.RangeEnd != ""
} }
func (j DhcpConfigV6) isValid() bool { func (j DhcpConfigV6) isValid() bool {
return j.RangeStart != nil return j.RangeStart != nil && *j.RangeStart != ""
} }
type DhcpStaticLeases []DhcpStaticLease type DhcpStaticLeases []DhcpStaticLease

View File

@@ -287,11 +287,28 @@ type Client struct {
SafeSearch *SafeSearchConfig `json:"safe_search,omitempty"` SafeSearch *SafeSearchConfig `json:"safe_search,omitempty"`
SafebrowsingEnabled *bool `json:"safebrowsing_enabled,omitempty"` SafebrowsingEnabled *bool `json:"safebrowsing_enabled,omitempty"`
// Deprecated: // Deprecated:
SafesearchEnabled *bool `json:"safesearch_enabled,omitempty"` SafesearchEnabled *bool `json:"safesearch_enabled,omitempty"`
Tags *[]string `json:"tags,omitempty"` Tags *[]string `json:"tags,omitempty"`
Upstreams *[]string `json:"upstreams,omitempty"` Upstreams *[]string `json:"upstreams,omitempty"`
UseGlobalBlockedServices *bool `json:"use_global_blocked_services,omitempty"`
UseGlobalSettings *bool `json:"use_global_settings,omitempty"` // UpstreamsCacheEnabled NOTE: If `upstreams_cache_enabled` is not set in HTTP API
// `GET /clients/add` request then default value (false) will be used.
//
// If `upstreams_cache_enabled` is not set in HTTP API
// `GET /clients/update` request then the existing value will not be
// changed.
//
// This behaviour can be changed in the future versions.
UpstreamsCacheEnabled *bool `json:"upstreams_cache_enabled,omitempty"`
// UpstreamsCacheSize NOTE: If `upstreams_cache_enabled` is not set in HTTP API
// `GET /clients/update` request then the existing value will not be
// changed.
//
// This behaviour can be changed in the future versions.
UpstreamsCacheSize *int `json:"upstreams_cache_size,omitempty"`
UseGlobalBlockedServices *bool `json:"use_global_blocked_services,omitempty"`
UseGlobalSettings *bool `json:"use_global_settings,omitempty"`
} }
// ClientAuto Auto-Client information // ClientAuto Auto-Client information
@@ -402,7 +419,16 @@ type DNSConfig struct {
ProtectionDisabledUntil *string `json:"protection_disabled_until,omitempty"` ProtectionDisabledUntil *string `json:"protection_disabled_until,omitempty"`
ProtectionEnabled *bool `json:"protection_enabled,omitempty"` ProtectionEnabled *bool `json:"protection_enabled,omitempty"`
Ratelimit *int `json:"ratelimit,omitempty"` Ratelimit *int `json:"ratelimit,omitempty"`
ResolveClients *bool `json:"resolve_clients,omitempty"`
// RatelimitSubnetSubnetLenIpv4 Length of the subnet mask for IPv4 addresses.
RatelimitSubnetSubnetLenIpv4 *int `json:"ratelimit_subnet_subnet_len_ipv4,omitempty"`
// RatelimitSubnetSubnetLenIpv6 Length of the subnet mask for IPv6 addresses.
RatelimitSubnetSubnetLenIpv6 *int `json:"ratelimit_subnet_subnet_len_ipv6,omitempty"`
// RatelimitWhitelist List of IP addresses excluded from rate limiting.
RatelimitWhitelist *[]string `json:"ratelimit_whitelist,omitempty"`
ResolveClients *bool `json:"resolve_clients,omitempty"`
// UpstreamDns Upstream servers, port is optional after colon. Empty value will reset it to default values. // UpstreamDns Upstream servers, port is optional after colon. Empty value will reset it to default values.
UpstreamDns *[]string `json:"upstream_dns,omitempty"` UpstreamDns *[]string `json:"upstream_dns,omitempty"`

View File

@@ -0,0 +1,74 @@
package model
import (
"github.com/bakito/adguardhome-sync/pkg/utils"
. "github.com/onsi/ginkgo/v2"
"github.com/onsi/gomega"
)
var _ = Describe("Types", func() {
Context("DhcpConfigV4", func() {
DescribeTable("DhcpConfigV4 should not be valid",
func(v4 DhcpConfigV4) {
gomega.Ω(v4.isValid()).Should(gomega.BeFalse())
},
Entry(`When GatewayIp is nil`, DhcpConfigV4{
GatewayIp: nil,
SubnetMask: utils.Ptr("2.2.2.2"),
RangeStart: utils.Ptr("3.3.3.3"),
RangeEnd: utils.Ptr("4.4.4.4"),
}),
Entry(`When GatewayIp is ""`, DhcpConfigV4{
GatewayIp: utils.Ptr(""),
SubnetMask: utils.Ptr("2.2.2.2"),
RangeStart: utils.Ptr("3.3.3.3"),
RangeEnd: utils.Ptr("4.4.4.4"),
}),
Entry(`When SubnetMask is nil`, DhcpConfigV4{
GatewayIp: utils.Ptr("1.1.1.1"),
SubnetMask: nil,
RangeStart: utils.Ptr("3.3.3.3"),
RangeEnd: utils.Ptr("4.4.4.4"),
}),
Entry(`When SubnetMask is ""`, DhcpConfigV4{
GatewayIp: utils.Ptr("1.1.1.1"),
SubnetMask: utils.Ptr(""),
RangeStart: utils.Ptr("3.3.3.3"),
RangeEnd: utils.Ptr("4.4.4.4"),
}),
Entry(`When SubnetMask is nil`, DhcpConfigV4{
GatewayIp: utils.Ptr("1.1.1.1"),
SubnetMask: utils.Ptr("2.2.2.2"),
RangeStart: nil,
RangeEnd: utils.Ptr("4.4.4.4"),
}),
Entry(`When SubnetMask is ""`, DhcpConfigV4{
GatewayIp: utils.Ptr("1.1.1.1"),
SubnetMask: utils.Ptr("2.2.2.2"),
RangeStart: utils.Ptr(""),
RangeEnd: utils.Ptr("4.4.4.4"),
}),
Entry(`When RangeEnd is nil`, DhcpConfigV4{
GatewayIp: utils.Ptr("1.1.1.1"),
SubnetMask: utils.Ptr("2.2.2.2"),
RangeStart: utils.Ptr("3.3.3.3"),
RangeEnd: nil,
}),
Entry(`When RangeEnd is ""`, DhcpConfigV4{
GatewayIp: utils.Ptr("1.1.1.1"),
SubnetMask: utils.Ptr("2.2.2.2"),
RangeStart: utils.Ptr("3.3.3.3"),
RangeEnd: utils.Ptr(""),
}),
)
})
Context("DhcpConfigV6", func() {
DescribeTable("DhcpConfigV6 should not be valid",
func(v6 DhcpConfigV6) {
gomega.Ω(v6.isValid()).Should(gomega.BeFalse())
},
Entry(`When SubnetMask is nil`, DhcpConfigV6{RangeStart: nil}),
Entry(`When SubnetMask is ""`, DhcpConfigV6{RangeStart: utils.Ptr("")}),
)
})
})

View File

@@ -403,7 +403,29 @@ var _ = Describe("Types", func() {
} }
Ω(dc1.HasConfig()).Should(BeFalse()) Ω(dc1.HasConfig()).Should(BeFalse())
}) })
It("should not have a v4 config", func() { It("should not have a v4 config with nil IP", func() {
dc1 := &model.DhcpStatus{
V4: &model.DhcpConfigV4{
GatewayIp: nil,
},
V6: &model.DhcpConfigV6{
RangeStart: utils.Ptr("1.2.3.5"),
},
}
Ω(dc1.HasConfig()).Should(BeTrue())
})
It("should not have a v4 config with empty IP", func() {
dc1 := &model.DhcpStatus{
V4: &model.DhcpConfigV4{
GatewayIp: utils.Ptr(""),
},
V6: &model.DhcpConfigV6{
RangeStart: utils.Ptr("1.2.3.5"),
},
}
Ω(dc1.HasConfig()).Should(BeTrue())
})
It("should not have a v6 config", func() {
dc1 := &model.DhcpStatus{ dc1 := &model.DhcpStatus{
V4: &model.DhcpConfigV4{ V4: &model.DhcpConfigV4{
GatewayIp: utils.Ptr("1.2.3.4"), GatewayIp: utils.Ptr("1.2.3.4"),
@@ -416,15 +438,6 @@ var _ = Describe("Types", func() {
} }
Ω(dc1.HasConfig()).Should(BeTrue()) Ω(dc1.HasConfig()).Should(BeTrue())
}) })
It("should not have a v6 config", func() {
dc1 := &model.DhcpStatus{
V4: &model.DhcpConfigV4{},
V6: &model.DhcpConfigV6{
RangeStart: utils.Ptr("1.2.3.5"),
},
}
Ω(dc1.HasConfig()).Should(BeTrue())
})
}) })
}) })
}) })

95
pkg/config/config.go Normal file
View File

@@ -0,0 +1,95 @@
package config
import (
"errors"
"regexp"
"github.com/bakito/adguardhome-sync/pkg/log"
"github.com/bakito/adguardhome-sync/pkg/types"
"github.com/caarlos0/env/v10"
)
var (
envReplicasURLPattern = regexp.MustCompile(`^REPLICA(\d+)_URL=(.*)`)
logger = log.GetLogger("config")
)
func Get(configFile string, flags Flags) (*types.Config, error) {
path, err := configFilePath(configFile)
if err != nil {
return nil, err
}
cfg := initialConfig()
// read yaml config
if err := readFile(cfg, path); err != nil {
return nil, err
}
// overwrite from command flags
if err := readFlags(cfg, flags); err != nil {
return nil, err
}
// *bool field creates issues when already not nil
cfg.Origin.DHCPServerEnabled = nil // origin filed makes no sense to be set.
// keep previously set value
replicaDhcpServer := cfg.Replica.DHCPServerEnabled
cfg.Replica.DHCPServerEnabled = nil
// overwrite from env vars
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 not set from env, use previous value
if cfg.Replica.DHCPServerEnabled == nil {
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 == "" {
cfg.Replica = nil
}
if len(cfg.Replicas) > 0 && cfg.Replica != nil {
return nil, errors.New("mixed replica config in use. " +
"Do not use single replica and numbered (list) replica config combined")
}
handleDeprecatedEnvVars(cfg)
if cfg.Replica != nil {
cfg.Replicas = []types.AdGuardInstance{*cfg.Replica}
cfg.Replica = nil
}
cfg.Replicas, err = enrichReplicasFromEnv(cfg.Replicas)
return cfg, err
}
func initialConfig() *types.Config {
return &types.Config{
RunOnStart: true,
Origin: types.AdGuardInstance{
APIPath: "/control",
},
Replica: &types.AdGuardInstance{
APIPath: "/control",
},
API: types.API{
Port: 8080,
},
Features: types.NewFeatures(true),
}
}

View File

@@ -1,4 +1,4 @@
package cmd_test package config_test
import ( import (
"testing" "testing"
@@ -9,5 +9,5 @@ import (
func TestCmd(t *testing.T) { func TestCmd(t *testing.T) {
RegisterFailHandler(Fail) RegisterFailHandler(Fail)
RunSpecs(t, "Cmd Suite") RunSpecs(t, "Config Suite")
} }

249
pkg/config/config_test.go Normal file
View File

@@ -0,0 +1,249 @@
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"
)
var _ = Describe("Config", func() {
Context("Get", func() {
var (
flags *flagsmock.MockFlags
mockCtrl *gm.Controller
)
BeforeEach(func() {
mockCtrl = gm.NewController(GinkgoT())
flags = flagsmock.NewMockFlags(mockCtrl)
})
AfterEach(func() {
defer mockCtrl.Finish()
})
Context("Get", func() {
Context("Mixed 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).Should(HaveOccurred())
Ω(err.Error()).Should(ContainSubstring("mixed replica config in use"))
})
})
Context("Origin Url", func() {
It("should have the origin URL 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.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)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Origin.URL).Should(Equal("https://origin-flag:443"))
})
It("should have the origin URL from the config env var", func() {
os.Setenv("ORIGIN_URL", "https://origin-env:443")
defer func() {
_ = os.Unsetenv("ORIGIN_URL")
}()
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)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.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)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.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)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Replicas[0].InsecureSkipVerify).Should(BeTrue())
})
It("should have the insecure skip verify from the config env var", func() {
os.Setenv("REPLICA_INSECURE_SKIP_VERIFY", "false")
defer func() {
_ = os.Unsetenv("REPLICA_INSECURE_SKIP_VERIFY")
}()
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)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Replicas[0].InsecureSkipVerify).Should(BeFalse())
})
})
Context("Replica 1 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_replicas.yaml", flags)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Replicas[0].InsecureSkipVerify).Should(BeFalse())
})
It("should have the insecure skip verify from the config env var", func() {
os.Setenv("REPLICA1_INSECURE_SKIP_VERIFY", "true")
defer func() {
_ = os.Unsetenv("REPLICA1_INSECURE_SKIP_VERIFY")
}()
flags.EXPECT().Changed(gm.Any()).Return(false).AnyTimes()
cfg, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.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)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.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(gm.Any()).Return(false).AnyTimes()
flags.EXPECT().GetInt(config.FlagApiPort).Return(9990, nil).AnyTimes()
cfg, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.API.Port).Should(Equal(9990))
})
It("should have the api port from the config env var", func() {
os.Setenv("API_PORT", "9999")
defer func() {
_ = os.Unsetenv("API_PORT")
}()
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()
cfg, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.API.Port).Should(Equal(9999))
})
})
Context("Replica DHCPServerEnabled", 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)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Replicas[0].DHCPServerEnabled).ShouldNot(BeNil())
Ω(*cfg.Replicas[0].DHCPServerEnabled).Should(BeFalse())
})
})
Context("Replica 1 DHCPServerEnabled", 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)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Replicas[0].DHCPServerEnabled).ShouldNot(BeNil())
Ω(*cfg.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)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.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(gm.Any()).Return(false).AnyTimes()
flags.EXPECT().GetInt(config.FlagApiPort).Return(9990, nil).AnyTimes()
cfg, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.API.Port).Should(Equal(9990))
})
It("should have the api port from the config env var", func() {
os.Setenv("API_PORT", "9999")
defer func() {
_ = os.Unsetenv("API_PORT")
}()
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()
cfg, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.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)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.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(gm.Any()).Return(false).AnyTimes()
flags.EXPECT().GetBool(config.FlagFeatureDnsServerConfig).Return(true, nil).AnyTimes()
cfg, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Features.DNS.ServerConfig).Should(BeTrue())
})
It("should have the feature dns server config from the config env var", func() {
os.Setenv("FEATURES_DNS_SERVER_CONFIG", "false")
defer func() {
_ = os.Unsetenv("FEATURES_DNS_SERVER_CONFIG")
}()
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()
cfg, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Features.DNS.ServerConfig).Should(BeFalse())
})
It("should have the feature dns server config from the config DEPRECATED env var", func() {
os.Setenv("FEATURES_DNS_SERVERCONFIG", "false")
defer func() {
_ = os.Unsetenv("FEATURES_DNS_SERVERCONFIG")
}()
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()
cfg, err := config.Get("../../testdata/config_test_replicas.yaml", flags)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Features.DNS.ServerConfig).Should(BeFalse())
})
})
})
})
})

View File

@@ -0,0 +1,138 @@
package config_test
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"
)
var envVars = []string{
"FEATURES_GENERAL_SETTINGS",
"FEATURES_QUERY_LOG_CONFIG",
"FEATURES_STATS_CONFIG",
"FEATURES_CLIENT_SETTINGS",
"FEATURES_SERVICES",
"FEATURES_FILTERS",
"FEATURES_DHCP_SERVER_CONFIG",
"FEATURES_DHCP_STATIC_LEASES",
"FEATURES_DNS_SERVER_CONFIG",
"FEATURES_DNS_ACCESS_LISTS",
"FEATURES_DNS_REWRITES",
"REPLICA1_INTERFACE_NAME",
"REPLICA1_DHCP_SERVER_ENABLED",
}
var deprecatedEnvVars = []string{
"FEATURES_GENERALSETTINGS",
"FEATURES_QUERYLOGCONFIG",
"FEATURES_STATSCONFIG",
"FEATURES_CLIENTSETTINGS",
"FEATURES_SERVICES",
"FEATURES_FILTERS",
"FEATURES_DHCP_SERVERCONFIG",
"FEATURES_DHCP_STATICLEASES",
"FEATURES_DNS_SERVERCONFIG",
"FEATURES_DNS_ACCESSLISTS",
"FEATURES_DNS_REWRITES",
"REPLICA1_INTERFACENAME",
"REPLICA1_DHCPSERVERENABLED",
}
var _ = Describe("Config", func() {
Context("deprecated", func() {
BeforeEach(func() {
for _, envVar := range deprecatedEnvVars {
Ω(os.Setenv(envVar, "false")).ShouldNot(HaveOccurred())
}
})
AfterEach(func() {
for _, envVar := range deprecatedEnvVars {
Ω(os.Unsetenv(envVar)).ShouldNot(HaveOccurred())
}
})
Context("Get", func() {
It("features should be false", func() {
cfg, err := config.Get("", nil)
Ω(err).ShouldNot(HaveOccurred())
verifyFeatures(cfg, false)
})
})
})
Context("current", func() {
BeforeEach(func() {
for _, envVar := range envVars {
Ω(os.Unsetenv(envVar)).ShouldNot(HaveOccurred())
}
})
AfterEach(func() {
for _, envVar := range envVars {
Ω(os.Unsetenv(envVar)).ShouldNot(HaveOccurred())
}
})
Context("Get", func() {
It("features should be true by default", func() {
cfg, err := config.Get("", nil)
Ω(err).ShouldNot(HaveOccurred())
verifyFeatures(cfg, true)
})
It("features should be true by default", func() {
cfg, err := config.Get("", nil)
Ω(err).ShouldNot(HaveOccurred())
verifyFeatures(cfg, true)
})
It("features should be false", func() {
for _, envVar := range envVars {
Ω(os.Setenv(envVar, "false")).ShouldNot(HaveOccurred())
}
cfg, err := config.Get("", nil)
Ω(err).ShouldNot(HaveOccurred())
verifyFeatures(cfg, false)
})
Context("interface name", 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)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.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)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Replicas[0].DHCPServerEnabled).ShouldNot(BeNil())
Ω(*cfg.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)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Replicas[0].DHCPServerEnabled).ShouldNot(BeNil())
Ω(*cfg.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))
}

145
pkg/config/env.go Normal file
View File

@@ -0,0 +1,145 @@
package config
import (
"fmt"
"os"
"strconv"
"strings"
"github.com/bakito/adguardhome-sync/pkg/types"
"github.com/bakito/adguardhome-sync/pkg/utils"
"github.com/caarlos0/env/v10"
)
func handleDeprecatedEnvVars(cfg *types.Config) {
if val, ok := checkDeprecatedEnvVar("RUNONSTART", "RUN_ON_START"); ok {
cfg.RunOnStart, _ = strconv.ParseBool(val)
}
if val, ok := checkDeprecatedEnvVar("API_DARKMODE", "API_DARK_MODE"); ok {
cfg.API.DarkMode, _ = strconv.ParseBool(val)
}
if val, ok := checkDeprecatedEnvVar("FEATURES_GENERALSETTINGS", "FEATURES_GENERAL_SETTINGS"); ok {
cfg.Features.GeneralSettings, _ = strconv.ParseBool(val)
}
if val, ok := checkDeprecatedEnvVar("FEATURES_QUERYLOGCONFIG", "FEATURES_QUERY_LOG_CONFIG"); ok {
cfg.Features.QueryLogConfig, _ = strconv.ParseBool(val)
}
if val, ok := checkDeprecatedEnvVar("FEATURES_STATSCONFIG", "FEATURES_STATS_CONFIG"); ok {
cfg.Features.StatsConfig, _ = strconv.ParseBool(val)
}
if val, ok := checkDeprecatedEnvVar("FEATURES_CLIENTSETTINGS", "FEATURES_CLIENT_SETTINGS"); ok {
cfg.Features.ClientSettings, _ = strconv.ParseBool(val)
}
if val, ok := checkDeprecatedEnvVar("FEATURES_DHCP_SERVERCONFIG", "FEATURES_DHCP_SERVER_CONFIG"); ok {
cfg.Features.DHCP.ServerConfig, _ = strconv.ParseBool(val)
}
if val, ok := checkDeprecatedEnvVar("FEATURES_DHCP_STATICLEASES", "FEATURES_DHCP_STATIC_LEASES"); ok {
cfg.Features.DHCP.StaticLeases, _ = strconv.ParseBool(val)
}
if val, ok := checkDeprecatedEnvVar("FEATURES_DNS_ACCESSLISTS", "FEATURES_DNS_ACCESS_LISTS"); ok {
cfg.Features.DNS.AccessLists, _ = strconv.ParseBool(val)
}
if val, ok := checkDeprecatedEnvVar("FEATURES_DNS_SERVERCONFIG", "FEATURES_DNS_SERVER_CONFIG"); ok {
cfg.Features.DNS.ServerConfig, _ = strconv.ParseBool(val)
}
if cfg.Replica != nil {
if val, ok := checkDeprecatedEnvVar("REPLICA_WEBURL", "REPLICA_WEB_URL"); ok {
cfg.Replica.WebURL = val
}
if val, ok := checkDeprecatedEnvVar("REPLICA_AUTOSETUP", "REPLICA_AUTO_SETUP"); ok {
cfg.Replica.AutoSetup, _ = strconv.ParseBool(val)
}
if val, ok := checkDeprecatedEnvVar("REPLICA_INTERFACENAME", "REPLICA_INTERFACE_NAME"); ok {
cfg.Replica.InterfaceName = val
}
if val, ok := checkDeprecatedEnvVar("REPLICA_DHCPSERVERENABLED", "REPLICA_DHCP_SERVER_ENABLED"); ok {
if b, err := strconv.ParseBool(val); err != nil {
cfg.Replica.DHCPServerEnabled = utils.Ptr(b)
}
}
}
}
func checkDeprecatedEnvVar(oldName string, 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)
if newOK {
return new, true
}
return old, oldOK
}
func checkDeprecatedReplicaEnvVar(oldPattern string, newPattern string, replicaID int) (string, bool) {
return checkDeprecatedEnvVar(fmt.Sprintf(oldPattern, replicaID), fmt.Sprintf(newPattern, replicaID))
}
// Manually collect replicas from env.
func enrichReplicasFromEnv(initialReplicas []types.AdGuardInstance) ([]types.AdGuardInstance, error) {
var replicas []types.AdGuardInstance
for _, v := range os.Environ() {
if envReplicasURLPattern.MatchString(v) {
sm := envReplicasURLPattern.FindStringSubmatch(v)
id, _ := strconv.Atoi(sm[1])
if id <= 0 {
return nil, fmt.Errorf("numbered replica env variables must have a number id >= 1, got %q", v)
}
if id > len(initialReplicas) {
replicas = append(replicas, types.AdGuardInstance{URL: sm[2]})
} else {
re := initialReplicas[id-1]
re.URL = sm[2]
replicas = append(replicas, re)
}
}
}
if len(replicas) == 0 {
replicas = initialReplicas
}
for i := range replicas {
reID := i + 1
// keep previously set value
replicaDhcpServer := replicas[i].DHCPServerEnabled
replicas[i].DHCPServerEnabled = nil
if err := env.ParseWithOptions(&replicas[i], env.Options{Prefix: fmt.Sprintf("REPLICA%d_", reID)}); err != nil {
return nil, err
}
if replicas[i].DHCPServerEnabled == nil {
replicas[i].DHCPServerEnabled = replicaDhcpServer
}
if val, ok := checkDeprecatedReplicaEnvVar("REPLICA%d_APIPATH", "REPLICA%d_API_PATH", reID); ok {
replicas[i].APIPath = val
}
if val, ok := checkDeprecatedReplicaEnvVar("REPLICA%d_INSECURESKIPVERIFY", "REPLICA%d_INSECURE_SKIP_VERIFY", reID); ok {
replicas[i].InsecureSkipVerify = strings.EqualFold(val, "true")
}
if val, ok := checkDeprecatedReplicaEnvVar("REPLICA%d_AUTOSETUP", "REPLICA%d_AUTO_SETUP", reID); ok {
replicas[i].AutoSetup = strings.EqualFold(val, "true")
}
if val, ok := checkDeprecatedReplicaEnvVar("REPLICA%d_INTERFACENAME", "REPLICA%d_INTERFACE_NAME", reID); ok {
replicas[i].InterfaceName = val
}
if dhcpEnabled, ok := checkDeprecatedReplicaEnvVar("REPLICA%d_DHCPSERVERENABLED", "REPLICA%d_DHCP_SERVER_ENABLED", reID); ok {
if strings.EqualFold(dhcpEnabled, "true") {
replicas[i].DHCPServerEnabled = utils.Ptr(true)
} else if strings.EqualFold(dhcpEnabled, "false") {
replicas[i].DHCPServerEnabled = utils.Ptr(false)
}
}
if replicas[i].APIPath == "" {
replicas[i].APIPath = "/control"
}
}
return replicas, nil
}

25
pkg/config/env_test.go Normal file
View File

@@ -0,0 +1,25 @@
package config
import (
"os"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("Config", func() {
Context("env", func() {
Context("enrichReplicasFromEnv", func() {
It("should have the origin URL from the config env var", func() {
_ = os.Setenv("REPLICA0_URL", "https://origin-env:443")
defer func() {
_ = os.Unsetenv("REPLICA0_URL")
}()
_, err := enrichReplicasFromEnv(nil)
Ω(err).Should(HaveOccurred())
Ω(err.Error()).Should(ContainSubstring("numbered replica env variables must have a number id >= 1"))
})
})
})
})

34
pkg/config/file.go Normal file
View File

@@ -0,0 +1,34 @@
package config
import (
"os"
"path/filepath"
"github.com/bakito/adguardhome-sync/pkg/types"
"gopkg.in/yaml.v3"
)
func readFile(cfg *types.Config, path string) error {
if _, err := os.Stat(path); err == nil {
b, err := os.ReadFile(path)
if err != nil {
return err
}
if err := yaml.Unmarshal(b, cfg); err != nil {
return err
}
}
return nil
}
func configFilePath(configFile string) (string, error) {
if configFile == "" {
// Find home directory.
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
return filepath.Join(home, ".adguardhome-sync"), nil
}
return configFile, nil
}

32
pkg/config/file_test.go Normal file
View File

@@ -0,0 +1,32 @@
package config
import (
"os"
"path/filepath"
"github.com/google/uuid"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("Config", func() {
var ()
BeforeEach(func() {
})
Context("configFilePath", func() {
It("should return the same value", func() {
path := uuid.NewString()
result, err := configFilePath(path)
Ω(err).ShouldNot(HaveOccurred())
Ω(result).Should(Equal(path))
})
It("should the file in HOME dir", func() {
home := os.Getenv("HOME")
result, err := configFilePath("")
Ω(err).ShouldNot(HaveOccurred())
Ω(result).Should(Equal(filepath.Join(home, "/.adguardhome-sync")))
})
})
})

44
pkg/config/flag-names.go Normal file
View File

@@ -0,0 +1,44 @@
package config
const (
FlagCron = "cron"
FlagRunOnStart = "runOnStart"
FlagPrintConfigOnly = "printConfigOnly"
FlagContinueOnError = "continueOnError"
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"
FlagFeatureGeneral = "feature-general-settings"
FlagFeatureQueryLog = "feature-query-log-config"
FlagFeatureStats = "feature-stats-config"
FlagFeatureClient = "feature-client-settings"
FlagFeatureServices = "feature-services"
FlagFeatureFilters = "feature-filters"
FlagOriginURL = "origin-url"
FlagOriginWebURL = "origin-web-url"
FlagOriginApiPath = "origin-api-path"
FlagOriginUsername = "origin-username"
FlagOriginPassword = "origin-password"
FlagOriginCookie = "origin-cookie"
FlagOriginISV = "origin-insecure-skip-verify"
FlagReplicaURL = "replica-url"
FlagReplicaWebURL = "replica-web-url"
FlagReplicaApiPath = "replica-api-path"
FlagReplicaUsername = "replica-username"
FlagReplicaPassword = "replica-password"
FlagReplicaCookie = "replica-cookie"
FlagReplicaISV = "replica-insecure-skip-verify"
FlagReplicaAutoSetup = "replica-auto-setup"
FlagReplicaInterfaceName = "replica-interface-name"
)

283
pkg/config/flags.go Normal file
View File

@@ -0,0 +1,283 @@
package config
import (
"github.com/bakito/adguardhome-sync/pkg/types"
)
func readFlags(cfg *types.Config, flags Flags) error {
if flags == nil {
return nil
}
fr := &flagReader{
cfg: cfg,
flags: flags,
}
if err := fr.readRootFlags(); err != nil {
return err
}
if err := fr.readApiFlags(); err != nil {
return err
}
if err := fr.readFeatureFlags(); err != nil {
return err
}
if err := fr.readOriginFlags(); err != nil {
return err
}
if err := fr.readReplicaFlags(); err != nil {
return err
}
return nil
}
type flagReader struct {
cfg *types.Config
flags Flags
}
func (fr *flagReader) readReplicaFlags() error {
if err := fr.setStringFlag(FlagReplicaURL, func(cgf *types.Config, value string) {
fr.cfg.Replica.URL = value
}); err != nil {
return err
}
if err := fr.setStringFlag(FlagReplicaWebURL, func(cgf *types.Config, value string) {
fr.cfg.Replica.WebURL = value
}); err != nil {
return err
}
if err := fr.setStringFlag(FlagReplicaApiPath, func(cgf *types.Config, value string) {
fr.cfg.Replica.APIPath = value
}); err != nil {
return err
}
if err := fr.setStringFlag(FlagReplicaUsername, func(cgf *types.Config, value string) {
fr.cfg.Replica.Username = value
}); err != nil {
return err
}
if err := fr.setStringFlag(FlagReplicaPassword, func(cgf *types.Config, value string) {
fr.cfg.Replica.Password = value
}); err != nil {
return err
}
if err := fr.setStringFlag(FlagReplicaCookie, func(cgf *types.Config, value string) {
fr.cfg.Replica.Cookie = value
}); err != nil {
return err
}
if err := fr.setBoolFlag(FlagReplicaISV, func(cgf *types.Config, value bool) {
fr.cfg.Replica.InsecureSkipVerify = value
}); err != nil {
return err
}
if err := fr.setBoolFlag(FlagReplicaAutoSetup, func(cgf *types.Config, value bool) {
fr.cfg.Replica.AutoSetup = value
}); err != nil {
return err
}
if err := 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 {
if err := fr.setStringFlag(FlagOriginURL, func(cgf *types.Config, value string) {
fr.cfg.Origin.URL = value
}); err != nil {
return err
}
if err := fr.setStringFlag(FlagOriginWebURL, func(cgf *types.Config, value string) {
fr.cfg.Origin.WebURL = value
}); err != nil {
return err
}
if err := fr.setStringFlag(FlagOriginApiPath, func(cgf *types.Config, value string) {
fr.cfg.Origin.APIPath = value
}); err != nil {
return err
}
if err := fr.setStringFlag(FlagOriginUsername, func(cgf *types.Config, value string) {
fr.cfg.Origin.Username = value
}); err != nil {
return err
}
if err := fr.setStringFlag(FlagOriginPassword, func(cgf *types.Config, value string) {
fr.cfg.Origin.Password = value
}); err != nil {
return err
}
if err := fr.setStringFlag(FlagOriginCookie, func(cgf *types.Config, value string) {
fr.cfg.Origin.Cookie = value
}); err != nil {
return err
}
if err := 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 {
if err := fr.setBoolFlag(FlagFeatureDhcpServerConfig, func(cgf *types.Config, value bool) {
fr.cfg.Features.DHCP.ServerConfig = value
}); err != nil {
return err
}
if err := fr.setBoolFlag(FlagFeatureDhcpStaticLeases, func(cgf *types.Config, value bool) {
fr.cfg.Features.DHCP.StaticLeases = value
}); err != nil {
return err
}
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) {
fr.cfg.Features.DNS.AccessLists = value
}); err != nil {
return err
}
if err := fr.setBoolFlag(FlagFeatureDnsRewrites, func(cgf *types.Config, value bool) {
fr.cfg.Features.DNS.Rewrites = value
}); err != nil {
return err
}
if err := fr.setBoolFlag(FlagFeatureGeneral, func(cgf *types.Config, value bool) {
fr.cfg.Features.GeneralSettings = value
}); err != nil {
return err
}
if err := fr.setBoolFlag(FlagFeatureQueryLog, func(cgf *types.Config, value bool) {
fr.cfg.Features.QueryLogConfig = value
}); err != nil {
return err
}
if err := fr.setBoolFlag(FlagFeatureStats, func(cgf *types.Config, value bool) {
fr.cfg.Features.StatsConfig = value
}); err != nil {
return err
}
if err := fr.setBoolFlag(FlagFeatureClient, func(cgf *types.Config, value bool) {
fr.cfg.Features.ClientSettings = value
}); err != nil {
return err
}
if err := fr.setBoolFlag(FlagFeatureServices, func(cgf *types.Config, value bool) {
fr.cfg.Features.Services = value
}); err != nil {
return err
}
if err := fr.setBoolFlag(FlagFeatureFilters, func(cgf *types.Config, value bool) {
fr.cfg.Features.Filters = 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) {
fr.cfg.API.Username = value
}); err != nil {
return
}
if err = fr.setStringFlag(FlagApiPassword, func(cgf *types.Config, value string) {
fr.cfg.API.Password = value
}); err != nil {
return
}
if err = 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) {
fr.cfg.Cron = value
}); err != nil {
return
}
if err = fr.setBoolFlag(FlagRunOnStart, func(cgf *types.Config, value bool) {
fr.cfg.RunOnStart = value
}); err != nil {
return
}
if err = fr.setBoolFlag(FlagPrintConfigOnly, func(cgf *types.Config, value bool) {
fr.cfg.PrintConfigOnly = value
}); err != nil {
return
}
if err = fr.setBoolFlag(FlagContinueOnError, func(cgf *types.Config, value bool) {
fr.cfg.ContinueOnError = value
}); err != nil {
return
}
return
}
type Flags interface {
Changed(name string) bool
GetString(name string) (string, error)
GetInt(name string) (int, error)
GetBool(name string) (bool, error)
}
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 {
return err
} else {
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 {
return err
} else {
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 {
return err
} else {
cb(fr.cfg, value)
}
}
return nil
}
type callback[T any] func(cgf *types.Config, value T)

235
pkg/config/flags_test.go Normal file
View File

@@ -0,0 +1,235 @@
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"
)
var _ = Describe("Config", func() {
var (
cfg *types.Config
flags *flagsmock.MockFlags
mockCtrl *gm.Controller
)
BeforeEach(func() {
cfg = &types.Config{
Replica: &types.AdGuardInstance{},
Features: types.Features{
DNS: types.DNS{
AccessLists: true,
ServerConfig: true,
Rewrites: true,
},
DHCP: types.DHCP{
ServerConfig: true,
StaticLeases: true,
},
GeneralSettings: true,
QueryLogConfig: true,
StatsConfig: true,
ClientSettings: true,
Services: true,
Filters: true,
},
}
mockCtrl = gm.NewController(GinkgoT())
flags = flagsmock.NewMockFlags(mockCtrl)
})
AfterEach(func() {
defer mockCtrl.Finish()
})
Context("readFlags", func() {
It("should not change the config with nil flags", func() {
clone := cfg.DeepCopy()
err := readFlags(cfg, nil)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg).Should(Equal(clone))
})
It("should not change the config with no changed flags", func() {
clone := cfg.DeepCopy()
flags.EXPECT().Changed(gm.Any()).Return(false).AnyTimes()
err := readFlags(cfg, flags)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg).Should(Equal(clone))
})
})
Context("readFeatureFlags", func() {
It("should disable all flags", func() {
flags.EXPECT().Changed(gm.Any()).DoAndReturn(func(name string) bool {
return strings.HasPrefix(name, "feature")
}).AnyTimes()
flags.EXPECT().GetBool(gm.Any()).Return(false, nil).AnyTimes()
err := readFlags(cfg, flags)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Features).Should(Equal(types.Features{
DNS: types.DNS{
AccessLists: false,
ServerConfig: false,
Rewrites: false,
},
DHCP: types.DHCP{
ServerConfig: false,
StaticLeases: false,
},
GeneralSettings: false,
QueryLogConfig: false,
StatsConfig: false,
ClientSettings: false,
Services: false,
Filters: false,
}))
})
})
Context("readApiFlags", func() {
It("should change all values", func() {
cfg.API = types.API{
Port: 1111,
Username: "2222",
Password: "3333",
DarkMode: false,
}
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)
err := readFlags(cfg, flags)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.API).Should(Equal(types.API{
Port: 9999,
Username: "aaaa",
Password: "bbbb",
DarkMode: true,
}))
})
})
Context("readRootFlags", func() {
It("should change all values", func() {
cfg.Cron = "*/10 * * * *"
cfg.PrintConfigOnly = false
cfg.ContinueOnError = false
cfg.RunOnStart = false
flags.EXPECT().Changed(FlagCron).Return(true)
flags.EXPECT().Changed(FlagRunOnStart).Return(true)
flags.EXPECT().Changed(FlagPrintConfigOnly).Return(true)
flags.EXPECT().Changed(FlagContinueOnError).Return(true)
flags.EXPECT().Changed(gm.Any()).Return(false).AnyTimes()
flags.EXPECT().GetString(FlagCron).Return("*/30 * * * *", nil)
flags.EXPECT().GetBool(FlagRunOnStart).Return(true, nil)
flags.EXPECT().GetBool(FlagPrintConfigOnly).Return(true, nil)
flags.EXPECT().GetBool(FlagContinueOnError).Return(true, nil)
err := readFlags(cfg, flags)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Cron).Should(Equal("*/30 * * * *"))
Ω(cfg.RunOnStart).Should(BeTrue())
Ω(cfg.PrintConfigOnly).Should(BeTrue())
Ω(cfg.ContinueOnError).Should(BeTrue())
})
})
Context("readOriginFlags", func() {
It("should change all values", func() {
cfg.Origin = types.AdGuardInstance{
URL: "1",
WebURL: "2",
APIPath: "3",
Username: "4",
Password: "5",
Cookie: "6",
InsecureSkipVerify: false,
}
flags.EXPECT().Changed(FlagOriginURL).Return(true)
flags.EXPECT().Changed(FlagOriginWebURL).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)
flags.EXPECT().Changed(FlagOriginISV).Return(true)
flags.EXPECT().Changed(gm.Any()).Return(false).AnyTimes()
flags.EXPECT().GetString(FlagOriginURL).Return("a", nil)
flags.EXPECT().GetString(FlagOriginWebURL).Return("b", 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)
flags.EXPECT().GetBool(FlagOriginISV).Return(true, nil)
err := readFlags(cfg, flags)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Origin).Should(Equal(types.AdGuardInstance{
URL: "a",
WebURL: "b",
APIPath: "c",
Username: "d",
Password: "e",
Cookie: "f",
InsecureSkipVerify: true,
}))
})
})
Context("readReplicaFlags", func() {
It("should change all values", func() {
cfg.Replica = &types.AdGuardInstance{
URL: "1",
WebURL: "2",
APIPath: "3",
Username: "4",
Password: "5",
Cookie: "6",
InsecureSkipVerify: false,
AutoSetup: false,
InterfaceName: "7",
}
flags.EXPECT().Changed(FlagReplicaURL).Return(true)
flags.EXPECT().Changed(FlagReplicaWebURL).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)
flags.EXPECT().Changed(FlagReplicaISV).Return(true)
flags.EXPECT().Changed(FlagReplicaAutoSetup).Return(true)
flags.EXPECT().Changed(FlagReplicaInterfaceName).Return(true)
flags.EXPECT().Changed(gm.Any()).Return(false).AnyTimes()
flags.EXPECT().GetString(FlagReplicaURL).Return("a", nil)
flags.EXPECT().GetString(FlagReplicaWebURL).Return("b", 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)
flags.EXPECT().GetBool(FlagReplicaISV).Return(true, nil)
flags.EXPECT().GetBool(FlagReplicaAutoSetup).Return(true, nil)
flags.EXPECT().GetString(FlagReplicaInterfaceName).Return("g", nil)
err := readFlags(cfg, flags)
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Replica).Should(Equal(&types.AdGuardInstance{
URL: "a",
WebURL: "b",
APIPath: "c",
Username: "d",
Password: "e",
Cookie: "f",
InsecureSkipVerify: true,
AutoSetup: true,
InterfaceName: "g",
}))
})
})
})

View File

@@ -1,5 +1,10 @@
// Code generated by MockGen. DO NOT EDIT. // Code generated by MockGen. DO NOT EDIT.
// Source: github.com/bakito/adguardhome-sync/pkg/client (interfaces: Client) // Source: github.com/bakito/adguardhome-sync/pkg/client (interfaces: Client)
//
// Generated by this command:
//
// mockgen -package client -destination pkg/mocks/client/mock.go github.com/bakito/adguardhome-sync/pkg/client Client
//
// Package client is a generated GoMock package. // Package client is a generated GoMock package.
package client package client
@@ -8,7 +13,7 @@ import (
reflect "reflect" reflect "reflect"
model "github.com/bakito/adguardhome-sync/pkg/client/model" model "github.com/bakito/adguardhome-sync/pkg/client/model"
gomock "github.com/golang/mock/gomock" gomock "go.uber.org/mock/gomock"
) )
// MockClient is a mock of Client interface. // MockClient is a mock of Client interface.
@@ -49,65 +54,52 @@ func (mr *MockClientMockRecorder) AccessList() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AccessList", reflect.TypeOf((*MockClient)(nil).AccessList)) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AccessList", reflect.TypeOf((*MockClient)(nil).AccessList))
} }
// AddClients mocks base method. // AddClient mocks base method.
func (m *MockClient) AddClients(arg0 ...*model.Client) error { func (m *MockClient) AddClient(arg0 *model.Client) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
varargs := []interface{}{} ret := m.ctrl.Call(m, "AddClient", arg0)
for _, a := range arg0 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "AddClients", varargs...)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
return ret0 return ret0
} }
// AddClients indicates an expected call of AddClients. // AddClient indicates an expected call of AddClient.
func (mr *MockClientMockRecorder) AddClients(arg0 ...interface{}) *gomock.Call { func (mr *MockClientMockRecorder) AddClient(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddClients", reflect.TypeOf((*MockClient)(nil).AddClients), arg0...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddClient", reflect.TypeOf((*MockClient)(nil).AddClient), arg0)
} }
// AddDHCPStaticLeases mocks base method. // AddDHCPStaticLease mocks base method.
func (m *MockClient) AddDHCPStaticLeases(arg0 ...model.DhcpStaticLease) error { func (m *MockClient) AddDHCPStaticLease(arg0 model.DhcpStaticLease) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
varargs := []interface{}{} ret := m.ctrl.Call(m, "AddDHCPStaticLease", arg0)
for _, a := range arg0 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "AddDHCPStaticLeases", varargs...)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
return ret0 return ret0
} }
// AddDHCPStaticLeases indicates an expected call of AddDHCPStaticLeases. // AddDHCPStaticLease indicates an expected call of AddDHCPStaticLease.
func (mr *MockClientMockRecorder) AddDHCPStaticLeases(arg0 ...interface{}) *gomock.Call { func (mr *MockClientMockRecorder) AddDHCPStaticLease(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddDHCPStaticLeases", reflect.TypeOf((*MockClient)(nil).AddDHCPStaticLeases), arg0...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddDHCPStaticLease", reflect.TypeOf((*MockClient)(nil).AddDHCPStaticLease), arg0)
} }
// AddFilters mocks base method. // AddFilter mocks base method.
func (m *MockClient) AddFilters(arg0 bool, arg1 ...model.Filter) error { func (m *MockClient) AddFilter(arg0 bool, arg1 model.Filter) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
varargs := []interface{}{arg0} ret := m.ctrl.Call(m, "AddFilter", arg0, arg1)
for _, a := range arg1 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "AddFilters", varargs...)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
return ret0 return ret0
} }
// AddFilters indicates an expected call of AddFilters. // AddFilter indicates an expected call of AddFilter.
func (mr *MockClientMockRecorder) AddFilters(arg0 interface{}, arg1 ...interface{}) *gomock.Call { func (mr *MockClientMockRecorder) AddFilter(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0}, arg1...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddFilter", reflect.TypeOf((*MockClient)(nil).AddFilter), arg0, arg1)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddFilters", reflect.TypeOf((*MockClient)(nil).AddFilters), varargs...)
} }
// AddRewriteEntries mocks base method. // AddRewriteEntries mocks base method.
func (m *MockClient) AddRewriteEntries(arg0 ...model.RewriteEntry) error { func (m *MockClient) AddRewriteEntries(arg0 ...model.RewriteEntry) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
varargs := []interface{}{} varargs := []any{}
for _, a := range arg0 { for _, a := range arg0 {
varargs = append(varargs, a) varargs = append(varargs, a)
} }
@@ -117,7 +109,7 @@ func (m *MockClient) AddRewriteEntries(arg0 ...model.RewriteEntry) error {
} }
// AddRewriteEntries indicates an expected call of AddRewriteEntries. // AddRewriteEntries indicates an expected call of AddRewriteEntries.
func (mr *MockClientMockRecorder) AddRewriteEntries(arg0 ...interface{}) *gomock.Call { func (mr *MockClientMockRecorder) AddRewriteEntries(arg0 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddRewriteEntries", reflect.TypeOf((*MockClient)(nil).AddRewriteEntries), arg0...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddRewriteEntries", reflect.TypeOf((*MockClient)(nil).AddRewriteEntries), arg0...)
} }
@@ -182,65 +174,52 @@ func (mr *MockClientMockRecorder) DNSConfig() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DNSConfig", reflect.TypeOf((*MockClient)(nil).DNSConfig)) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DNSConfig", reflect.TypeOf((*MockClient)(nil).DNSConfig))
} }
// DeleteClients mocks base method. // DeleteClient mocks base method.
func (m *MockClient) DeleteClients(arg0 ...*model.Client) error { func (m *MockClient) DeleteClient(arg0 *model.Client) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
varargs := []interface{}{} ret := m.ctrl.Call(m, "DeleteClient", arg0)
for _, a := range arg0 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "DeleteClients", varargs...)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
return ret0 return ret0
} }
// DeleteClients indicates an expected call of DeleteClients. // DeleteClient indicates an expected call of DeleteClient.
func (mr *MockClientMockRecorder) DeleteClients(arg0 ...interface{}) *gomock.Call { func (mr *MockClientMockRecorder) DeleteClient(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteClients", reflect.TypeOf((*MockClient)(nil).DeleteClients), arg0...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteClient", reflect.TypeOf((*MockClient)(nil).DeleteClient), arg0)
} }
// DeleteDHCPStaticLeases mocks base method. // DeleteDHCPStaticLease mocks base method.
func (m *MockClient) DeleteDHCPStaticLeases(arg0 ...model.DhcpStaticLease) error { func (m *MockClient) DeleteDHCPStaticLease(arg0 model.DhcpStaticLease) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
varargs := []interface{}{} ret := m.ctrl.Call(m, "DeleteDHCPStaticLease", arg0)
for _, a := range arg0 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "DeleteDHCPStaticLeases", varargs...)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
return ret0 return ret0
} }
// DeleteDHCPStaticLeases indicates an expected call of DeleteDHCPStaticLeases. // DeleteDHCPStaticLease indicates an expected call of DeleteDHCPStaticLease.
func (mr *MockClientMockRecorder) DeleteDHCPStaticLeases(arg0 ...interface{}) *gomock.Call { func (mr *MockClientMockRecorder) DeleteDHCPStaticLease(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteDHCPStaticLeases", reflect.TypeOf((*MockClient)(nil).DeleteDHCPStaticLeases), arg0...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteDHCPStaticLease", reflect.TypeOf((*MockClient)(nil).DeleteDHCPStaticLease), arg0)
} }
// DeleteFilters mocks base method. // DeleteFilter mocks base method.
func (m *MockClient) DeleteFilters(arg0 bool, arg1 ...model.Filter) error { func (m *MockClient) DeleteFilter(arg0 bool, arg1 model.Filter) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
varargs := []interface{}{arg0} ret := m.ctrl.Call(m, "DeleteFilter", arg0, arg1)
for _, a := range arg1 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "DeleteFilters", varargs...)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
return ret0 return ret0
} }
// DeleteFilters indicates an expected call of DeleteFilters. // DeleteFilter indicates an expected call of DeleteFilter.
func (mr *MockClientMockRecorder) DeleteFilters(arg0 interface{}, arg1 ...interface{}) *gomock.Call { func (mr *MockClientMockRecorder) DeleteFilter(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0}, arg1...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteFilter", reflect.TypeOf((*MockClient)(nil).DeleteFilter), arg0, arg1)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteFilters", reflect.TypeOf((*MockClient)(nil).DeleteFilters), varargs...)
} }
// DeleteRewriteEntries mocks base method. // DeleteRewriteEntries mocks base method.
func (m *MockClient) DeleteRewriteEntries(arg0 ...model.RewriteEntry) error { func (m *MockClient) DeleteRewriteEntries(arg0 ...model.RewriteEntry) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
varargs := []interface{}{} varargs := []any{}
for _, a := range arg0 { for _, a := range arg0 {
varargs = append(varargs, a) varargs = append(varargs, a)
} }
@@ -250,7 +229,7 @@ func (m *MockClient) DeleteRewriteEntries(arg0 ...model.RewriteEntry) error {
} }
// DeleteRewriteEntries indicates an expected call of DeleteRewriteEntries. // DeleteRewriteEntries indicates an expected call of DeleteRewriteEntries.
func (mr *MockClientMockRecorder) DeleteRewriteEntries(arg0 ...interface{}) *gomock.Call { func (mr *MockClientMockRecorder) DeleteRewriteEntries(arg0 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRewriteEntries", reflect.TypeOf((*MockClient)(nil).DeleteRewriteEntries), arg0...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRewriteEntries", reflect.TypeOf((*MockClient)(nil).DeleteRewriteEntries), arg0...)
} }
@@ -353,7 +332,7 @@ func (m *MockClient) RefreshFilters(arg0 bool) error {
} }
// RefreshFilters indicates an expected call of RefreshFilters. // RefreshFilters indicates an expected call of RefreshFilters.
func (mr *MockClientMockRecorder) RefreshFilters(arg0 interface{}) *gomock.Call { func (mr *MockClientMockRecorder) RefreshFilters(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RefreshFilters", reflect.TypeOf((*MockClient)(nil).RefreshFilters), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RefreshFilters", reflect.TypeOf((*MockClient)(nil).RefreshFilters), arg0)
} }
@@ -412,7 +391,7 @@ func (m *MockClient) SetAccessList(arg0 *model.AccessList) error {
} }
// SetAccessList indicates an expected call of SetAccessList. // SetAccessList indicates an expected call of SetAccessList.
func (mr *MockClientMockRecorder) SetAccessList(arg0 interface{}) *gomock.Call { func (mr *MockClientMockRecorder) SetAccessList(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper() 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), arg0)
} }
@@ -426,7 +405,7 @@ func (m *MockClient) SetBlockedServices(arg0 *[]string) error {
} }
// SetBlockedServices indicates an expected call of SetBlockedServices. // SetBlockedServices indicates an expected call of SetBlockedServices.
func (mr *MockClientMockRecorder) SetBlockedServices(arg0 interface{}) *gomock.Call { func (mr *MockClientMockRecorder) SetBlockedServices(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetBlockedServices", reflect.TypeOf((*MockClient)(nil).SetBlockedServices), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetBlockedServices", reflect.TypeOf((*MockClient)(nil).SetBlockedServices), arg0)
} }
@@ -440,7 +419,7 @@ func (m *MockClient) SetBlockedServicesSchedule(arg0 *model.BlockedServicesSched
} }
// SetBlockedServicesSchedule indicates an expected call of SetBlockedServicesSchedule. // SetBlockedServicesSchedule indicates an expected call of SetBlockedServicesSchedule.
func (mr *MockClientMockRecorder) SetBlockedServicesSchedule(arg0 interface{}) *gomock.Call { func (mr *MockClientMockRecorder) SetBlockedServicesSchedule(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetBlockedServicesSchedule", reflect.TypeOf((*MockClient)(nil).SetBlockedServicesSchedule), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetBlockedServicesSchedule", reflect.TypeOf((*MockClient)(nil).SetBlockedServicesSchedule), arg0)
} }
@@ -454,7 +433,7 @@ func (m *MockClient) SetCustomRules(arg0 *[]string) error {
} }
// SetCustomRules indicates an expected call of SetCustomRules. // SetCustomRules indicates an expected call of SetCustomRules.
func (mr *MockClientMockRecorder) SetCustomRules(arg0 interface{}) *gomock.Call { func (mr *MockClientMockRecorder) SetCustomRules(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetCustomRules", reflect.TypeOf((*MockClient)(nil).SetCustomRules), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetCustomRules", reflect.TypeOf((*MockClient)(nil).SetCustomRules), arg0)
} }
@@ -468,7 +447,7 @@ func (m *MockClient) SetDNSConfig(arg0 *model.DNSConfig) error {
} }
// SetDNSConfig indicates an expected call of SetDNSConfig. // SetDNSConfig indicates an expected call of SetDNSConfig.
func (mr *MockClientMockRecorder) SetDNSConfig(arg0 interface{}) *gomock.Call { func (mr *MockClientMockRecorder) SetDNSConfig(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper() 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), arg0)
} }
@@ -482,7 +461,7 @@ func (m *MockClient) SetDhcpConfig(arg0 *model.DhcpStatus) error {
} }
// SetDhcpConfig indicates an expected call of SetDhcpConfig. // SetDhcpConfig indicates an expected call of SetDhcpConfig.
func (mr *MockClientMockRecorder) SetDhcpConfig(arg0 interface{}) *gomock.Call { func (mr *MockClientMockRecorder) SetDhcpConfig(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper() 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), arg0)
} }
@@ -496,7 +475,7 @@ func (m *MockClient) SetProfileInfo(arg0 *model.ProfileInfo) error {
} }
// SetProfileInfo indicates an expected call of SetProfileInfo. // SetProfileInfo indicates an expected call of SetProfileInfo.
func (mr *MockClientMockRecorder) SetProfileInfo(arg0 interface{}) *gomock.Call { func (mr *MockClientMockRecorder) SetProfileInfo(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetProfileInfo", reflect.TypeOf((*MockClient)(nil).SetProfileInfo), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetProfileInfo", reflect.TypeOf((*MockClient)(nil).SetProfileInfo), arg0)
} }
@@ -510,7 +489,7 @@ func (m *MockClient) SetQueryLogConfig(arg0 *model.QueryLogConfig) error {
} }
// SetQueryLogConfig indicates an expected call of SetQueryLogConfig. // SetQueryLogConfig indicates an expected call of SetQueryLogConfig.
func (mr *MockClientMockRecorder) SetQueryLogConfig(arg0 interface{}) *gomock.Call { func (mr *MockClientMockRecorder) SetQueryLogConfig(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper() 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), arg0)
} }
@@ -524,7 +503,7 @@ func (m *MockClient) SetSafeSearchConfig(arg0 *model.SafeSearchConfig) error {
} }
// SetSafeSearchConfig indicates an expected call of SetSafeSearchConfig. // SetSafeSearchConfig indicates an expected call of SetSafeSearchConfig.
func (mr *MockClientMockRecorder) SetSafeSearchConfig(arg0 interface{}) *gomock.Call { func (mr *MockClientMockRecorder) SetSafeSearchConfig(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSafeSearchConfig", reflect.TypeOf((*MockClient)(nil).SetSafeSearchConfig), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetSafeSearchConfig", reflect.TypeOf((*MockClient)(nil).SetSafeSearchConfig), arg0)
} }
@@ -538,7 +517,7 @@ func (m *MockClient) SetStatsConfig(arg0 *model.StatsConfig) error {
} }
// SetStatsConfig indicates an expected call of SetStatsConfig. // SetStatsConfig indicates an expected call of SetStatsConfig.
func (mr *MockClientMockRecorder) SetStatsConfig(arg0 interface{}) *gomock.Call { func (mr *MockClientMockRecorder) SetStatsConfig(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetStatsConfig", reflect.TypeOf((*MockClient)(nil).SetStatsConfig), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetStatsConfig", reflect.TypeOf((*MockClient)(nil).SetStatsConfig), arg0)
} }
@@ -596,7 +575,7 @@ func (m *MockClient) ToggleFiltering(arg0 bool, arg1 int) error {
} }
// ToggleFiltering indicates an expected call of ToggleFiltering. // ToggleFiltering indicates an expected call of ToggleFiltering.
func (mr *MockClientMockRecorder) ToggleFiltering(arg0, arg1 interface{}) *gomock.Call { func (mr *MockClientMockRecorder) ToggleFiltering(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ToggleFiltering", reflect.TypeOf((*MockClient)(nil).ToggleFiltering), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ToggleFiltering", reflect.TypeOf((*MockClient)(nil).ToggleFiltering), arg0, arg1)
} }
@@ -610,7 +589,7 @@ func (m *MockClient) ToggleParental(arg0 bool) error {
} }
// ToggleParental indicates an expected call of ToggleParental. // ToggleParental indicates an expected call of ToggleParental.
func (mr *MockClientMockRecorder) ToggleParental(arg0 interface{}) *gomock.Call { func (mr *MockClientMockRecorder) ToggleParental(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ToggleParental", reflect.TypeOf((*MockClient)(nil).ToggleParental), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ToggleParental", reflect.TypeOf((*MockClient)(nil).ToggleParental), arg0)
} }
@@ -624,7 +603,7 @@ func (m *MockClient) ToggleProtection(arg0 bool) error {
} }
// ToggleProtection indicates an expected call of ToggleProtection. // ToggleProtection indicates an expected call of ToggleProtection.
func (mr *MockClientMockRecorder) ToggleProtection(arg0 interface{}) *gomock.Call { func (mr *MockClientMockRecorder) ToggleProtection(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ToggleProtection", reflect.TypeOf((*MockClient)(nil).ToggleProtection), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ToggleProtection", reflect.TypeOf((*MockClient)(nil).ToggleProtection), arg0)
} }
@@ -638,44 +617,35 @@ func (m *MockClient) ToggleSafeBrowsing(arg0 bool) error {
} }
// ToggleSafeBrowsing indicates an expected call of ToggleSafeBrowsing. // ToggleSafeBrowsing indicates an expected call of ToggleSafeBrowsing.
func (mr *MockClientMockRecorder) ToggleSafeBrowsing(arg0 interface{}) *gomock.Call { func (mr *MockClientMockRecorder) ToggleSafeBrowsing(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ToggleSafeBrowsing", reflect.TypeOf((*MockClient)(nil).ToggleSafeBrowsing), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ToggleSafeBrowsing", reflect.TypeOf((*MockClient)(nil).ToggleSafeBrowsing), arg0)
} }
// UpdateClients mocks base method. // UpdateClient mocks base method.
func (m *MockClient) UpdateClients(arg0 ...*model.Client) error { func (m *MockClient) UpdateClient(arg0 *model.Client) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
varargs := []interface{}{} ret := m.ctrl.Call(m, "UpdateClient", arg0)
for _, a := range arg0 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "UpdateClients", varargs...)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
return ret0 return ret0
} }
// UpdateClients indicates an expected call of UpdateClients. // UpdateClient indicates an expected call of UpdateClient.
func (mr *MockClientMockRecorder) UpdateClients(arg0 ...interface{}) *gomock.Call { func (mr *MockClientMockRecorder) UpdateClient(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateClients", reflect.TypeOf((*MockClient)(nil).UpdateClients), arg0...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateClient", reflect.TypeOf((*MockClient)(nil).UpdateClient), arg0)
} }
// UpdateFilters mocks base method. // UpdateFilter mocks base method.
func (m *MockClient) UpdateFilters(arg0 bool, arg1 ...model.Filter) error { func (m *MockClient) UpdateFilter(arg0 bool, arg1 model.Filter) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
varargs := []interface{}{arg0} ret := m.ctrl.Call(m, "UpdateFilter", arg0, arg1)
for _, a := range arg1 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "UpdateFilters", varargs...)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
return ret0 return ret0
} }
// UpdateFilters indicates an expected call of UpdateFilters. // UpdateFilter indicates an expected call of UpdateFilter.
func (mr *MockClientMockRecorder) UpdateFilters(arg0 interface{}, arg1 ...interface{}) *gomock.Call { func (mr *MockClientMockRecorder) UpdateFilter(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{arg0}, arg1...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateFilter", reflect.TypeOf((*MockClient)(nil).UpdateFilter), arg0, arg1)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateFilters", reflect.TypeOf((*MockClient)(nil).UpdateFilters), varargs...)
} }

98
pkg/mocks/flags/mock.go Normal file
View File

@@ -0,0 +1,98 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/bakito/adguardhome-sync/pkg/config (interfaces: Flags)
//
// Generated by this command:
//
// mockgen -package client -destination pkg/mocks/flags/mock.go github.com/bakito/adguardhome-sync/pkg/config Flags
//
// Package client is a generated GoMock package.
package client
import (
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockFlags is a mock of Flags interface.
type MockFlags struct {
ctrl *gomock.Controller
recorder *MockFlagsMockRecorder
}
// MockFlagsMockRecorder is the mock recorder for MockFlags.
type MockFlagsMockRecorder struct {
mock *MockFlags
}
// NewMockFlags creates a new mock instance.
func NewMockFlags(ctrl *gomock.Controller) *MockFlags {
mock := &MockFlags{ctrl: ctrl}
mock.recorder = &MockFlagsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockFlags) EXPECT() *MockFlagsMockRecorder {
return m.recorder
}
// Changed mocks base method.
func (m *MockFlags) Changed(arg0 string) bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Changed", arg0)
ret0, _ := ret[0].(bool)
return ret0
}
// Changed indicates an expected call of Changed.
func (mr *MockFlagsMockRecorder) Changed(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Changed", reflect.TypeOf((*MockFlags)(nil).Changed), arg0)
}
// GetBool mocks base method.
func (m *MockFlags) GetBool(arg0 string) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetBool", arg0)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetBool indicates an expected call of GetBool.
func (mr *MockFlagsMockRecorder) GetBool(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBool", reflect.TypeOf((*MockFlags)(nil).GetBool), arg0)
}
// GetInt mocks base method.
func (m *MockFlags) GetInt(arg0 string) (int, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetInt", arg0)
ret0, _ := ret[0].(int)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetInt indicates an expected call of GetInt.
func (mr *MockFlagsMockRecorder) GetInt(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInt", reflect.TypeOf((*MockFlags)(nil).GetInt), arg0)
}
// GetString mocks base method.
func (m *MockFlags) GetString(arg0 string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetString", arg0)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetString indicates an expected call of GetString.
func (mr *MockFlagsMockRecorder) GetString(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetString", reflect.TypeOf((*MockFlags)(nil).GetString), arg0)
}

283
pkg/sync/action-general.go Normal file
View File

@@ -0,0 +1,283 @@
package sync
import (
"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 (
actionProfileInfo = func(ac *actionContext) error {
if pro, err := ac.client.ProfileInfo(); err != nil {
return err
} else if merged := pro.ShouldSyncFor(ac.origin.profileInfo); merged != nil {
return ac.client.SetProfileInfo(merged)
}
return nil
}
actionProtection = func(ac *actionContext) error {
if ac.origin.status.ProtectionEnabled != ac.replicaStatus.ProtectionEnabled {
return ac.client.ToggleProtection(ac.origin.status.ProtectionEnabled)
}
return nil
}
actionParental = func(ac *actionContext) error {
if rp, err := ac.client.Parental(); err != nil {
return err
} else if ac.origin.parental != rp {
return ac.client.ToggleParental(ac.origin.parental)
}
return nil
}
actionSafeSearchConfig = func(ac *actionContext) error {
if ssc, err := ac.client.SafeSearchConfig(); err != nil {
return err
} else if !ac.origin.safeSearch.Equals(ssc) {
return ac.client.SetSafeSearchConfig(ac.origin.safeSearch)
}
return nil
}
actionSafeBrowsing = func(ac *actionContext) error {
if rs, err := ac.client.SafeBrowsing(); err != nil {
return err
} else if ac.origin.safeBrowsing != rs {
if err = ac.client.ToggleSafeBrowsing(ac.origin.safeBrowsing); err != nil {
return err
}
}
return nil
}
actionQueryLogConfig = func(ac *actionContext) error {
qlc, err := ac.client.QueryLogConfig()
if err != nil {
return err
}
if !ac.origin.queryLogConfig.Equals(qlc) {
return ac.client.SetQueryLogConfig(ac.origin.queryLogConfig)
}
return nil
}
actionStatsConfig = func(ac *actionContext) error {
sc, err := ac.client.StatsConfig()
if err != nil {
return err
}
if ac.origin.statsConfig.Interval != sc.Interval {
return ac.client.SetStatsConfig(ac.origin.statsConfig)
}
return nil
}
actionDNSRewrites = func(ac *actionContext) error {
replicaRewrites, err := ac.client.RewriteList()
if err != nil {
return err
}
a, r, d := replicaRewrites.Merge(ac.origin.rewrites)
if err = ac.client.DeleteRewriteEntries(r...); err != nil {
return err
}
if err = ac.client.AddRewriteEntries(a...); err != nil {
return err
}
for _, dupl := range d {
ac.rl.With("domain", dupl.Domain, "answer", dupl.Answer).Warn("Skipping duplicated rewrite from source")
}
return nil
}
actionFilters = func(ac *actionContext) error {
rf, err := ac.client.Filtering()
if err != nil {
return err
}
if err = syncFilterType(ac.rl, ac.origin.filters.Filters, rf.Filters, false, ac.client, ac.continueOnError); err != nil {
return err
}
if err = syncFilterType(ac.rl, ac.origin.filters.WhitelistFilters, rf.WhitelistFilters, true, ac.client, ac.continueOnError); err != nil {
return err
}
if utils.PtrToString(ac.origin.filters.UserRules) != utils.PtrToString(rf.UserRules) {
return ac.client.SetCustomRules(ac.origin.filters.UserRules)
}
if ac.origin.filters.Enabled != rf.Enabled || ac.origin.filters.Interval != rf.Interval {
return ac.client.ToggleFiltering(*ac.origin.filters.Enabled, *ac.origin.filters.Interval)
}
return nil
}
actionBlockedServices = func(ac *actionContext) error {
rs, err := ac.client.BlockedServices()
if err != nil {
return err
}
if !model.EqualsStringSlice(ac.origin.blockedServices, rs, true) {
return ac.client.SetBlockedServices(ac.origin.blockedServices)
}
return nil
}
actionBlockedServicesSchedule = func(ac *actionContext) error {
rbss, err := ac.client.BlockedServicesSchedule()
if err != nil {
return err
}
if !ac.origin.blockedServicesSchedule.Equals(rbss) {
return ac.client.SetBlockedServicesSchedule(ac.origin.blockedServicesSchedule)
}
return nil
}
actionClientSettings = func(ac *actionContext) error {
rc, err := ac.client.Clients()
if err != nil {
return err
}
a, u, r := rc.Merge(ac.origin.clients)
for _, client := range r {
if err := ac.client.DeleteClient(client); err != nil {
ac.rl.With("client-name", client.Name, "error", err).Error("error deleting client setting")
if !ac.continueOnError {
return err
}
}
}
for _, client := range a {
if err := ac.client.AddClient(client); err != nil {
ac.rl.With("client-name", client.Name, "error", err).Error("error adding client setting")
if !ac.continueOnError {
return err
}
}
}
for _, client := range u {
if err := ac.client.UpdateClient(client); err != nil {
ac.rl.With("client-name", client.Name, "error", err).Error("error updating client setting")
if !ac.continueOnError {
return err
}
}
}
return nil
}
actionDNSAccessLists = func(ac *actionContext) error {
al, err := ac.client.AccessList()
if err != nil {
return err
}
if !al.Equals(ac.origin.accessList) {
return ac.client.SetAccessList(ac.origin.accessList)
}
return nil
}
actionDNSServerConfig = func(ac *actionContext) error {
dc, err := ac.client.DNSConfig()
if err != nil {
return err
}
if !dc.Equals(ac.origin.dnsConfig) {
if err = ac.client.SetDNSConfig(ac.origin.dnsConfig); err != nil {
return err
}
}
return nil
}
actionDHCPServerConfig = func(ac *actionContext) error {
if ac.origin.dhcpServerConfig.HasConfig() {
sc, err := ac.client.DhcpConfig()
if err != nil {
return err
}
origClone := ac.origin.dhcpServerConfig.Clone()
if ac.replica.InterfaceName != "" {
// overwrite interface name
origClone.InterfaceName = utils.Ptr(ac.replica.InterfaceName)
}
if ac.replica.DHCPServerEnabled != nil {
// overwrite dhcp enabled
origClone.Enabled = ac.replica.DHCPServerEnabled
}
if !sc.CleanAndEquals(origClone) {
return ac.client.SetDhcpConfig(origClone)
}
}
return nil
}
actionDHCPStaticLeases = func(ac *actionContext) error {
sc, err := ac.client.DhcpConfig()
if err != nil {
return err
}
a, r := model.MergeDhcpStaticLeases(sc.StaticLeases, ac.origin.dhcpServerConfig.StaticLeases)
for _, lease := range r {
if err := ac.client.DeleteDHCPStaticLease(lease); err != nil {
ac.rl.With("hostname", lease.Hostname, "error", err).Error("error deleting dhcp static lease")
if !ac.continueOnError {
return err
}
}
}
for _, lease := range a {
if err := ac.client.AddDHCPStaticLease(lease); err != nil {
ac.rl.With("hostname", lease.Hostname, "error", err).Error("error adding dhcp static lease")
if !ac.continueOnError {
return err
}
}
}
return nil
}
)
func syncFilterType(rl *zap.SugaredLogger, of *[]model.Filter, rFilters *[]model.Filter, whitelist bool, replica client.Client, continueOnError bool) error {
fa, fu, fd := model.MergeFilters(rFilters, of)
for _, f := range fd {
if err := replica.DeleteFilter(whitelist, f); err != nil {
rl.With("filter", f.Name, "url", f.Url, "whitelist", whitelist, "error", err).Error("error deleting filter")
if !continueOnError {
return err
}
}
}
for _, f := range fa {
if err := replica.AddFilter(whitelist, f); err != nil {
rl.With("filter", f.Name, "url", f.Url, "whitelist", whitelist, "error", err).Error("error adding filter")
if !continueOnError {
return err
}
}
}
for _, f := range fu {
if err := replica.UpdateFilter(whitelist, f); err != nil {
rl.With("filter", f.Name, "url", f.Url, "whitelist", whitelist, "error", err).Error("error updating filter")
if !continueOnError {
return err
}
}
}
if len(fa) > 0 || len(fu) > 0 {
if err := replica.RefreshFilters(whitelist); err != nil {
return err
}
}
return nil
}

104
pkg/sync/action.go Normal file
View File

@@ -0,0 +1,104 @@
package sync
import (
"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) {
if cfg.Features.GeneralSettings {
actions = append(actions,
action("profile info", actionProfileInfo),
action("protection", actionProtection),
action("parental", actionParental),
action("safe search config", actionSafeSearchConfig),
action("safe browsing", actionSafeBrowsing),
)
}
if cfg.Features.QueryLogConfig {
actions = append(actions,
action("query log config", actionQueryLogConfig),
)
}
if cfg.Features.StatsConfig {
actions = append(actions,
action("stats config", actionStatsConfig),
)
}
if cfg.Features.DNS.Rewrites {
actions = append(actions,
action("DNS rewrites", actionDNSRewrites),
)
}
if cfg.Features.Filters {
actions = append(actions,
action("actionFilters", actionFilters),
)
}
if cfg.Features.Services {
actions = append(actions,
action("blocked services", actionBlockedServices),
action("blocked services schedule", actionBlockedServicesSchedule),
)
}
if cfg.Features.ClientSettings {
actions = append(actions,
action("client settings", actionClientSettings),
)
}
if cfg.Features.DNS.AccessLists {
actions = append(actions,
action("DNS access lists", actionDNSAccessLists),
)
}
if cfg.Features.DNS.ServerConfig {
actions = append(actions,
action("DNS server config", actionDNSServerConfig),
)
}
if cfg.Features.DHCP.ServerConfig {
actions = append(actions,
action("DHCP server config", actionDHCPServerConfig),
)
}
if cfg.Features.DHCP.StaticLeases {
actions = append(actions,
action("DHCP static leases", actionDHCPStaticLeases),
)
}
return actions
}
type syncAction interface {
sync(ac *actionContext) error
name() string
}
type actionContext struct {
rl *zap.SugaredLogger
origin *origin
client client.Client
replicaStatus *model.ServerStatus
continueOnError bool
replica types.AdGuardInstance
}
type defaultAction struct {
myName string
doSync func(ac *actionContext) error
}
func action(name string, f func(ac *actionContext) error) syncAction {
return &defaultAction{myName: name, doSync: f}
}
func (d *defaultAction) sync(ac *actionContext) error {
return d.doSync(ac)
}
func (d *defaultAction) name() string {
return d.myName
}

View File

@@ -128,8 +128,9 @@ type syncStatus struct {
} }
type replicaStatus struct { type replicaStatus struct {
Host string `json:"origin"` Host string `json:"host"`
URL string `json:"url"` URL string `json:"url"`
Status string `json:"status"` Status string `json:"status"`
Error string `json:"error"` Error string `json:"error,omitempty"`
ProtectionEnabled *bool `json:"protection_enabled"`
} }

View File

@@ -90,6 +90,7 @@ type worker struct {
running bool running bool
cron *cron.Cron cron *cron.Cron
createClient func(instance types.AdGuardInstance) (client.Client, error) createClient func(instance types.AdGuardInstance) (client.Client, error)
actions []syncAction
} }
func (w *worker) status() *syncStatus { func (w *worker) status() *syncStatus {
@@ -112,23 +113,32 @@ func (w *worker) status() *syncStatus {
return syncStatus return syncStatus
} }
func (w *worker) getStatus(inst types.AdGuardInstance) replicaStatus { func (w *worker) getStatus(inst types.AdGuardInstance) (st replicaStatus) {
st = replicaStatus{Host: inst.WebHost, URL: inst.WebURL}
oc, err := w.createClient(inst) oc, err := w.createClient(inst)
if err != nil { if err != nil {
l.With("error", err, "url", w.cfg.Origin.URL).Error("Error creating origin client") l.With("error", err, "url", w.cfg.Origin.URL).Error("Error creating origin client")
return replicaStatus{Host: oc.Host(), URL: inst.URL, Error: err.Error(), Status: "danger"} st.Status = "danger"
st.Error = err.Error()
return
} }
sl := l.With("from", oc.Host()) sl := l.With("from", inst.WebHost)
_, err = oc.Status() status, err := oc.Status()
if err != nil { if err != nil {
if errors.Is(err, client.ErrSetupNeeded) { if errors.Is(err, client.ErrSetupNeeded) {
return replicaStatus{Host: oc.Host(), URL: inst.URL, Error: err.Error(), Status: "warning"} st.Status = "warning"
st.Error = err.Error()
return
} }
sl.With("error", err).Error("Error getting origin status") sl.With("error", err).Error("Error getting origin status")
return replicaStatus{Host: oc.Host(), URL: inst.URL, Error: err.Error(), Status: "danger"} st.Status = "danger"
st.Error = err.Error()
return
} }
st := replicaStatus{Host: oc.Host(), URL: inst.URL, Status: "success"} st.Status = "success"
return st st.ProtectionEnabled = utils.Ptr(status.ProtectionEnabled)
return
} }
func (w *worker) sync() { func (w *worker) sync() {
@@ -203,7 +213,7 @@ func (w *worker) sync() {
o.filters, err = oc.Filtering() o.filters, err = oc.Filtering()
if err != nil { if err != nil {
sl.With("error", err).Error("Error getting origin filters") sl.With("error", err).Error("Error getting origin actionFilters")
return return
} }
o.clients, err = oc.Clients() o.clients, err = oc.Clients()
@@ -242,6 +252,8 @@ func (w *worker) sync() {
} }
} }
w.actions = setupActions(w.cfg)
replicas := w.cfg.UniqueReplicas() replicas := w.cfg.UniqueReplicas()
for _, replica := range replicas { for _, replica := range replicas {
w.syncTo(sl, o, replica) w.syncTo(sl, o, replica)
@@ -258,66 +270,37 @@ func (w *worker) syncTo(l *zap.SugaredLogger, o *origin, replica types.AdGuardIn
rl := l.With("to", rc.Host()) rl := l.With("to", rc.Host())
rl.Info("Start sync") rl.Info("Start sync")
rs, err := w.statusWithSetup(rl, replica, rc) replicaStatus, err := w.statusWithSetup(rl, replica, rc)
if err != nil { if err != nil {
rl.With("error", err).Error("Error getting replica status") rl.With("error", err).Error("Error getting replica status")
return return
} }
rl.With("version", rs.Version).Info("Connected to replica") rl.With("version", replicaStatus.Version).Info("Connected to replica")
if versions.IsNewerThan(versions.MinAgh, rs.Version) { if versions.IsNewerThan(versions.MinAgh, replicaStatus.Version) {
rl.With("error", err, "version", rs.Version).Errorf("Replica AdGuard Home version must be >= %s", versions.MinAgh) rl.With("error", err, "version", replicaStatus.Version).Errorf("Replica AdGuard Home version must be >= %s", versions.MinAgh)
return return
} }
if o.status.Version != rs.Version { if o.status.Version != replicaStatus.Version {
rl.With("originVersion", o.status.Version, "replicaVersion", rs.Version).Warn("Versions do not match") rl.With("originVersion", o.status.Version, "replicaVersion", replicaStatus.Version).Warn("Versions do not match")
} }
err = w.syncGeneralSettings(o, rs, rc) ac := &actionContext{
if err != nil { continueOnError: w.cfg.ContinueOnError,
rl.With("error", err).Error("Error syncing general settings") rl: rl,
return origin: o,
replicaStatus: replicaStatus,
client: rc,
replica: replica,
} }
for _, action := range w.actions {
err = w.syncConfigs(o, rc) if err := action.sync(ac); err != nil {
if err != nil { rl.With("error", err).Errorf("Error syncing %s", action.name())
rl.With("error", err).Error("Error syncing configs") if !w.cfg.ContinueOnError {
return return
} }
err = w.syncRewrites(rl, o.rewrites, rc)
if err != nil {
rl.With("error", err).Error("Error syncing rewrites")
return
}
err = w.syncFilters(o.filters, rc)
if err != nil {
rl.With("error", err).Error("Error syncing filters")
return
}
err = w.syncServices(o.blockedServices, o.blockedServicesSchedule, rc)
if err != nil {
rl.With("error", err).Error("Error syncing blockedServices")
return
}
if err = w.syncClients(o.clients, rc); err != nil {
rl.With("error", err).Error("Error syncing clients")
return
}
if err = w.syncDNS(o.accessList, o.dnsConfig, rc); err != nil {
rl.With("error", err).Error("Error syncing dns")
return
}
if w.cfg.Features.DHCP.ServerConfig || w.cfg.Features.DHCP.StaticLeases {
if err = w.syncDHCPServer(o.dhcpServerConfig, rc, replica); err != nil {
rl.With("error", err).Error("Error syncing dhcp")
return
} }
} }
@@ -339,260 +322,6 @@ func (w *worker) statusWithSetup(rl *zap.SugaredLogger, replica types.AdGuardIns
return rs, err return rs, err
} }
func (w *worker) syncServices(os *model.BlockedServicesArray, obss *model.BlockedServicesSchedule, replica client.Client) error {
if w.cfg.Features.Services {
rs, err := replica.BlockedServices()
if err != nil {
return err
}
if !model.EqualsStringSlice(os, rs, true) {
if err := replica.SetBlockedServices(os); err != nil {
return err
}
}
rbss, err := replica.BlockedServicesSchedule()
if err != nil {
return err
}
if !obss.Equals(rbss) {
if err := replica.SetBlockedServicesSchedule(obss); err != nil {
return err
}
}
}
return nil
}
func (w *worker) syncFilters(of *model.FilterStatus, replica client.Client) error {
if w.cfg.Features.Filters {
rf, err := replica.Filtering()
if err != nil {
return err
}
if err = w.syncFilterType(of.Filters, rf.Filters, false, replica); err != nil {
return err
}
if err = w.syncFilterType(of.WhitelistFilters, rf.WhitelistFilters, true, replica); err != nil {
return err
}
if utils.PtrToString(of.UserRules) != utils.PtrToString(rf.UserRules) {
return replica.SetCustomRules(of.UserRules)
}
if of.Enabled != rf.Enabled || of.Interval != rf.Interval {
if err = replica.ToggleFiltering(*of.Enabled, *of.Interval); err != nil {
return err
}
}
}
return nil
}
func (w *worker) syncFilterType(of *[]model.Filter, rFilters *[]model.Filter, whitelist bool, replica client.Client) error {
fa, fu, fd := model.MergeFilters(rFilters, of)
if err := replica.DeleteFilters(whitelist, fd...); err != nil {
return err
}
if err := replica.AddFilters(whitelist, fa...); err != nil {
return err
}
if err := replica.UpdateFilters(whitelist, fu...); err != nil {
return err
}
if len(fa) > 0 || len(fu) > 0 {
if err := replica.RefreshFilters(whitelist); err != nil {
return err
}
}
return nil
}
func (w *worker) syncRewrites(rl *zap.SugaredLogger, or *model.RewriteEntries, replica client.Client) error {
if w.cfg.Features.DNS.Rewrites {
replicaRewrites, err := replica.RewriteList()
if err != nil {
return err
}
a, r, d := replicaRewrites.Merge(or)
if err = replica.DeleteRewriteEntries(r...); err != nil {
return err
}
if err = replica.AddRewriteEntries(a...); err != nil {
return err
}
for _, dupl := range d {
rl.With("domain", dupl.Domain, "answer", dupl.Answer).Warn("Skipping duplicated rewrite from source")
}
}
return nil
}
func (w *worker) syncClients(oc *model.Clients, replica client.Client) error {
if w.cfg.Features.ClientSettings {
rc, err := replica.Clients()
if err != nil {
return err
}
a, u, r := rc.Merge(oc)
if err = replica.DeleteClients(r...); err != nil {
return err
}
if err = replica.AddClients(a...); err != nil {
return err
}
if err = replica.UpdateClients(u...); err != nil {
return err
}
}
return nil
}
func (w *worker) syncGeneralSettings(o *origin, rs *model.ServerStatus, replica client.Client) error {
if w.cfg.Features.GeneralSettings {
if pro, err := replica.ProfileInfo(); err != nil {
return err
} else if merged := pro.ShouldSyncFor(o.profileInfo); merged != nil {
if err = replica.SetProfileInfo(merged); err != nil {
return err
}
}
if o.status.ProtectionEnabled != rs.ProtectionEnabled {
if err := replica.ToggleProtection(o.status.ProtectionEnabled); err != nil {
return err
}
}
if rp, err := replica.Parental(); err != nil {
return err
} else if o.parental != rp {
if err = replica.ToggleParental(o.parental); err != nil {
return err
}
}
if ssc, err := replica.SafeSearchConfig(); err != nil {
return err
} else if !o.safeSearch.Equals(ssc) {
if err = replica.SetSafeSearchConfig(o.safeSearch); err != nil {
return err
}
}
if rs, err := replica.SafeBrowsing(); err != nil {
return err
} else if o.safeBrowsing != rs {
if err = replica.ToggleSafeBrowsing(o.safeBrowsing); err != nil {
return err
}
}
}
return nil
}
func (w *worker) syncConfigs(o *origin, rc client.Client) error {
if w.cfg.Features.QueryLogConfig {
qlc, err := rc.QueryLogConfig()
if err != nil {
return err
}
if !o.queryLogConfig.Equals(qlc) {
if err = rc.SetQueryLogConfig(o.queryLogConfig); err != nil {
return err
}
}
}
if w.cfg.Features.StatsConfig {
sc, err := rc.StatsConfig()
if err != nil {
return err
}
if o.statsConfig.Interval != sc.Interval {
if err = rc.SetStatsConfig(o.statsConfig); err != nil {
return err
}
}
}
return nil
}
func (w *worker) syncDNS(oal *model.AccessList, odc *model.DNSConfig, rc client.Client) error {
if w.cfg.Features.DNS.AccessLists {
al, err := rc.AccessList()
if err != nil {
return err
}
if !al.Equals(oal) {
if err = rc.SetAccessList(oal); err != nil {
return err
}
}
}
if w.cfg.Features.DNS.ServerConfig {
dc, err := rc.DNSConfig()
if err != nil {
return err
}
if !dc.Equals(odc) {
if err = rc.SetDNSConfig(odc); err != nil {
return err
}
}
}
return nil
}
func (w *worker) syncDHCPServer(osc *model.DhcpStatus, rc client.Client, replica types.AdGuardInstance) error {
if !w.cfg.Features.DHCP.ServerConfig && !w.cfg.Features.DHCP.StaticLeases {
return nil
}
sc, err := rc.DhcpConfig()
if w.cfg.Features.DHCP.ServerConfig && osc.HasConfig() {
if err != nil {
return err
}
origClone := osc.Clone()
if replica.InterfaceName != "" {
// overwrite interface name
origClone.InterfaceName = utils.Ptr(replica.InterfaceName)
}
if replica.DHCPServerEnabled != nil {
// overwrite dhcp enabled
origClone.Enabled = replica.DHCPServerEnabled
}
if !sc.Equals(origClone) {
if err = rc.SetDhcpConfig(origClone); err != nil {
return err
}
}
}
if w.cfg.Features.DHCP.StaticLeases {
a, r := model.MergeDhcpStaticLeases(sc.StaticLeases, osc.StaticLeases)
if err = rc.DeleteDHCPStaticLeases(r...); err != nil {
return err
}
if err = rc.AddDHCPStaticLeases(a...); err != nil {
return err
}
}
return nil
}
type origin struct { type origin struct {
status *model.ServerStatus status *model.ServerStatus
rewrites *model.RewriteEntries rewrites *model.RewriteEntries

View File

@@ -9,20 +9,19 @@ import (
"github.com/bakito/adguardhome-sync/pkg/types" "github.com/bakito/adguardhome-sync/pkg/types"
"github.com/bakito/adguardhome-sync/pkg/utils" "github.com/bakito/adguardhome-sync/pkg/utils"
"github.com/bakito/adguardhome-sync/pkg/versions" "github.com/bakito/adguardhome-sync/pkg/versions"
gm "github.com/golang/mock/gomock"
"github.com/google/uuid" "github.com/google/uuid"
. "github.com/onsi/ginkgo/v2" . "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
gm "go.uber.org/mock/gomock"
) )
var boolTrue = true
var _ = Describe("Sync", func() { var _ = Describe("Sync", func() {
var ( var (
mockCtrl *gm.Controller mockCtrl *gm.Controller
cl *clientmock.MockClient cl *clientmock.MockClient
w *worker w *worker
te error te error
ac *actionContext
) )
BeforeEach(func() { BeforeEach(func() {
@@ -50,16 +49,38 @@ var _ = Describe("Sync", func() {
StatsConfig: true, StatsConfig: true,
QueryLogConfig: true, QueryLogConfig: true,
}, },
Replicas: []types.AdGuardInstance{
{},
},
}, },
} }
te = errors.New(uuid.NewString()) te = errors.New(uuid.NewString())
ac = &actionContext{
continueOnError: false,
rl: l,
origin: &origin{
profileInfo: &model.ProfileInfo{
Name: "origin",
Language: "en",
Theme: "auto",
},
status: &model.ServerStatus{},
safeSearch: &model.SafeSearchConfig{},
queryLogConfig: &model.QueryLogConfig{},
statsConfig: &model.StatsConfig{},
},
replicaStatus: &model.ServerStatus{},
client: cl,
replica: w.cfg.Replicas[0],
}
}) })
AfterEach(func() { AfterEach(func() {
defer mockCtrl.Finish() defer mockCtrl.Finish()
}) })
Context("worker", func() { Context("worker", func() {
Context("syncRewrites", func() { Context("actionDNSRewrites", func() {
var ( var (
domain string domain string
answer string answer string
@@ -70,289 +91,238 @@ var _ = Describe("Sync", func() {
BeforeEach(func() { BeforeEach(func() {
domain = uuid.NewString() domain = uuid.NewString()
answer = uuid.NewString() answer = uuid.NewString()
reO = []model.RewriteEntry{{Domain: utils.Ptr(domain), Answer: utils.Ptr(answer)}} reO = model.RewriteEntries{{Domain: utils.Ptr(domain), Answer: utils.Ptr(answer)}}
reR = []model.RewriteEntry{{Domain: utils.Ptr(domain), Answer: utils.Ptr(answer)}} reR = model.RewriteEntries{{Domain: utils.Ptr(domain), Answer: utils.Ptr(answer)}}
}) })
It("should have no changes (empty slices)", func() { It("should have no changes (empty slices)", func() {
ac.origin.rewrites = &reO
cl.EXPECT().RewriteList().Return(&reR, nil) cl.EXPECT().RewriteList().Return(&reR, nil)
cl.EXPECT().AddRewriteEntries() cl.EXPECT().AddRewriteEntries()
cl.EXPECT().DeleteRewriteEntries() cl.EXPECT().DeleteRewriteEntries()
err := w.syncRewrites(l, &reO, cl) err := actionDNSRewrites(ac)
Ω(err).ShouldNot(HaveOccurred()) Ω(err).ShouldNot(HaveOccurred())
}) })
It("should add one rewrite entry", func() { It("should add one rewrite entry", func() {
reR = []model.RewriteEntry{} reR = []model.RewriteEntry{}
ac.origin.rewrites = &reO
cl.EXPECT().RewriteList().Return(&reR, nil) cl.EXPECT().RewriteList().Return(&reR, nil)
cl.EXPECT().AddRewriteEntries(reO[0]) cl.EXPECT().AddRewriteEntries(reO[0])
cl.EXPECT().DeleteRewriteEntries() cl.EXPECT().DeleteRewriteEntries()
err := w.syncRewrites(l, &reO, cl) err := actionDNSRewrites(ac)
Ω(err).ShouldNot(HaveOccurred()) Ω(err).ShouldNot(HaveOccurred())
}) })
It("should remove one rewrite entry", func() { It("should remove one rewrite entry", func() {
reO = []model.RewriteEntry{} reO = []model.RewriteEntry{}
ac.origin.rewrites = &reO
cl.EXPECT().RewriteList().Return(&reR, nil) cl.EXPECT().RewriteList().Return(&reR, nil)
cl.EXPECT().AddRewriteEntries() cl.EXPECT().AddRewriteEntries()
cl.EXPECT().DeleteRewriteEntries(reR[0]) cl.EXPECT().DeleteRewriteEntries(reR[0])
err := w.syncRewrites(l, &reO, cl) err := actionDNSRewrites(ac)
Ω(err).ShouldNot(HaveOccurred()) Ω(err).ShouldNot(HaveOccurred())
}) })
It("should remove one rewrite entry", func() { It("should remove one rewrite entry", func() {
reO = []model.RewriteEntry{} reO = []model.RewriteEntry{}
ac.origin.rewrites = &reO
cl.EXPECT().RewriteList().Return(&reR, nil) cl.EXPECT().RewriteList().Return(&reR, nil)
cl.EXPECT().AddRewriteEntries() cl.EXPECT().AddRewriteEntries()
cl.EXPECT().DeleteRewriteEntries(reR[0]) cl.EXPECT().DeleteRewriteEntries(reR[0])
err := w.syncRewrites(l, &reO, cl) err := actionDNSRewrites(ac)
Ω(err).ShouldNot(HaveOccurred()) Ω(err).ShouldNot(HaveOccurred())
}) })
It("should return error when error on RewriteList()", func() { It("should return error when error on RewriteList()", func() {
ac.origin.rewrites = &reO
cl.EXPECT().RewriteList().Return(nil, te) cl.EXPECT().RewriteList().Return(nil, te)
err := w.syncRewrites(l, &reO, cl) err := actionDNSRewrites(ac)
Ω(err).Should(HaveOccurred()) Ω(err).Should(HaveOccurred())
}) })
It("should return error when error on AddRewriteEntries()", func() { It("should return error when error on AddRewriteEntries()", func() {
ac.origin.rewrites = &reO
cl.EXPECT().RewriteList().Return(&reR, nil) cl.EXPECT().RewriteList().Return(&reR, nil)
cl.EXPECT().DeleteRewriteEntries() cl.EXPECT().DeleteRewriteEntries()
cl.EXPECT().AddRewriteEntries().Return(te) cl.EXPECT().AddRewriteEntries().Return(te)
err := w.syncRewrites(l, &reO, cl) err := actionDNSRewrites(ac)
Ω(err).Should(HaveOccurred()) Ω(err).Should(HaveOccurred())
}) })
It("should return error when error on DeleteRewriteEntries()", func() { It("should return error when error on DeleteRewriteEntries()", func() {
ac.origin.rewrites = &reO
cl.EXPECT().RewriteList().Return(&reR, nil) cl.EXPECT().RewriteList().Return(&reR, nil)
cl.EXPECT().DeleteRewriteEntries().Return(te) cl.EXPECT().DeleteRewriteEntries().Return(te)
err := w.syncRewrites(l, &reO, cl) err := actionDNSRewrites(ac)
Ω(err).Should(HaveOccurred()) Ω(err).Should(HaveOccurred())
}) })
}) })
Context("syncClients", func() { Context("actionClientSettings", func() {
var ( var (
clO *model.Clients
clR *model.Clients clR *model.Clients
name string name string
) )
BeforeEach(func() { BeforeEach(func() {
name = uuid.NewString() name = uuid.NewString()
clO = &model.Clients{Clients: &model.ClientsArray{{Name: utils.Ptr(name)}}} ac.origin.clients = &model.Clients{Clients: &model.ClientsArray{{Name: utils.Ptr(name)}}}
clR = &model.Clients{Clients: &model.ClientsArray{{Name: utils.Ptr(name)}}} clR = &model.Clients{Clients: &model.ClientsArray{{Name: utils.Ptr(name)}}}
}) })
It("should have no changes (empty slices)", func() { It("should have no changes (empty slices)", func() {
cl.EXPECT().Clients().Return(clR, nil) cl.EXPECT().Clients().Return(clR, nil)
cl.EXPECT().AddClients() err := actionClientSettings(ac)
cl.EXPECT().UpdateClients()
cl.EXPECT().DeleteClients()
err := w.syncClients(clO, cl)
Ω(err).ShouldNot(HaveOccurred()) Ω(err).ShouldNot(HaveOccurred())
}) })
It("should add one client", func() { It("should add one client", func() {
clR.Clients = &model.ClientsArray{} clR.Clients = &model.ClientsArray{}
cl.EXPECT().Clients().Return(clR, nil) cl.EXPECT().Clients().Return(clR, nil)
cl.EXPECT().AddClients(&(*clO.Clients)[0]) cl.EXPECT().AddClient(&(*ac.origin.clients.Clients)[0])
cl.EXPECT().UpdateClients() err := actionClientSettings(ac)
cl.EXPECT().DeleteClients()
err := w.syncClients(clO, cl)
Ω(err).ShouldNot(HaveOccurred()) Ω(err).ShouldNot(HaveOccurred())
}) })
It("should update one client", func() { It("should update one client", func() {
(*clR.Clients)[0].FilteringEnabled = utils.Ptr(true) (*clR.Clients)[0].FilteringEnabled = utils.Ptr(true)
cl.EXPECT().Clients().Return(clR, nil) cl.EXPECT().Clients().Return(clR, nil)
cl.EXPECT().AddClients() cl.EXPECT().UpdateClient(&(*ac.origin.clients.Clients)[0])
cl.EXPECT().UpdateClients(&(*clO.Clients)[0]) err := actionClientSettings(ac)
cl.EXPECT().DeleteClients()
err := w.syncClients(clO, cl)
Ω(err).ShouldNot(HaveOccurred()) Ω(err).ShouldNot(HaveOccurred())
}) })
It("should delete one client", func() { It("should delete one client", func() {
clO.Clients = &model.ClientsArray{} ac.origin.clients.Clients = &model.ClientsArray{}
cl.EXPECT().Clients().Return(clR, nil) cl.EXPECT().Clients().Return(clR, nil)
cl.EXPECT().AddClients() cl.EXPECT().DeleteClient(&(*clR.Clients)[0])
cl.EXPECT().UpdateClients() err := actionClientSettings(ac)
cl.EXPECT().DeleteClients(&(*clR.Clients)[0])
err := w.syncClients(clO, cl)
Ω(err).ShouldNot(HaveOccurred()) Ω(err).ShouldNot(HaveOccurred())
}) })
It("should return error when error on Clients()", func() { It("should return error when error on Clients()", func() {
cl.EXPECT().Clients().Return(nil, te) cl.EXPECT().Clients().Return(nil, te)
err := w.syncClients(clO, cl) err := actionClientSettings(ac)
Ω(err).Should(HaveOccurred())
})
It("should return error when error on AddClients()", func() {
cl.EXPECT().Clients().Return(clR, nil)
cl.EXPECT().DeleteClients()
cl.EXPECT().AddClients().Return(te)
err := w.syncClients(clO, cl)
Ω(err).Should(HaveOccurred())
})
It("should return error when error on UpdateClients()", func() {
cl.EXPECT().Clients().Return(clR, nil)
cl.EXPECT().DeleteClients()
cl.EXPECT().AddClients()
cl.EXPECT().UpdateClients().Return(te)
err := w.syncClients(clO, cl)
Ω(err).Should(HaveOccurred())
})
It("should return error when error on DeleteClients()", func() {
cl.EXPECT().Clients().Return(clR, nil)
cl.EXPECT().DeleteClients().Return(te)
err := w.syncClients(clO, cl)
Ω(err).Should(HaveOccurred()) Ω(err).Should(HaveOccurred())
}) })
}) })
Context("syncGeneralSettings", func() { Context("actionParental", func() {
var (
o *origin
rs *model.ServerStatus
)
BeforeEach(func() {
o = &origin{
profileInfo: &model.ProfileInfo{
Name: "origin",
Language: "en",
Theme: "auto",
},
status: &model.ServerStatus{},
safeSearch: &model.SafeSearchConfig{},
}
rs = &model.ServerStatus{}
})
It("should have no changes", func() { It("should have no changes", func() {
cl.EXPECT().Parental() cl.EXPECT().Parental()
cl.EXPECT().ProfileInfo().Return(o.profileInfo, nil) err := actionParental(ac)
cl.EXPECT().SafeSearchConfig().Return(o.safeSearch, nil)
cl.EXPECT().SafeBrowsing()
err := w.syncGeneralSettings(o, rs, cl)
Ω(err).ShouldNot(HaveOccurred())
})
It("should have protection enabled changes", func() {
o.status.ProtectionEnabled = true
cl.EXPECT().ToggleProtection(true)
cl.EXPECT().Parental()
cl.EXPECT().ProfileInfo().Return(o.profileInfo, nil)
cl.EXPECT().SafeSearchConfig().Return(o.safeSearch, nil)
cl.EXPECT().SafeBrowsing()
err := w.syncGeneralSettings(o, rs, cl)
Ω(err).ShouldNot(HaveOccurred()) Ω(err).ShouldNot(HaveOccurred())
}) })
It("should have parental enabled changes", func() { It("should have parental enabled changes", func() {
o.parental = true ac.origin.parental = true
cl.EXPECT().Parental() cl.EXPECT().Parental()
cl.EXPECT().ToggleParental(true) cl.EXPECT().ToggleParental(true)
cl.EXPECT().ProfileInfo().Return(o.profileInfo, nil) err := actionParental(ac)
cl.EXPECT().SafeSearchConfig().Return(o.safeSearch, nil) Ω(err).ShouldNot(HaveOccurred())
cl.EXPECT().SafeBrowsing() })
err := w.syncGeneralSettings(o, rs, cl) })
Context("actionProtection", func() {
It("should have no changes", func() {
err := actionProtection(ac)
Ω(err).ShouldNot(HaveOccurred())
})
It("should have protection enabled changes", func() {
ac.origin.status.ProtectionEnabled = true
cl.EXPECT().ToggleProtection(true)
err := actionProtection(ac)
Ω(err).ShouldNot(HaveOccurred())
})
})
Context("actionSafeSearchConfig", func() {
It("should have no changes", func() {
cl.EXPECT().SafeSearchConfig().Return(ac.origin.safeSearch, nil)
err := actionSafeSearchConfig(ac)
Ω(err).ShouldNot(HaveOccurred()) Ω(err).ShouldNot(HaveOccurred())
}) })
It("should have safeSearch enabled changes", func() { It("should have safeSearch enabled changes", func() {
o.safeSearch = &model.SafeSearchConfig{Enabled: utils.Ptr(true)} ac.origin.safeSearch = &model.SafeSearchConfig{Enabled: utils.Ptr(true)}
cl.EXPECT().Parental()
cl.EXPECT().SafeSearchConfig().Return(&model.SafeSearchConfig{}, nil) cl.EXPECT().SafeSearchConfig().Return(&model.SafeSearchConfig{}, nil)
cl.EXPECT().ProfileInfo().Return(o.profileInfo, nil) cl.EXPECT().SetSafeSearchConfig(ac.origin.safeSearch)
cl.EXPECT().SetSafeSearchConfig(o.safeSearch) err := actionSafeSearchConfig(ac)
cl.EXPECT().SafeBrowsing()
err := w.syncGeneralSettings(o, rs, cl)
Ω(err).ShouldNot(HaveOccurred()) Ω(err).ShouldNot(HaveOccurred())
}) })
It("should have Duckduckgo safeSearch enabled changed", func() { It("should have Duckduckgo safeSearch enabled changed", func() {
o.safeSearch = &model.SafeSearchConfig{Duckduckgo: utils.Ptr(true)} ac.origin.safeSearch = &model.SafeSearchConfig{Duckduckgo: utils.Ptr(true)}
cl.EXPECT().Parental()
cl.EXPECT().ProfileInfo().Return(o.profileInfo, nil)
cl.EXPECT().SafeSearchConfig().Return(&model.SafeSearchConfig{Google: utils.Ptr(true)}, nil) cl.EXPECT().SafeSearchConfig().Return(&model.SafeSearchConfig{Google: utils.Ptr(true)}, nil)
cl.EXPECT().SafeBrowsing() cl.EXPECT().SetSafeSearchConfig(ac.origin.safeSearch)
cl.EXPECT().SetSafeSearchConfig(o.safeSearch) err := actionSafeSearchConfig(ac)
Ω(err).ShouldNot(HaveOccurred())
err := w.syncGeneralSettings(o, rs, cl) })
})
Context("actionProfileInfo", func() {
It("should have no changes", func() {
cl.EXPECT().ProfileInfo().Return(ac.origin.profileInfo, nil)
err := actionProfileInfo(ac)
Ω(err).ShouldNot(HaveOccurred()) Ω(err).ShouldNot(HaveOccurred())
}) })
It("should have profileInfo language changed", func() { It("should have profileInfo language changed", func() {
o.profileInfo.Language = "de" ac.origin.profileInfo.Language = "de"
cl.EXPECT().Parental()
cl.EXPECT().ProfileInfo().Return(&model.ProfileInfo{Name: "replica", Language: "en"}, nil) cl.EXPECT().ProfileInfo().Return(&model.ProfileInfo{Name: "replica", Language: "en"}, nil)
cl.EXPECT().SafeSearchConfig().Return(o.safeSearch, nil)
cl.EXPECT().SafeBrowsing()
cl.EXPECT().SetProfileInfo(&model.ProfileInfo{ cl.EXPECT().SetProfileInfo(&model.ProfileInfo{
Language: "de", Language: "de",
Name: "replica", Name: "replica",
Theme: "auto", Theme: "auto",
}) })
err := w.syncGeneralSettings(o, rs, cl) err := actionProfileInfo(ac)
Ω(err).ShouldNot(HaveOccurred()) Ω(err).ShouldNot(HaveOccurred())
}) })
It("should not sync profileInfo if language is not set", func() { It("should not sync profileInfo if language is not set", func() {
o.profileInfo.Language = "" ac.origin.profileInfo.Language = ""
cl.EXPECT().Parental()
cl.EXPECT().ProfileInfo().Return(&model.ProfileInfo{Name: "replica", Language: "en", Theme: "auto"}, nil) cl.EXPECT().ProfileInfo().Return(&model.ProfileInfo{Name: "replica", Language: "en", Theme: "auto"}, nil)
cl.EXPECT().SafeSearchConfig().Return(o.safeSearch, nil) cl.EXPECT().SetProfileInfo(ac.origin.profileInfo).Times(0)
cl.EXPECT().SafeBrowsing() err := actionProfileInfo(ac)
cl.EXPECT().SetProfileInfo(o.profileInfo).Times(0)
err := w.syncGeneralSettings(o, rs, cl)
Ω(err).ShouldNot(HaveOccurred())
})
It("should not sync profileInfo if language is not set", func() {
o.profileInfo.Language = ""
cl.EXPECT().Parental()
cl.EXPECT().ProfileInfo().Return(&model.ProfileInfo{Name: "replica", Language: "en", Theme: "auto"}, nil)
cl.EXPECT().SafeSearchConfig().Return(o.safeSearch, nil)
cl.EXPECT().SafeBrowsing()
cl.EXPECT().SetProfileInfo(o.profileInfo).Times(0)
err := w.syncGeneralSettings(o, rs, cl)
Ω(err).ShouldNot(HaveOccurred()) Ω(err).ShouldNot(HaveOccurred())
}) })
It("should not sync profileInfo if theme is not set", func() { It("should not sync profileInfo if theme is not set", func() {
o.profileInfo.Theme = "" ac.origin.profileInfo.Theme = ""
cl.EXPECT().Parental()
cl.EXPECT().ProfileInfo().Return(&model.ProfileInfo{Name: "replica", Language: "en", Theme: "auto"}, nil) cl.EXPECT().ProfileInfo().Return(&model.ProfileInfo{Name: "replica", Language: "en", Theme: "auto"}, nil)
cl.EXPECT().SafeSearchConfig().Return(o.safeSearch, nil) cl.EXPECT().SetProfileInfo(ac.origin.profileInfo).Times(0)
cl.EXPECT().SafeBrowsing() err := actionProfileInfo(ac)
cl.EXPECT().SetProfileInfo(o.profileInfo).Times(0)
err := w.syncGeneralSettings(o, rs, cl)
Ω(err).ShouldNot(HaveOccurred())
})
It("should have safeBrowsing enabled changes", func() {
o.safeBrowsing = true
cl.EXPECT().Parental()
cl.EXPECT().ProfileInfo().Return(o.profileInfo, nil)
cl.EXPECT().SafeSearchConfig().Return(o.safeSearch, nil)
cl.EXPECT().SafeBrowsing()
cl.EXPECT().ToggleSafeBrowsing(true)
err := w.syncGeneralSettings(o, rs, cl)
Ω(err).ShouldNot(HaveOccurred()) Ω(err).ShouldNot(HaveOccurred())
}) })
}) })
Context("syncConfigs", func() { Context("actionSafeBrowsing", func() {
var ( It("should have no changes", func() {
o *origin cl.EXPECT().SafeBrowsing()
qlc *model.QueryLogConfig err := actionSafeBrowsing(ac)
sc *model.StatsConfig Ω(err).ShouldNot(HaveOccurred())
) })
It("should have safeBrowsing enabled changes", func() {
ac.origin.safeBrowsing = true
cl.EXPECT().SafeBrowsing()
cl.EXPECT().ToggleSafeBrowsing(true)
err := actionSafeBrowsing(ac)
Ω(err).ShouldNot(HaveOccurred())
})
})
Context("actionQueryLogConfig", func() {
var qlc *model.QueryLogConfig
BeforeEach(func() { BeforeEach(func() {
o = &origin{
queryLogConfig: &model.QueryLogConfig{},
statsConfig: &model.StatsConfig{},
}
qlc = &model.QueryLogConfig{} qlc = &model.QueryLogConfig{}
sc = &model.StatsConfig{}
}) })
It("should have no changes", func() { It("should have no changes", func() {
cl.EXPECT().QueryLogConfig().Return(qlc, nil) cl.EXPECT().QueryLogConfig().Return(qlc, nil)
cl.EXPECT().StatsConfig().Return(sc, nil) err := actionQueryLogConfig(ac)
err := w.syncConfigs(o, cl)
Ω(err).ShouldNot(HaveOccurred()) Ω(err).ShouldNot(HaveOccurred())
}) })
It("should have QueryLogConfig changes", func() { It("should have QueryLogConfig changes", func() {
var interval model.QueryLogConfigInterval = 123 var interval model.QueryLogConfigInterval = 123
o.queryLogConfig.Interval = &interval ac.origin.queryLogConfig.Interval = &interval
cl.EXPECT().QueryLogConfig().Return(qlc, nil) cl.EXPECT().QueryLogConfig().Return(qlc, nil)
cl.EXPECT().SetQueryLogConfig(&model.QueryLogConfig{AnonymizeClientIp: nil, Interval: &interval, Enabled: nil}) cl.EXPECT().SetQueryLogConfig(&model.QueryLogConfig{AnonymizeClientIp: nil, Interval: &interval, Enabled: nil})
err := actionQueryLogConfig(ac)
Ω(err).ShouldNot(HaveOccurred())
})
})
Context("syncConfigs", func() {
var sc *model.StatsConfig
BeforeEach(func() {
sc = &model.StatsConfig{}
})
It("should have no changes", func() {
cl.EXPECT().StatsConfig().Return(sc, nil) cl.EXPECT().StatsConfig().Return(sc, nil)
err := w.syncConfigs(o, cl) err := actionStatsConfig(ac)
Ω(err).ShouldNot(HaveOccurred()) Ω(err).ShouldNot(HaveOccurred())
}) })
It("should have StatsConfig changes", func() { It("should have StatsConfig changes", func() {
var interval model.StatsConfigInterval = 123 var interval model.StatsConfigInterval = 123
o.statsConfig.Interval = &interval ac.origin.statsConfig.Interval = &interval
cl.EXPECT().QueryLogConfig().Return(qlc, nil)
cl.EXPECT().StatsConfig().Return(sc, nil) cl.EXPECT().StatsConfig().Return(sc, nil)
cl.EXPECT().SetStatsConfig(&model.StatsConfig{Interval: &interval}) cl.EXPECT().SetStatsConfig(&model.StatsConfig{Interval: &interval})
err := w.syncConfigs(o, cl) err := actionStatsConfig(ac)
Ω(err).ShouldNot(HaveOccurred()) Ω(err).ShouldNot(HaveOccurred())
}) })
}) })
@@ -389,161 +359,209 @@ var _ = Describe("Sync", func() {
Ω(st).Should(BeNil()) Ω(st).Should(BeNil())
}) })
}) })
Context("syncServices", func() { Context("actionBlockedServices", func() {
var ( var rbs *model.BlockedServicesArray
obs *model.BlockedServicesArray
rbs *model.BlockedServicesArray
obss *model.BlockedServicesSchedule
)
BeforeEach(func() { BeforeEach(func() {
obs = &model.BlockedServicesArray{"foo"} ac.origin.blockedServices = &model.BlockedServicesArray{"foo"}
rbs = &model.BlockedServicesArray{"foo"} rbs = &model.BlockedServicesArray{"foo"}
obss = &model.BlockedServicesSchedule{}
}) })
It("should have no changes", func() { It("should have no changes", func() {
cl.EXPECT().BlockedServices().Return(rbs, nil) cl.EXPECT().BlockedServices().Return(rbs, nil)
cl.EXPECT().BlockedServicesSchedule().Return(obss, nil) err := actionBlockedServices(ac)
err := w.syncServices(obs, obss, cl)
Ω(err).ShouldNot(HaveOccurred()) Ω(err).ShouldNot(HaveOccurred())
}) })
It("should have blockedServices changes", func() { It("should have blockedServices changes", func() {
obs = &model.BlockedServicesArray{"bar"} ac.origin.blockedServices = &model.BlockedServicesArray{"bar"}
cl.EXPECT().BlockedServices().Return(rbs, nil) cl.EXPECT().BlockedServices().Return(rbs, nil)
cl.EXPECT().BlockedServicesSchedule().Return(obss, nil) cl.EXPECT().SetBlockedServices(ac.origin.blockedServices)
cl.EXPECT().SetBlockedServices(obs) err := actionBlockedServices(ac)
err := w.syncServices(obs, obss, cl) Ω(err).ShouldNot(HaveOccurred())
})
})
Context("actionBlockedServicesSchedule", func() {
var rbss *model.BlockedServicesSchedule
BeforeEach(func() {
ac.origin.blockedServicesSchedule = &model.BlockedServicesSchedule{}
rbss = &model.BlockedServicesSchedule{}
})
It("should have no changes", func() {
cl.EXPECT().BlockedServicesSchedule().Return(ac.origin.blockedServicesSchedule, nil)
err := actionBlockedServicesSchedule(ac)
Ω(err).ShouldNot(HaveOccurred())
})
It("should have blockedServices schedule changes", func() {
ac.origin.blockedServicesSchedule = &model.BlockedServicesSchedule{Ids: utils.Ptr([]string{"bar"})}
cl.EXPECT().BlockedServicesSchedule().Return(rbss, nil)
cl.EXPECT().SetBlockedServicesSchedule(ac.origin.blockedServicesSchedule)
err := actionBlockedServicesSchedule(ac)
Ω(err).ShouldNot(HaveOccurred()) Ω(err).ShouldNot(HaveOccurred())
}) })
}) })
Context("syncFilters", func() { Context("syncFilters", func() {
var ( var rf *model.FilterStatus
of *model.FilterStatus
rf *model.FilterStatus
)
BeforeEach(func() { BeforeEach(func() {
of = &model.FilterStatus{} ac.origin.filters = &model.FilterStatus{}
rf = &model.FilterStatus{} rf = &model.FilterStatus{}
}) })
It("should have no changes", func() { It("should have no changes", func() {
cl.EXPECT().Filtering().Return(rf, nil) cl.EXPECT().Filtering().Return(rf, nil)
cl.EXPECT().AddFilters(false) err := actionFilters(ac)
cl.EXPECT().UpdateFilters(false)
cl.EXPECT().DeleteFilters(false)
cl.EXPECT().AddFilters(true)
cl.EXPECT().UpdateFilters(true)
cl.EXPECT().DeleteFilters(true)
err := w.syncFilters(of, cl)
Ω(err).ShouldNot(HaveOccurred()) Ω(err).ShouldNot(HaveOccurred())
}) })
It("should have changes user roles", func() { It("should have changes user roles", func() {
of.UserRules = utils.Ptr([]string{"foo"}) ac.origin.filters.UserRules = utils.Ptr([]string{"foo"})
cl.EXPECT().Filtering().Return(rf, nil) cl.EXPECT().Filtering().Return(rf, nil)
cl.EXPECT().AddFilters(false) cl.EXPECT().SetCustomRules(ac.origin.filters.UserRules)
cl.EXPECT().UpdateFilters(false) err := actionFilters(ac)
cl.EXPECT().DeleteFilters(false)
cl.EXPECT().AddFilters(true)
cl.EXPECT().UpdateFilters(true)
cl.EXPECT().DeleteFilters(true)
cl.EXPECT().SetCustomRules(of.UserRules)
err := w.syncFilters(of, cl)
Ω(err).ShouldNot(HaveOccurred()) Ω(err).ShouldNot(HaveOccurred())
}) })
It("should have changed filtering config", func() { It("should have changed filtering config", func() {
of.Enabled = utils.Ptr(true) ac.origin.filters.Enabled = utils.Ptr(true)
of.Interval = utils.Ptr(123) ac.origin.filters.Interval = utils.Ptr(123)
cl.EXPECT().Filtering().Return(rf, nil) cl.EXPECT().Filtering().Return(rf, nil)
cl.EXPECT().AddFilters(false) cl.EXPECT().ToggleFiltering(*ac.origin.filters.Enabled, *ac.origin.filters.Interval)
cl.EXPECT().UpdateFilters(false) err := actionFilters(ac)
cl.EXPECT().DeleteFilters(false) Ω(err).ShouldNot(HaveOccurred())
cl.EXPECT().AddFilters(true) })
cl.EXPECT().UpdateFilters(true) It("should add a filter", func() {
cl.EXPECT().DeleteFilters(true) ac.origin.filters.Filters = utils.Ptr([]model.Filter{{Name: "foo", Url: "https://foo.bar"}})
cl.EXPECT().ToggleFiltering(*of.Enabled, *of.Interval) cl.EXPECT().Filtering().Return(rf, nil)
err := w.syncFilters(of, cl) cl.EXPECT().AddFilter(false, model.Filter{Name: "foo", Url: "https://foo.bar"})
cl.EXPECT().RefreshFilters(gm.Any())
err := actionFilters(ac)
Ω(err).ShouldNot(HaveOccurred())
})
It("should delete a filter", func() {
rf.Filters = utils.Ptr([]model.Filter{{Name: "foo", Url: "https://foo.bar"}})
cl.EXPECT().Filtering().Return(rf, nil)
cl.EXPECT().DeleteFilter(false, model.Filter{Name: "foo", Url: "https://foo.bar"})
err := actionFilters(ac)
Ω(err).ShouldNot(HaveOccurred())
})
It("should update a filter", func() {
ac.origin.filters.Filters = utils.Ptr([]model.Filter{{Name: "foo", Url: "https://foo.bar", Enabled: true}})
rf.Filters = utils.Ptr([]model.Filter{{Name: "foo", Url: "https://foo.bar"}})
cl.EXPECT().Filtering().Return(rf, nil)
cl.EXPECT().UpdateFilter(false, model.Filter{Name: "foo", Url: "https://foo.bar", Enabled: true})
cl.EXPECT().RefreshFilters(gm.Any())
err := actionFilters(ac)
Ω(err).ShouldNot(HaveOccurred())
})
It("should abort after failed added filter", func() {
ac.continueOnError = false
ac.origin.filters.Filters = utils.Ptr([]model.Filter{{Name: "foo", Url: "https://foo.bar"}})
cl.EXPECT().Filtering().Return(rf, nil)
cl.EXPECT().AddFilter(false, model.Filter{Name: "foo", Url: "https://foo.bar"}).Return(errors.New("test failure"))
err := actionFilters(ac)
Ω(err).Should(HaveOccurred())
})
It("should continue after failed added filter", func() {
ac.continueOnError = true
ac.origin.filters.Filters = utils.Ptr([]model.Filter{{Name: "foo", Url: "https://foo.bar"}, {Name: "bar", Url: "https://bar.foo"}})
cl.EXPECT().Filtering().Return(rf, nil)
cl.EXPECT().AddFilter(false, model.Filter{Name: "foo", Url: "https://foo.bar"}).Return(errors.New("test failure"))
cl.EXPECT().AddFilter(false, model.Filter{Name: "bar", Url: "https://bar.foo"})
cl.EXPECT().RefreshFilters(gm.Any())
err := actionFilters(ac)
Ω(err).ShouldNot(HaveOccurred()) Ω(err).ShouldNot(HaveOccurred())
}) })
}) })
Context("syncDNS", func() { Context("actionDNSAccessLists", func() {
var ( var ral *model.AccessList
oal *model.AccessList
ral *model.AccessList
odc *model.DNSConfig
rdc *model.DNSConfig
)
BeforeEach(func() { BeforeEach(func() {
oal = &model.AccessList{} ac.origin.accessList = &model.AccessList{}
ral = &model.AccessList{} ral = &model.AccessList{}
odc = &model.DNSConfig{}
rdc = &model.DNSConfig{}
}) })
It("should have no changes", func() { It("should have no changes", func() {
cl.EXPECT().AccessList().Return(ral, nil) cl.EXPECT().AccessList().Return(ral, nil)
cl.EXPECT().DNSConfig().Return(rdc, nil) err := actionDNSAccessLists(ac)
err := w.syncDNS(oal, odc, cl)
Ω(err).ShouldNot(HaveOccurred()) Ω(err).ShouldNot(HaveOccurred())
}) })
It("should have access list changes", func() { It("should have access list changes", func() {
ral.BlockedHosts = utils.Ptr([]string{"foo"}) ral.BlockedHosts = utils.Ptr([]string{"foo"})
cl.EXPECT().AccessList().Return(ral, nil) cl.EXPECT().AccessList().Return(ral, nil)
cl.EXPECT().DNSConfig().Return(rdc, nil) cl.EXPECT().SetAccessList(ac.origin.accessList)
cl.EXPECT().SetAccessList(oal) err := actionDNSAccessLists(ac)
err := w.syncDNS(oal, odc, cl)
Ω(err).ShouldNot(HaveOccurred())
})
It("should have dns config changes", func() {
rdc.BootstrapDns = utils.Ptr([]string{"foo"})
cl.EXPECT().AccessList().Return(ral, nil)
cl.EXPECT().DNSConfig().Return(rdc, nil)
cl.EXPECT().SetDNSConfig(odc)
err := w.syncDNS(oal, odc, cl)
Ω(err).ShouldNot(HaveOccurred()) Ω(err).ShouldNot(HaveOccurred())
}) })
}) })
Context("syncDHCPServer", func() { Context("actionDNSServerConfig", func() {
var ( var rdc *model.DNSConfig
osc *model.DhcpStatus
rsc *model.DhcpStatus
)
BeforeEach(func() { BeforeEach(func() {
osc = &model.DhcpStatus{V4: &model.DhcpConfigV4{ ac.origin.dnsConfig = &model.DNSConfig{}
GatewayIp: utils.Ptr("1.2.3.4"), rdc = &model.DNSConfig{}
RangeStart: utils.Ptr("1.2.3.5"), })
RangeEnd: utils.Ptr("1.2.3.6"), It("should have no changes", func() {
SubnetMask: utils.Ptr("255.255.255.0"), cl.EXPECT().DNSConfig().Return(rdc, nil)
}} err := actionDNSServerConfig(ac)
Ω(err).ShouldNot(HaveOccurred())
})
It("should have dns config changes", func() {
rdc.BootstrapDns = utils.Ptr([]string{"foo"})
cl.EXPECT().DNSConfig().Return(rdc, nil)
cl.EXPECT().SetDNSConfig(ac.origin.dnsConfig)
err := actionDNSServerConfig(ac)
Ω(err).ShouldNot(HaveOccurred())
})
})
Context("actionDHCPServerConfig", func() {
var rsc *model.DhcpStatus
BeforeEach(func() {
ac.origin.dhcpServerConfig = &model.DhcpStatus{
V4: &model.DhcpConfigV4{
GatewayIp: utils.Ptr("1.2.3.4"),
RangeStart: utils.Ptr("1.2.3.5"),
RangeEnd: utils.Ptr("1.2.3.6"),
SubnetMask: utils.Ptr("255.255.255.0"),
},
}
rsc = &model.DhcpStatus{} rsc = &model.DhcpStatus{}
w.cfg.Features.DHCP.StaticLeases = false w.cfg.Features.DHCP.StaticLeases = false
}) })
It("should have no changes", func() { It("should have no changes", func() {
rsc.V4 = osc.V4 rsc.V4 = ac.origin.dhcpServerConfig.V4
cl.EXPECT().DhcpConfig().Return(rsc, nil) cl.EXPECT().DhcpConfig().Return(rsc, nil)
err := w.syncDHCPServer(osc, cl, types.AdGuardInstance{}) err := actionDHCPServerConfig(ac)
Ω(err).ShouldNot(HaveOccurred()) Ω(err).ShouldNot(HaveOccurred())
}) })
It("should have changes", func() { It("should have changes", func() {
rsc.Enabled = utils.Ptr(true) rsc.Enabled = utils.Ptr(true)
cl.EXPECT().DhcpConfig().Return(rsc, nil) cl.EXPECT().DhcpConfig().Return(rsc, nil)
cl.EXPECT().SetDhcpConfig(osc) cl.EXPECT().SetDhcpConfig(ac.origin.dhcpServerConfig)
err := w.syncDHCPServer(osc, cl, types.AdGuardInstance{}) err := actionDHCPServerConfig(ac)
Ω(err).ShouldNot(HaveOccurred()) Ω(err).ShouldNot(HaveOccurred())
}) })
It("should use replica interface name", func() { It("should use replica interface name", func() {
ac.replica.InterfaceName = "foo"
cl.EXPECT().DhcpConfig().Return(rsc, nil) cl.EXPECT().DhcpConfig().Return(rsc, nil)
oscClone := osc.Clone() oscClone := ac.origin.dhcpServerConfig.Clone()
oscClone.InterfaceName = utils.Ptr("foo") oscClone.InterfaceName = utils.Ptr("foo")
cl.EXPECT().SetDhcpConfig(oscClone) cl.EXPECT().SetDhcpConfig(oscClone)
err := w.syncDHCPServer(osc, cl, types.AdGuardInstance{InterfaceName: "foo"}) err := actionDHCPServerConfig(ac)
Ω(err).ShouldNot(HaveOccurred()) Ω(err).ShouldNot(HaveOccurred())
}) })
It("should enable the target dhcp server", func() { It("should enable the target dhcp server", func() {
ac.replica.DHCPServerEnabled = utils.Ptr(true)
cl.EXPECT().DhcpConfig().Return(rsc, nil) cl.EXPECT().DhcpConfig().Return(rsc, nil)
oscClone := osc.Clone() oscClone := ac.origin.dhcpServerConfig.Clone()
oscClone.Enabled = utils.Ptr(true) oscClone.Enabled = utils.Ptr(true)
cl.EXPECT().SetDhcpConfig(oscClone) cl.EXPECT().SetDhcpConfig(oscClone)
err := w.syncDHCPServer(osc, cl, types.AdGuardInstance{DHCPServerEnabled: &boolTrue}) err := actionDHCPServerConfig(ac)
Ω(err).ShouldNot(HaveOccurred())
})
It("should not sync empty IPv4", func() {
ac.replica.DHCPServerEnabled = utils.Ptr(false)
ac.origin.dhcpServerConfig.V4 = &model.DhcpConfigV4{
GatewayIp: utils.Ptr(""),
}
err := actionDHCPServerConfig(ac)
Ω(err).ShouldNot(HaveOccurred()) Ω(err).ShouldNot(HaveOccurred())
}) })
}) })
@@ -604,23 +622,12 @@ var _ = Describe("Sync", func() {
cl.EXPECT().AddRewriteEntries() cl.EXPECT().AddRewriteEntries()
cl.EXPECT().DeleteRewriteEntries() cl.EXPECT().DeleteRewriteEntries()
cl.EXPECT().Filtering().Return(&model.FilterStatus{}, nil) cl.EXPECT().Filtering().Return(&model.FilterStatus{}, nil)
cl.EXPECT().AddFilters(false)
cl.EXPECT().UpdateFilters(false)
cl.EXPECT().DeleteFilters(false)
cl.EXPECT().AddFilters(true)
cl.EXPECT().UpdateFilters(true)
cl.EXPECT().DeleteFilters(true)
cl.EXPECT().BlockedServices() cl.EXPECT().BlockedServices()
cl.EXPECT().BlockedServicesSchedule() cl.EXPECT().BlockedServicesSchedule()
cl.EXPECT().Clients().Return(&model.Clients{}, nil) cl.EXPECT().Clients().Return(&model.Clients{}, nil)
cl.EXPECT().AddClients()
cl.EXPECT().UpdateClients()
cl.EXPECT().DeleteClients()
cl.EXPECT().AccessList().Return(&model.AccessList{}, nil) cl.EXPECT().AccessList().Return(&model.AccessList{}, nil)
cl.EXPECT().DNSConfig().Return(&model.DNSConfig{}, nil) cl.EXPECT().DNSConfig().Return(&model.DNSConfig{}, nil)
cl.EXPECT().DhcpConfig().Return(&model.DhcpStatus{}, nil) cl.EXPECT().DhcpConfig().Return(&model.DhcpStatus{}, nil)
cl.EXPECT().AddDHCPStaticLeases().Return(nil)
cl.EXPECT().DeleteDHCPStaticLeases().Return(nil)
w.sync() w.sync()
}) })
It("should not sync DHCP", func() { It("should not sync DHCP", func() {
@@ -656,18 +663,9 @@ var _ = Describe("Sync", func() {
cl.EXPECT().AddRewriteEntries() cl.EXPECT().AddRewriteEntries()
cl.EXPECT().DeleteRewriteEntries() cl.EXPECT().DeleteRewriteEntries()
cl.EXPECT().Filtering().Return(&model.FilterStatus{}, nil) cl.EXPECT().Filtering().Return(&model.FilterStatus{}, nil)
cl.EXPECT().AddFilters(false)
cl.EXPECT().UpdateFilters(false)
cl.EXPECT().DeleteFilters(false)
cl.EXPECT().AddFilters(true)
cl.EXPECT().UpdateFilters(true)
cl.EXPECT().DeleteFilters(true)
cl.EXPECT().BlockedServices() cl.EXPECT().BlockedServices()
cl.EXPECT().BlockedServicesSchedule() cl.EXPECT().BlockedServicesSchedule()
cl.EXPECT().Clients().Return(&model.Clients{}, nil) cl.EXPECT().Clients().Return(&model.Clients{}, nil)
cl.EXPECT().AddClients()
cl.EXPECT().UpdateClients()
cl.EXPECT().DeleteClients()
cl.EXPECT().AccessList().Return(&model.AccessList{}, nil) cl.EXPECT().AccessList().Return(&model.AccessList{}, nil)
cl.EXPECT().DNSConfig().Return(&model.DNSConfig{}, nil) cl.EXPECT().DNSConfig().Return(&model.DNSConfig{}, nil)
w.sync() w.sync()

View File

@@ -4,33 +4,61 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
) )
func NewFeatures(enabled bool) Features {
return Features{
DNS: DNS{
AccessLists: enabled,
ServerConfig: enabled,
Rewrites: enabled,
},
DHCP: DHCP{
ServerConfig: enabled,
StaticLeases: enabled,
},
GeneralSettings: enabled,
QueryLogConfig: enabled,
StatsConfig: enabled,
ClientSettings: enabled,
Services: enabled,
Filters: enabled,
}
}
// Features feature flags // Features feature flags
type Features struct { type Features struct {
DNS DNS `json:"dns" yaml:"dns"` DNS DNS `json:"dns" yaml:"dns"`
DHCP DHCP `json:"dhcp" yaml:"dhcp"` DHCP DHCP `json:"dhcp" yaml:"dhcp"`
GeneralSettings bool `json:"generalSettings" yaml:"generalSettings"` GeneralSettings bool `json:"generalSettings" yaml:"generalSettings" env:"FEATURES_GENERAL_SETTINGS"`
QueryLogConfig bool `json:"queryLogConfig" yaml:"queryLogConfig"` QueryLogConfig bool `json:"queryLogConfig" yaml:"queryLogConfig" env:"FEATURES_QUERY_LOG_CONFIG"`
StatsConfig bool `json:"statsConfig" yaml:"statsConfig"` StatsConfig bool `json:"statsConfig" yaml:"statsConfig" env:"FEATURES_STATS_CONFIG"`
ClientSettings bool `json:"clientSettings" yaml:"clientSettings"` ClientSettings bool `json:"clientSettings" yaml:"clientSettings" env:"FEATURES_CLIENT_SETTINGS"`
Services bool `json:"services" yaml:"services"` Services bool `json:"services" yaml:"services" env:"FEATURES_SERVICES"`
Filters bool `json:"filters" yaml:"filters"` Filters bool `json:"filters" yaml:"filters" env:"FEATURES_FILTERS"`
} }
// DHCP features // DHCP features
type DHCP struct { type DHCP struct {
ServerConfig bool `json:"serverConfig" yaml:"serverConfig"` ServerConfig bool `json:"serverConfig" yaml:"serverConfig" env:"FEATURES_DHCP_SERVER_CONFIG"`
StaticLeases bool `json:"staticLeases" yaml:"staticLeases"` StaticLeases bool `json:"staticLeases" yaml:"staticLeases" env:"FEATURES_DHCP_STATIC_LEASES"`
} }
// DNS features // DNS features
type DNS struct { type DNS struct {
AccessLists bool `json:"accessLists" yaml:"accessLists"` AccessLists bool `json:"accessLists" yaml:"accessLists" env:"FEATURES_DNS_ACCESS_LISTS"`
ServerConfig bool `json:"serverConfig" yaml:"serverConfig"` ServerConfig bool `json:"serverConfig" yaml:"serverConfig" env:"FEATURES_DNS_SERVER_CONFIG"`
Rewrites bool `json:"rewrites" yaml:"rewrites"` Rewrites bool `json:"rewrites" yaml:"rewrites" env:"FEATURES_DNS_REWRITES"`
} }
// LogDisabled log all disabled features // LogDisabled log all disabled features
func (f *Features) LogDisabled(l *zap.SugaredLogger) { func (f *Features) LogDisabled(l *zap.SugaredLogger) {
features := f.collectDisabled()
if len(features) > 0 {
l.With("features", features).Info("Disabled features")
}
}
func (f *Features) collectDisabled() []string {
var features []string var features []string
if !f.DHCP.ServerConfig { if !f.DHCP.ServerConfig {
features = append(features, "DHCP.ServerConfig") features = append(features, "DHCP.ServerConfig")
@@ -65,8 +93,5 @@ func (f *Features) LogDisabled(l *zap.SugaredLogger) {
if !f.Filters { if !f.Filters {
features = append(features, "Filters") features = append(features, "Filters")
} }
return features
if len(features) > 0 {
l.With("features", features).Info("Disabled features")
}
} }

View File

@@ -2,6 +2,8 @@ package types
import ( import (
"fmt" "fmt"
"net/url"
"strings"
"go.uber.org/zap" "go.uber.org/zap"
) )
@@ -14,30 +16,44 @@ const (
// Config application configuration struct // Config application configuration struct
// +k8s:deepcopy-gen=true // +k8s:deepcopy-gen=true
type Config struct { type Config struct {
Origin AdGuardInstance `json:"origin" yaml:"origin"` Origin AdGuardInstance `json:"origin" yaml:"origin" env:"ORIGIN"`
Replica *AdGuardInstance `json:"replica,omitempty" yaml:"replica,omitempty"` Replica *AdGuardInstance `json:"replica,omitempty" yaml:"replica,omitempty" env:"REPLICA"`
Replicas []AdGuardInstance `json:"replicas,omitempty" yaml:"replicas,omitempty"` Replicas []AdGuardInstance `json:"replicas,omitempty" yaml:"replicas,omitempty"`
Cron string `json:"cron,omitempty" yaml:"cron,omitempty"` Cron string `json:"cron,omitempty" yaml:"cron,omitempty" env:"CRON"`
RunOnStart bool `json:"runOnStart,omitempty" yaml:"runOnStart,omitempty"` RunOnStart bool `json:"runOnStart,omitempty" yaml:"runOnStart,omitempty" env:"RUN_ON_START"`
API API `json:"api,omitempty" yaml:"api,omitempty"` PrintConfigOnly bool `json:"printConfigOnly,omitempty" yaml:"printConfigOnly,omitempty" env:"PRINT_CONFIG_ONLY"`
Features Features `json:"features,omitempty" yaml:"features,omitempty"` 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_"`
} }
// API configuration // API configuration
type API struct { type API struct {
Port int `json:"port,omitempty" yaml:"port,omitempty"` Port int `json:"port,omitempty" yaml:"port,omitempty" env:"API_PORT"`
Username string `json:"username,omitempty" yaml:"username,omitempty"` Username string `json:"username,omitempty" yaml:"username,omitempty" env:"API_USERNAME"`
Password string `json:"password,omitempty" yaml:"password,omitempty"` Password string `json:"password,omitempty" yaml:"password,omitempty" env:"API_PASSWORD"`
DarkMode bool `json:"darkMode,omitempty" yaml:"darkMode,omitempty"` DarkMode bool `json:"darkMode,omitempty" yaml:"darkMode,omitempty" env:"API_DARK_MODE"`
}
// 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 { func (cfg *Config) UniqueReplicas() []AdGuardInstance {
dedup := make(map[string]AdGuardInstance) dedup := make(map[string]AdGuardInstance)
if cfg.Replica != nil && cfg.Replica.URL != "" { if cfg.Replica != nil && cfg.Replica.URL != "" {
if cfg.Replica.APIPath == "" {
cfg.Replica.APIPath = DefaultAPIPath
}
dedup[cfg.Replica.Key()] = *cfg.Replica dedup[cfg.Replica.Key()] = *cfg.Replica
} }
for _, replica := range cfg.Replicas { for _, replica := range cfg.Replicas {
if replica.APIPath == "" {
replica.APIPath = DefaultAPIPath
}
if replica.URL != "" { if replica.URL != "" {
dedup[replica.Key()] = replica dedup[replica.Key()] = replica
} }
@@ -45,9 +61,6 @@ func (cfg *Config) UniqueReplicas() []AdGuardInstance {
var r []AdGuardInstance var r []AdGuardInstance
for _, replica := range dedup { for _, replica := range dedup {
if replica.APIPath == "" {
replica.APIPath = DefaultAPIPath
}
r = append(r, replica) r = append(r, replica)
} }
return r return r
@@ -55,6 +68,11 @@ func (cfg *Config) UniqueReplicas() []AdGuardInstance {
// Log the current config // Log the current config
func (cfg *Config) Log(l *zap.SugaredLogger) { func (cfg *Config) Log(l *zap.SugaredLogger) {
c := cfg.mask()
l.With("config", c).Debug("Using config")
}
func (cfg *Config) mask() *Config {
c := cfg.DeepCopy() c := cfg.DeepCopy()
c.Origin.Mask() c.Origin.Mask()
if c.Replica != nil { if c.Replica != nil {
@@ -67,21 +85,39 @@ func (cfg *Config) Log(l *zap.SugaredLogger) {
for i := range c.Replicas { for i := range c.Replicas {
c.Replicas[i].Mask() c.Replicas[i].Mask()
} }
l.With("config", c).Debug("Using config") c.API.Mask()
return c
}
func (cfg *Config) Init() error {
if err := cfg.Origin.Init(); err != nil {
return err
}
for i := range cfg.Replicas {
replica := &cfg.Replicas[i]
if err := replica.Init(); err != nil {
return err
}
}
return nil
} }
// AdGuardInstance AdguardHome config instance // AdGuardInstance AdguardHome config instance
// +k8s:deepcopy-gen=true // +k8s:deepcopy-gen=true
type AdGuardInstance struct { type AdGuardInstance struct {
URL string `json:"url" yaml:"url"` URL string `json:"url" yaml:"url" env:"URL"`
APIPath string `json:"apiPath,omitempty" yaml:"apiPath,omitempty"` WebURL string `json:"webURL" yaml:"webURL" env:"WEB_URL"`
Username string `json:"username,omitempty" yaml:"username,omitempty"` APIPath string `json:"apiPath,omitempty" yaml:"apiPath,omitempty" env:"API_PATH"`
Password string `json:"password,omitempty" yaml:"password,omitempty"` Username string `json:"username,omitempty" yaml:"username,omitempty" env:"USERNAME"`
Cookie string `json:"cookie,omitempty" yaml:"cookie,omitempty"` Password string `json:"password,omitempty" yaml:"password,omitempty" env:"PASSWORD"`
InsecureSkipVerify bool `json:"insecureSkipVerify" yaml:"insecureSkipVerify"` Cookie string `json:"cookie,omitempty" yaml:"cookie,omitempty" env:"COOKIE"`
AutoSetup bool `json:"autoSetup" yaml:"autoSetup"` InsecureSkipVerify bool `json:"insecureSkipVerify" yaml:"insecureSkipVerify" env:"INSECURE_SKIP_VERIFY"`
InterfaceName string `json:"interfaceName" yaml:"interfaceName"` AutoSetup bool `json:"autoSetup" yaml:"autoSetup" env:"AUTO_SETUP"`
DHCPServerEnabled *bool `json:"dhcpServerEnabled,omitempty" yaml:"dhcpServerEnabled,omitempty"` InterfaceName string `json:"interfaceName,omitempty" yaml:"interfaceName,omitempty" env:"INTERFACE_NAME"`
DHCPServerEnabled *bool `json:"dhcpServerEnabled,omitempty" yaml:"dhcpServerEnabled,omitempty" env:"DHCP_SERVER_ENABLED"`
Host string `json:"-" yaml:"-"`
WebHost string `json:"-" yaml:"-"`
} }
// Key AdGuardInstance key // Key AdGuardInstance key
@@ -95,11 +131,32 @@ func (i *AdGuardInstance) Mask() {
i.Password = mask(i.Password) i.Password = mask(i.Password)
} }
func (i *AdGuardInstance) Init() error {
u, err := url.Parse(i.URL)
if err != nil {
return err
}
i.Host = u.Host
if i.WebURL == "" {
i.WebHost = i.Host
i.WebURL = i.URL
} else {
u, err := url.Parse(i.WebURL)
if err != nil {
return err
}
i.WebHost = u.Host
}
return nil
}
func mask(s string) string { func mask(s string) string {
if s == "" { if s == "" {
return "***" return "***"
} }
return fmt.Sprintf("%v***%v", string(s[0]), string(s[len(s)-1])) 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

View File

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

99
pkg/types/types_test.go Normal file
View File

@@ -0,0 +1,99 @@
package types
import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("Types", func() {
Context("AdGuardInstance", func() {
var inst AdGuardInstance
BeforeEach(func() {
inst = AdGuardInstance{}
})
Context("Instance Init", func() {
BeforeEach(func() {
inst.URL = "https://localhost:3000"
})
It("should correctly set Host and WebHost if only URL is set", func() {
err := inst.Init()
Ω(err).ShouldNot(HaveOccurred())
Ω(inst.Host).Should(Equal("localhost:3000"))
Ω(inst.WebHost).Should(Equal("localhost:3000"))
Ω(inst.URL).Should(Equal("https://localhost:3000"))
Ω(inst.WebURL).Should(Equal("https://localhost:3000"))
})
It("should correctly set Host and WebHost if URL and WebURL are set", func() {
inst.WebURL = "https://127.0.0.1:4000"
err := inst.Init()
Ω(err).ShouldNot(HaveOccurred())
Ω(inst.Host).Should(Equal("localhost:3000"))
Ω(inst.WebHost).Should(Equal("127.0.0.1:4000"))
Ω(inst.WebURL).Should(Equal(inst.WebURL))
Ω(inst.URL).Should(Equal("https://localhost:3000"))
Ω(inst.WebURL).Should(Equal("https://127.0.0.1:4000"))
})
})
})
Context("Config", func() {
Context("init", func() {
cfg := Config{
Replicas: []AdGuardInstance{
{URL: "https://localhost:3000"},
},
}
err := cfg.Init()
Ω(err).ShouldNot(HaveOccurred())
Ω(cfg.Replicas[0].Host).Should(Equal("localhost:3000"))
Ω(cfg.Replicas[0].WebHost).Should(Equal("localhost:3000"))
Ω(cfg.Replicas[0].URL).Should(Equal("https://localhost:3000"))
Ω(cfg.Replicas[0].WebURL).Should(Equal("https://localhost:3000"))
})
Context("UniqueReplicas", func() {
It("should return unique replicas in the array", func() {
cfg := Config{
Replicas: []AdGuardInstance{
{URL: "a"},
{URL: "a", APIPath: DefaultAPIPath},
{URL: "a", APIPath: "foo"},
{URL: "b", APIPath: DefaultAPIPath},
},
Replica: &AdGuardInstance{URL: "b"},
}
replicas := cfg.UniqueReplicas()
Ω(replicas).Should(HaveLen(3))
})
})
Context("mask", func() {
It("should mask all names and passwords", func() {
cfg := Config{
Replicas: []AdGuardInstance{
{URL: "a", Username: "user", Password: "pass"},
},
Replica: &AdGuardInstance{URL: "a", Username: "user", Password: "pass"},
API: API{Username: "user", Password: "pass"},
}
masked := cfg.mask()
Ω(masked.Replicas[0].Username).Should(Equal("u**r"))
Ω(masked.Replicas[0].Password).Should(Equal("p**s"))
Ω(masked.Replica.Username).Should(Equal("u**r"))
Ω(masked.Replica.Password).Should(Equal("p**s"))
Ω(masked.API.Username).Should(Equal("u**r"))
Ω(masked.API.Password).Should(Equal("p**s"))
})
})
})
Context("Feature", func() {
Context("LogDisabled", func() {
It("should log all features", func() {
f := NewFeatures(false)
Ω(f.collectDisabled()).Should(HaveLen(11))
})
It("should log no features", func() {
f := NewFeatures(true)
Ω(f.collectDisabled()).Should(BeEmpty())
})
})
})
})

2
testdata/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
docker-compose.yaml
!config_test_*.yaml

37
testdata/config_test_replica.yaml vendored Normal file
View File

@@ -0,0 +1,37 @@
origin:
url: https://origin-file:443
webURL: https://origin-file:443
apiPath: /control
username: foo
password: '*********'
insecureSkipVerify: true
autoSetup: false
replica:
url: https://replica-file:443
webURL: https://replica-file:443
apiPath: /control
username: bar
password: '*********'
insecureSkipVerify: false
autoSetup: false
interfaceName: eth3
dhcpServerEnabled: false
cron: '*/15 * * * *'
runOnStart: true
printConfigOnly: true
api:
port: 9090
features:
dns:
accessLists: true
serverConfig: false
rewrites: true
dhcp:
serverConfig: true
staticLeases: true
generalSettings: true
queryLogConfig: true
statsConfig: true
clientSettings: true
services: true
filters: true

37
testdata/config_test_replicas.yaml vendored Normal file
View File

@@ -0,0 +1,37 @@
origin:
url: https://origin-file:443
webURL: https://origin-file:443
apiPath: /control
username: foo
password: '*********'
insecureSkipVerify: true
autoSetup: false
replicas:
- url: https://replica-file:443
webURL: https://replica-file:443
apiPath: /control
username: bar
password: '*********'
insecureSkipVerify: false
autoSetup: false
interfaceName: eth3
dhcpServerEnabled: false
cron: '*/15 * * * *'
runOnStart: true
printConfigOnly: true
api:
port: 9090
features:
dns:
accessLists: true
serverConfig: false
rewrites: true
dhcp:
serverConfig: true
staticLeases: true
generalSettings: true
queryLogConfig: true
statsConfig: true
clientSettings: true
services: true
filters: true

View File

@@ -0,0 +1,47 @@
origin:
url: https://origin-file:443
webURL: https://origin-file:443
apiPath: /control
username: foo
password: '*********'
insecureSkipVerify: true
autoSetup: false
replica:
url: https://replica-file:443
webURL: https://replica-file:443
apiPath: /control
username: bar
password: '*********'
insecureSkipVerify: false
autoSetup: false
interfaceName: eth3
dhcpServerEnabled: false
replicas:
- url: https://replicas-file:443
webURL: https://replicas-file:443
apiPath: /control
username: bar
password: '*********'
insecureSkipVerify: false
autoSetup: false
interfaceName: eth3
dhcpServerEnabled: false
cron: '*/15 * * * *'
runOnStart: true
printConfigOnly: true
api:
port: 9090
features:
dns:
accessLists: true
serverConfig: false
rewrites: true
dhcp:
serverConfig: true
staticLeases: true
generalSettings: true
queryLogConfig: true
statsConfig: true
clientSettings: true
services: true
filters: true

View File

@@ -6,4 +6,4 @@ kubectl config set-context --current --namespace=agh-e2e
if [[ $(helm list --no-headers -n agh-e2e | grep agh-e2e | wc -l) == "1" ]]; then if [[ $(helm list --no-headers -n agh-e2e | grep agh-e2e | wc -l) == "1" ]]; then
helm delete agh-e2e -n agh-e2e --wait helm delete agh-e2e -n agh-e2e --wait
fi fi
helm install agh-e2e testdata/e2e -n agh-e2e --create-namespace helm install agh-e2e testdata/e2e -n agh-e2e --create-namespace --set mode=${1}

View File

@@ -1,4 +1,8 @@
#!/bin/bash #!/bin/bash
set -e
kubectl wait --for=jsonpath='{.status.phase}'=Succeeded pod/adguardhome-sync --timeout=1m kubectl wait --for=jsonpath='{.status.phase}'=Succeeded pod/adguardhome-sync --timeout=1m
RESULT=$?
if [[ "${RESULT}" != "0" ]]; then
kubectl logs adguardhome-sync
fi
exit ${RESULT}

View File

@@ -0,0 +1,18 @@
{{- if eq .Values.mode "env" }}
apiVersion: v1
kind: ConfigMap
metadata:
name: sync-conf
namespace: {{ .Release.Namespace }}
data:
API_PORT: '0'
ORIGIN_URL: 'http://service-origin.{{ $.Release.Namespace }}.svc.cluster.local:3000'
ORIGIN_PASSWORD: 'password'
ORIGIN_USERNAME: 'username'
{{- range $i,$version := .Values.replica.versions }}
REPLICA{{ add 1 $i }}_AUTO_SETUP: 'true'
REPLICA{{ add 1 $i }}_URL: 'http://service-replica-{{ $version | toString | replace "." "-" }}.{{ $.Release.Namespace }}.svc.cluster.local:3000'
REPLICA{{ add 1 $i }}_PASSWORD: 'password'
REPLICA{{ add 1 $i }}_USERNAME: 'username'
{{- end }}
{{- end }}

View File

@@ -0,0 +1,22 @@
{{- if eq .Values.mode "file" }}
apiVersion: v1
kind: ConfigMap
metadata:
name: sync-conf
namespace: {{ .Release.Namespace }}
data:
config.yaml: |
origin:
url: 'http://service-origin.{{ $.Release.Namespace }}.svc.cluster.local:3000'
username: username
password: password
api:
port: 0
replicas:
{{- range $i,$version := .Values.replica.versions }}
- url: 'http://service-replica-{{ $version | toString | replace "." "-" }}.{{ $.Release.Namespace }}.svc.cluster.local:3000'
username: username
password: password
autoSetup: true
{{- end }}
{{- end }}

View File

@@ -1,17 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: sync-conf
namespace: {{ .Release.Namespace }}
data:
API_PORT: '0'
LOG_LEVEL: 'info'
ORIGIN_URL: 'http://service-origin.{{ $.Release.Namespace }}.svc.cluster.local:3000'
ORIGIN_PASSWORD: 'password'
ORIGIN_USERNAME: 'username'
{{ range $i,$version := .Values.replica.versions }}
REPLICA{{ $i }}_AUTOSETUP: 'true'
REPLICA{{ $i }}_URL: 'http://service-replica-{{ $version | toString | replace "." "-" }}.{{ $.Release.Namespace }}.svc.cluster.local:3000'
REPLICA{{ $i }}_PASSWORD: 'password'
REPLICA{{ $i }}_USERNAME: 'username'
{{- end }}

View File

@@ -1,3 +1,4 @@
{{- if eq .Values.mode "env" }}
apiVersion: v1 apiVersion: v1
kind: Pod kind: Pod
metadata: metadata:
@@ -7,7 +8,7 @@ spec:
serviceAccountName: agh-e2e serviceAccountName: agh-e2e
initContainers: initContainers:
- name: wait-for-others - name: wait-for-others
image: bitnami/kubectl:1.24 image: {{ .Values.kubectl.repository }}:{{ .Values.kubectl.tag }}
command: command:
- /bin/bash - /bin/bash
- -c - -c
@@ -19,7 +20,11 @@ spec:
command: command:
- /opt/go/adguardhome-sync - /opt/go/adguardhome-sync
- run - run
env:
- name: LOG_LEVEL
value: 'debug'
envFrom: envFrom:
- configMapRef: - configMapRef:
name: sync-conf name: sync-conf
restartPolicy: Never restartPolicy: Never
{{- end }}

View File

@@ -0,0 +1,36 @@
{{- if eq .Values.mode "file" }}
apiVersion: v1
kind: Pod
metadata:
name: adguardhome-sync
namespace: {{ $.Release.Namespace }}
spec:
serviceAccountName: agh-e2e
initContainers:
- name: wait-for-others
image: {{ .Values.kubectl.repository }}:{{ .Values.kubectl.tag }}
command:
- /bin/bash
- -c
- |
{{- .Files.Get "bin/wait-for-agh-pods.sh" | nindent 10}}
containers:
- name: adguardhome-sync
image: localhost:5001/adguardhome-sync:e2e
command:
- /opt/go/adguardhome-sync
- run
- '--config'
- /etc/go/adguardhome-sync/config.yaml
env:
- name: LOG_LEVEL
value: 'debug'
volumeMounts:
- name: config
mountPath: /etc/go/adguardhome-sync/
volumes:
- name: config
configMap:
name: sync-conf
restartPolicy: Never
{{- end }}

View File

@@ -1,4 +1,11 @@
replica: replica:
versions: versions:
- v0.107.40 - v0.107.40
- v0.107.43
- latest - latest
mode: env
kubectl:
repository: bitnami/kubectl
tag: 1.27

14
tools.go Normal file
View File

@@ -0,0 +1,14 @@
//go:build tools
// +build tools
package tools
import (
_ "github.com/bakito/semver"
_ "github.com/deepmap/oapi-codegen/v2/cmd/oapi-codegen"
_ "github.com/golangci/golangci-lint/cmd/golangci-lint"
_ "github.com/goreleaser/goreleaser"
_ "github.com/onsi/ginkgo/v2/ginkgo"
_ "go.uber.org/mock/mockgen"
_ "k8s.io/code-generator/cmd/deepcopy-gen"
)