feat: generate yaml config docs (#696)

This commit is contained in:
Marc Brugger
2025-11-11 20:40:57 +01:00
committed by GitHub
parent dde2356599
commit afee473692
6 changed files with 268 additions and 197 deletions

View File

@@ -1,4 +1,3 @@
## toolbox - start
## Generated with https://github.com/bakito/toolbox

289
README.md
View File

@@ -3,8 +3,6 @@
[![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)
# <img src="./media/adguardhome-sync.svg" alt="AdGuardHome sync" width="50"/> AdGuardHome sync
Synchronize [AdGuardHome](https://github.com/AdguardTeam/AdGuardHome) config to replica instances.
@@ -45,7 +43,144 @@ go install github.com/bakito/adguardhome-sync@latest
## Prerequisites
Both the origin instance and replica(s) must be initially set up with AdguardHome via the AdguardHome installation wizard.
Both the origin instance and replica(s) must be initially set up with AdguardHome via the AdguardHome installation
wizard.
## Config via environment variables
For Replicas replace `#` with the index number for the replica. E.g.: `REPLICA#_URL` -> `REPLICA1_URL`
<!-- env-doc-start -->
| Name | Type | Description |
| :--- | ---- |:----------- |
| CRON (string) | string | Cron expression for the sync interval |
| RUN_ON_START (bool) | bool | Run the sync on startup |
| PRINT_CONFIG_ONLY (bool) | bool | Print current config only and stop the application |
| CONTINUE_ON_ERROR (bool) | bool | Continue sync on errors |
| ORIGIN_URL (string) | string | URL of adguardhome instance |
| ORIGIN_WEB_URL (string) | string | Web URL of adguardhome instance |
| ORIGIN_API_PATH (string) | string | API Path |
| ORIGIN_USERNAME (string) | string | Adguardhome username |
| ORIGIN_PASSWORD (string) | string | Adguardhome password |
| ORIGIN_COOKIE (string) | string | Adguardhome cookie |
| ORIGIN_REQUEST_HEADERS (map) | map | Request Headers 'key1:value1,key2:value2' |
| ORIGIN_INSECURE_SKIP_VERIFY (bool) | bool | Skip TLS verification |
| ORIGIN_AUTO_SETUP (bool) | bool | Automatically setup the instance if it is not initialized |
| ORIGIN_INTERFACE_NAME (string) | string | Network interface name |
| ORIGIN_DHCP_SERVER_ENABLED (bool) | bool | Enable DHCP server |
| REPLICA#_URL (string) | string | URL of adguardhome instance |
| REPLICA#_WEB_URL (string) | string | Web URL of adguardhome instance |
| REPLICA#_API_PATH (string) | string | API Path |
| REPLICA#_USERNAME (string) | string | Adguardhome username |
| REPLICA#_PASSWORD (string) | string | Adguardhome password |
| REPLICA#_COOKIE (string) | string | Adguardhome cookie |
| REPLICA#_REQUEST_HEADERS (map) | map | Request Headers 'key1:value1,key2:value2' |
| REPLICA#_INSECURE_SKIP_VERIFY (bool) | bool | Skip TLS verification |
| REPLICA#_AUTO_SETUP (bool) | bool | Automatically setup the instance if it is not initialized |
| REPLICA#_INTERFACE_NAME (string) | string | Network interface name |
| REPLICA#_DHCP_SERVER_ENABLED (bool) | bool | Enable DHCP server |
| API_PORT (int) | int | API port (API is disabled if port is set to 0) |
| API_USERNAME (string) | string | API username |
| API_PASSWORD (string) | string | API password |
| API_DARK_MODE (bool) | bool | API dark mode |
| API_METRICS_ENABLED (bool) | bool | Enable metrics |
| API_METRICS_SCRAPE_INTERVAL (int64) | int64 | Interval for metrics scraping |
| API_METRICS_QUERY_LOG_LIMIT (int) | int | Metrics log query limit |
| API_TLS_CERT_DIR (string) | string | API TLS certificate directory |
| API_TLS_CERT_NAME (string) | string | API TLS certificate file name |
| API_TLS_KEY_NAME (string) | string | API TLS key file name |
| FEATURES_DNS_ACCESS_LISTS (bool) | bool | Sync DNS access lists |
| FEATURES_DNS_SERVER_CONFIG (bool) | bool | Sync DNS server config |
| FEATURES_DNS_REWRITES (bool) | bool | Sync DNS rewrites |
| FEATURES_DHCP_SERVER_CONFIG (bool) | bool | Sync DHCP server config |
| FEATURES_DHCP_STATIC_LEASES (bool) | bool | Sync DHCP static leases |
| FEATURES_GENERAL_SETTINGS (bool) | bool | Sync general settings |
| FEATURES_QUERY_LOG_CONFIG (bool) | bool | Sync query log config |
| FEATURES_STATS_CONFIG (bool) | bool | Sync stats config |
| FEATURES_CLIENT_SETTINGS (bool) | bool | Sync client settings |
| FEATURES_SERVICES (bool) | bool | Sync services |
| FEATURES_FILTERS (bool) | bool | Sync filters |
| FEATURES_THEME (bool) | bool | Sync the web UI theme |
| FEATURES_TLS_CONFIG (bool) | bool | Sync the TLS config |
<!-- env-doc-end -->
### YAML Configuration file
location: $HOME/.adguardhome-sync.yaml
<!-- yaml-doc-start -->
```yaml
cron: # (string) Cron expression for the sync interval
runOnStart: # (bool) Run the sync on startup
printConfigOnly: # (bool) Print current config only and stop the application
continueOnError: # (bool) Continue sync on errors
origin: # (struct) Origin instance
url: # (string) URL of adguardhome instance
webURL: # (string) Web URL of adguardhome instance
apiPath: # (string) API Path
username: # (string) Adguardhome username
password: # (string) Adguardhome password
cookie: # (string) Adguardhome cookie
requestHeaders: # (map) Request Headers 'key1:value1,key2:value2'
insecureSkipVerify: # (bool) Skip TLS verification
autoSetup: # (bool) Automatically setup the instance if it is not initialized
interfaceName: # (string) Network interface name
dhcpServerEnabled: # (bool) Enable DHCP server
replica: # (struct) Single or replica instance (don't use in combination with replicas')
url: # (string) URL of adguardhome instance
webURL: # (string) Web URL of adguardhome instance
apiPath: # (string) API Path
username: # (string) Adguardhome username
password: # (string) Adguardhome password
cookie: # (string) Adguardhome cookie
requestHeaders: # (map) Request Headers 'key1:value1,key2:value2'
insecureSkipVerify: # (bool) Skip TLS verification
autoSetup: # (bool) Automatically setup the instance if it is not initialized
interfaceName: # (string) Network interface name
dhcpServerEnabled: # (bool) Enable DHCP server
replicas: # (struct) List or replica instances (don't use in combination with replicas')
- url: # (string) URL of adguardhome instance
webURL: # (string) Web URL of adguardhome instance
apiPath: # (string) API Path
username: # (string) Adguardhome username
password: # (string) Adguardhome password
cookie: # (string) Adguardhome cookie
requestHeaders: # (map) Request Headers 'key1:value1,key2:value2'
insecureSkipVerify: # (bool) Skip TLS verification
autoSetup: # (bool) Automatically setup the instance if it is not initialized
interfaceName: # (string) Network interface name
dhcpServerEnabled: # (bool) Enable DHCP server
api: # (struct)
port: # (int) API port (API is disabled if port is set to 0)
username: # (string) API username
password: # (string) API password
darkMode: # (bool) API dark mode
metrics: # (struct)
enabled: # (bool) Enable metrics
scrapeInterval: # (int64) Interval for metrics scraping
queryLogLimit: # (int) Metrics log query limit
tls: # (struct)
certDir: # (string) API TLS certificate directory
certName: # (string) API TLS certificate file name
keyName: # (string) API TLS key file name
features: # (struct)
dns: # (struct)
accessLists: # (bool) Sync DNS access lists
serverConfig: # (bool) Sync DNS server config
rewrites: # (bool) Sync DNS rewrites
dhcp: # (struct)
serverConfig: # (bool) Sync DHCP server config
staticLeases: # (bool) Sync DHCP static leases
generalSettings: # (bool) Sync general settings
queryLogConfig: # (bool) Sync query log config
statsConfig: # (bool) Sync stats config
clientSettings: # (bool) Sync client settings
services: # (bool) Sync services
filters: # (bool) Sync filters
theme: # (bool) Sync the web UI theme
tlsConfig: # (bool) Sync the TLS config
```
<!-- yaml-doc-end -->
## Username / Password vs. Cookie
@@ -183,153 +318,13 @@ services:
restart: unless-stopped
```
## Config via environment variables
## Unraid
For Replicas replace `#` with the index number for the replica. E.g.: `REPLICA#_URL` -> `REPLICA1_URL`
<!-- env-doc-start -->
| Name | Type | Description |
| :--- | ---- |:----------- |
| ORIGIN_URL (string) | string | URL of adguardhome instance |
| ORIGIN_WEB_URL (string) | string | Web URL of adguardhome instance |
| ORIGIN_API_PATH (string) | string | API Path |
| ORIGIN_USERNAME (string) | string | Adguardhome username |
| ORIGIN_PASSWORD (string) | string | Adguardhome password |
| ORIGIN_COOKIE (string) | string | Adguardhome cookie |
| ORIGIN_REQUEST_HEADERS (map) | map | Request Headers 'key1:value1,key2:value2' |
| ORIGIN_INSECURE_SKIP_VERIFY (bool) | bool | Skip TLS verification |
| ORIGIN_AUTO_SETUP (bool) | bool | Automatically setup the instance if it is not initialized |
| ORIGIN_INTERFACE_NAME (string) | string | Network interface name |
| ORIGIN_DHCP_SERVER_ENABLED (bool) | bool | Enable DHCP server |
| REPLICA#_URL (string) | string | URL of adguardhome instance |
| REPLICA#_WEB_URL (string) | string | Web URL of adguardhome instance |
| REPLICA#_API_PATH (string) | string | API Path |
| REPLICA#_USERNAME (string) | string | Adguardhome username |
| REPLICA#_PASSWORD (string) | string | Adguardhome password |
| REPLICA#_COOKIE (string) | string | Adguardhome cookie |
| REPLICA#_REQUEST_HEADERS (map) | map | Request Headers 'key1:value1,key2:value2' |
| REPLICA#_INSECURE_SKIP_VERIFY (bool) | bool | Skip TLS verification |
| REPLICA#_AUTO_SETUP (bool) | bool | Automatically setup the instance if it is not initialized |
| REPLICA#_INTERFACE_NAME (string) | string | Network interface name |
| REPLICA#_DHCP_SERVER_ENABLED (bool) | bool | Enable DHCP server |
| CRON (string) | string | Cron expression for the sync interval |
| RUN_ON_START (bool) | bool | Run the sync on startup |
| PRINT_CONFIG_ONLY (bool) | bool | Print current config only and stop the application |
| CONTINUE_ON_ERROR (bool) | bool | Continue sync on errors |
| API_PORT (int) | int | API port (API is disabled if port is set to 0) |
| API_USERNAME (string) | string | API username |
| API_PASSWORD (string) | string | API password |
| API_DARK_MODE (bool) | bool | API dark mode |
| API_METRICS_ENABLED (bool) | bool | Enable metrics |
| API_METRICS_SCRAPE_INTERVAL (int64) | int64 | Interval for metrics scraping |
| API_METRICS_QUERY_LOG_LIMIT (int) | int | Metrics log query limit |
| API_TLS_CERT_DIR (string) | string | API TLS certificate directory |
| API_TLS_CERT_NAME (string) | string | API TLS certificate file name |
| API_TLS_KEY_NAME (string) | string | API TLS key file name |
| FEATURES_DNS_ACCESS_LISTS (bool) | bool | Sync DNS access lists |
| FEATURES_DNS_SERVER_CONFIG (bool) | bool | Sync DNS server config |
| FEATURES_DNS_REWRITES (bool) | bool | Sync DNS rewrites |
| FEATURES_DHCP_SERVER_CONFIG (bool) | bool | Sync DHCP server config |
| FEATURES_DHCP_STATIC_LEASES (bool) | bool | Sync DHCP static leases |
| FEATURES_GENERAL_SETTINGS (bool) | bool | Sync general settings |
| FEATURES_QUERY_LOG_CONFIG (bool) | bool | Sync query log config |
| FEATURES_STATS_CONFIG (bool) | bool | Sync stats config |
| FEATURES_CLIENT_SETTINGS (bool) | bool | Sync client settings |
| FEATURES_SERVICES (bool) | bool | Sync services |
| FEATURES_FILTERS (bool) | bool | Sync filters |
| FEATURES_THEME (bool) | bool | Sync the web UI theme |
| FEATURES_TLS_CONFIG (bool) | bool | Sync the TLS config |
<!-- env-doc-end -->
### Unraid
⚠️ Disclaimer: Tere exists an unraid tepmlate for this application. This template is not managed by this project.
⚠️ Disclaimer: There exists an unraid template for this application. This project does not manage this template.
Also, as unraid is not known to me, I cannot give any support on unraind templates.
Note when running the Docker container in Unraid please remove unneeded env variables if don't needed.
If replica2 isn't used this can cause sync errors.
### Config file
location: $HOME/.adguardhome-sync.yaml
```yaml
# cron expression to run in daemon mode. (default; "" = runs only once)
cron: "0 */2 * * *"
# runs the synchronisation on startup
runOnStart: true
# If enabled, the synchronisation task will not fail on single errors, but will log the errors and continue
continueOnError: false
origin:
# url of the origin instance
url: https://192.168.1.2:3000
# apiPath: define an api path if other than "/control"
# insecureSkipVerify: true # disable tls check
username: username
password: password
# cookie: Origin-Cookie-Name=CCCOOOKKKIIIEEE
# requestHeaders: # Additional request headers
# AAA: bbb
# replicas instances
replicas:
# url of the replica instance
- url: http://192.168.1.3
username: username
password: password
# cookie: Replica1-Cookie-Name=CCCOOOKKKIIIEEE
- url: http://192.168.1.4
username: username
password: password
# cookie: Replica2-Cookie-Name=CCCOOOKKKIIIEEE
# autoSetup: true # if true, AdGuardHome is automatically initialized.
# webURL: "https://some-other.url" # used in the web interface (default: <replica-url>
# requestHeaders: # Additional request headers
# AAA: bbb
# Configure the sync API server, disabled if api port is 0
api:
# Port, default 8080
port: 8080
# if username and password are defined, basic auth is applied to the sync API
username: username
password: password
# enable api dark mode
darkMode: true
# enable metrics on path '/metrics' (api port must be != 0)
# metrics:
# enabled: true
# scrapeInterval: 30s
# queryLogLimit: 10000
# enable tls for the api server
# tls:
# # the directory of the provided tls certs
# certDir: /path/to/certs
# # the name of the cert file (default: tls.crt)
# certName: foo.crt
# # the name of the key file (default: tls.key)
# keyName: bar.key
# Configure sync features; by default all features are enabled.
features:
generalSettings: true
queryLogConfig: true
statsConfig: true
clientSettings: true
services: true
filters: true
dhcp:
serverConfig: true
staticLeases: true
dns:
serverConfig: true
accessLists: true
rewrites: true
```
Note when running the Docker container in Unraid please remove unneeded env variables.
If replica2 isn't used, this can cause sync errors.
## Home Assistant AdGuard Home Add-on users

View File

@@ -4,7 +4,7 @@ package main
import (
"fmt"
"io"
"log"
"log/slog"
"os"
"reflect"
"strings"
@@ -12,45 +12,68 @@ import (
"github.com/bakito/adguardhome-sync/internal/types"
)
const (
envStartMarker = "<!-- env-doc-start -->"
envEndMarker = "<!-- env-doc-end -->"
yamlStartMarker = "<!-- yaml-doc-start -->"
yamlEndMarker = "<!-- yaml-doc-end -->"
)
func main() {
// Read the README.md file
slog.Info("Reading README.md")
content, err := os.ReadFile("README.md")
if err != nil {
log.Fatal(err)
slog.Error("Error reading README.md", "error", err)
os.Exit(1)
}
// Convert to string for easier manipulation
fileContent := string(content)
// Generate the environment variables documentation
slog.Info("Generating environment variables")
fileContent = generateEnvDocumentation(fileContent)
slog.Info("Generating yaml configuration")
fileContent = generateYAMLDocumentation(fileContent)
slog.Info("Writing README.md")
err = os.WriteFile("README.md", []byte(fileContent), 0o644)
if err != nil {
slog.Error("Error writing README.md", "error", err)
os.Exit(1)
}
}
func generateEnvDocumentation(fileContent string) string {
var buf strings.Builder
_, _ = buf.WriteString("| Name | Type | Description |\n")
_, _ = buf.WriteString("| :--- | ---- |:----------- |\n")
printEnvTags(&buf, reflect.TypeOf(types.Config{}), "")
writeEnvDocumentation(&buf, reflect.TypeOf(types.Config{}), "")
// Find the markers and replace content between them
startMarker := "<!-- env-doc-start -->"
endMarker := "<!-- env-doc-end -->"
start := strings.Index(fileContent, startMarker)
end := strings.Index(fileContent, endMarker)
if start == -1 || end == -1 {
log.Fatal("Could not find markers in README.md")
return updateDocumentationSection(fileContent, envStartMarker, envEndMarker, buf.String())
}
// Construct new content
newContent := fileContent[:start+len(startMarker)] + "\n" + buf.String() + fileContent[end:]
func generateYAMLDocumentation(fileContent string) string {
var buf strings.Builder
_, _ = buf.WriteString("```yaml\n")
writeYAMLDocumentation(&buf, reflect.TypeOf(types.Config{}), "", "")
_, _ = buf.WriteString("```\n")
// Write back to README.md
err = os.WriteFile("README.md", []byte(newContent), 0o644)
if err != nil {
log.Fatal(err)
}
return updateDocumentationSection(fileContent, yamlStartMarker, yamlEndMarker, buf.String())
}
// printEnvTags recursively prints all fields with `env` tags.
func printEnvTags(w io.Writer, t reflect.Type, prefix string) {
func updateDocumentationSection(fileContent, startMarker, endMarker, newContent string) string {
startIdx := strings.Index(fileContent, startMarker)
endIdx := strings.Index(fileContent, endMarker)
if startIdx == -1 || endIdx == -1 {
slog.Error(fmt.Sprintf("Could not find markers %s and %s in README.md", startMarker, endMarker))
os.Exit(1)
}
return fileContent[:startIdx+len(startMarker)] + "\n" + newContent + fileContent[endIdx:]
}
func writeEnvDocumentation(w io.Writer, t reflect.Type, prefix string) {
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
@@ -59,7 +82,7 @@ func printEnvTags(w io.Writer, t reflect.Type, prefix string) {
}
for _, field := range reflect.VisibleFields(t) {
if field.PkgPath != "" { // unexported field
if field.PkgPath != "" {
continue
}
@@ -72,25 +95,80 @@ func printEnvTags(w io.Writer, t reflect.Type, prefix string) {
envTag = "REPLICA#"
}
}
combinedTag := envTag
if prefix != "" && envTag != "" {
combinedTag = prefix + "_" + envTag
} else if prefix != "" {
combinedTag = prefix
}
combinedTag := buildCombinedTag(prefix, envTag)
ft := field.Type
if ft.Kind() == reflect.Ptr {
ft = ft.Elem()
}
if ft.Kind() == reflect.Struct && ft.Name() != "Time" { // skip time.Time
printEnvTags(w, ft, strings.TrimSuffix(combinedTag, "_"))
if ft.Kind() == reflect.Struct && ft.Name() != "Time" {
writeEnvDocumentation(w, ft, strings.TrimSuffix(combinedTag, "_"))
} else if envTag != "" {
envVar := strings.Trim(combinedTag, "_") + " (" + ft.Kind().String() + ")"
docs := field.Tag.Get("documentation")
_, _ = fmt.Fprintf(w, "| %s | %s | %s |\n", envVar, ft.Kind().String(), docs)
}
}
}
func writeYAMLDocumentation(w io.Writer, t reflect.Type, firstPrefix, otherPrefix string) {
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.Kind() != reflect.Struct {
return
}
var i int
for _, field := range reflect.VisibleFields(t) {
if field.PkgPath != "" {
continue
}
yamlTag := field.Tag.Get("yaml")
if yamlTag == "-" {
continue
}
yamlTag = strings.TrimSuffix(yamlTag, ",omitempty")
ft := field.Type
if ft.Kind() == reflect.Ptr {
ft = ft.Elem()
}
pf := otherPrefix
if i == 0 {
pf = firstPrefix
}
newFirstPrefix := pf + " "
newOtherPrefix := otherPrefix + " "
if yamlTag == "replicas" && ft.Kind() == reflect.Slice {
ft = ft.Elem()
newFirstPrefix += "- "
newOtherPrefix += " "
}
if yamlTag != "" {
docs := field.Tag.Get("documentation")
_, _ = fmt.Fprintf(w, "%s%s: # (%s) %s\n", pf, yamlTag, ft.Kind().String(), docs)
i++
}
if ft.Kind() == reflect.Struct && ft.Name() != "Time" {
writeYAMLDocumentation(w, ft, newFirstPrefix, newOtherPrefix)
}
}
}
func buildCombinedTag(prefix, envTag string) string {
if prefix != "" && envTag != "" {
return prefix + "_" + envTag
} else if prefix != "" {
return prefix
}
return envTag
}

View File

@@ -1,6 +1,6 @@
// Package model provides primitives to interact with the openapi HTTP API.
//
// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.5.0 DO NOT EDIT.
// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.5.1 DO NOT EDIT.
package model
import (

View File

@@ -20,16 +20,16 @@ const (
// Config application configuration struct
// +k8s:deepcopy-gen=true
type Config struct {
Cron string `documentation:"Cron expression for the sync interval" env:"CRON" json:"cron,omitempty" yaml:"cron,omitempty"`
RunOnStart bool `documentation:"Run the sync on startup" env:"RUN_ON_START" json:"runOnStart,omitempty" yaml:"runOnStart,omitempty"`
PrintConfigOnly bool `documentation:"Print current config only and stop the application" env:"PRINT_CONFIG_ONLY" json:"printConfigOnly,omitempty" yaml:"printConfigOnly,omitempty"`
ContinueOnError bool `documentation:"Continue sync on errors" env:"CONTINUE_ON_ERROR" json:"continueOnError,omitempty" yaml:"continueOnError,omitempty"`
// Origin adguardhome instance
Origin *AdGuardInstance `json:"origin" yaml:"origin"`
Origin *AdGuardInstance `documentation:"Origin instance" json:"origin" yaml:"origin"`
// One single replica adguardhome instance
Replica *AdGuardInstance `json:"replica,omitempty" yaml:"replica,omitempty"`
Replica *AdGuardInstance `documentation:"Single or replica instance (don't use in combination with replicas')" json:"replica,omitempty" yaml:"replica,omitempty"`
// Multiple replica instances
Replicas []AdGuardInstance `json:"replicas,omitempty" yaml:"replicas,omitempty" faker:"slice_len=2"`
Cron string `json:"cron,omitempty" yaml:"cron,omitempty" documentation:"Cron expression for the sync interval" env:"CRON"`
RunOnStart bool `json:"runOnStart,omitempty" yaml:"runOnStart,omitempty" documentation:"Run the sync on startup" env:"RUN_ON_START"`
PrintConfigOnly bool `json:"printConfigOnly,omitempty" yaml:"printConfigOnly,omitempty" documentation:"Print current config only and stop the application" env:"PRINT_CONFIG_ONLY"`
ContinueOnError bool `json:"continueOnError,omitempty" yaml:"continueOnError,omitempty" documentation:"Continue sync on errors" env:"CONTINUE_ON_ERROR"`
Replicas []AdGuardInstance `documentation:"List or replica instances (don't use in combination with replicas')" json:"replicas,omitempty" yaml:"replicas,omitempty" faker:"slice_len=2"`
API API ` json:"api,omitempty" yaml:"api,omitempty"`
Features Features ` json:"features,omitempty" yaml:"features,omitempty"`
}

View File

@@ -1,5 +1,4 @@
//go:build tools
// +build tools
package tools