diff --git a/.toolbox.mk b/.toolbox.mk index 73cc597..7edfa4c 100644 --- a/.toolbox.mk +++ b/.toolbox.mk @@ -1,4 +1,3 @@ - ## toolbox - start ## Generated with https://github.com/bakito/toolbox diff --git a/README.md b/README.md index 2890a32..91888d2 100644 --- a/README.md +++ b/README.md @@ -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) - - # AdGuardHome sync 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` + +| 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 | + + +### YAML Configuration file + +location: $HOME/.adguardhome-sync.yaml + + +```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 +``` + ## 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` - -| 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 | - +⚠️ 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. -### Unraid - -⚠️ Disclaimer: Tere exists an unraid tepmlate for this application. This template is not managed by this project. -Also, as unraid is not known to me, I can not give any support on unraind templates. - -Note when running the Docker container in Unraid please remove unneeded env variables if don't needed. -If replica2 isn't used this can cause sync errors. - -### Config file - -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: - # 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 diff --git a/cmd/docs/main.go b/cmd/docs/main.go index 81bebc4..dee8a03 100644 --- a/cmd/docs/main.go +++ b/cmd/docs/main.go @@ -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 = "" + envEndMarker = "" + yamlStartMarker = "" + yamlEndMarker = "" +) + 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 - var buf strings.Builder - _, _ = buf.WriteString("| Name | Type | Description |\n") - _, _ = buf.WriteString("| :--- | ---- |:----------- |\n") - printEnvTags(&buf, reflect.TypeOf(types.Config{}), "") + slog.Info("Generating environment variables") + fileContent = generateEnvDocumentation(fileContent) - // Find the markers and replace content between them - startMarker := "" - endMarker := "" + slog.Info("Generating yaml configuration") + fileContent = generateYAMLDocumentation(fileContent) - start := strings.Index(fileContent, startMarker) - end := strings.Index(fileContent, endMarker) - - if start == -1 || end == -1 { - log.Fatal("Could not find markers in README.md") - } - - // Construct new content - newContent := fileContent[:start+len(startMarker)] + "\n" + buf.String() + fileContent[end:] - - // Write back to README.md - err = os.WriteFile("README.md", []byte(newContent), 0o644) + slog.Info("Writing README.md") + err = os.WriteFile("README.md", []byte(fileContent), 0o644) if err != nil { - log.Fatal(err) + slog.Error("Error writing README.md", "error", err) + os.Exit(1) } } -// printEnvTags recursively prints all fields with `env` tags. -func printEnvTags(w io.Writer, t reflect.Type, prefix string) { +func generateEnvDocumentation(fileContent string) string { + var buf strings.Builder + _, _ = buf.WriteString("| Name | Type | Description |\n") + _, _ = buf.WriteString("| :--- | ---- |:----------- |\n") + writeEnvDocumentation(&buf, reflect.TypeOf(types.Config{}), "") + + return updateDocumentationSection(fileContent, envStartMarker, envEndMarker, buf.String()) +} + +func generateYAMLDocumentation(fileContent string) string { + var buf strings.Builder + _, _ = buf.WriteString("```yaml\n") + writeYAMLDocumentation(&buf, reflect.TypeOf(types.Config{}), "", "") + _, _ = buf.WriteString("```\n") + + return updateDocumentationSection(fileContent, yamlStartMarker, yamlEndMarker, buf.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 +} diff --git a/internal/client/model/model_generated.go b/internal/client/model/model_generated.go index 8edf39d..821f031 100644 --- a/internal/client/model/model_generated.go +++ b/internal/client/model/model_generated.go @@ -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 ( diff --git a/internal/types/types.go b/internal/types/types.go index fef0888..76dc232 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -20,18 +20,18 @@ 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"` - API API `json:"api,omitempty" yaml:"api,omitempty"` - Features Features `json:"features,omitempty" yaml:"features,omitempty"` + 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"` } // API configuration. diff --git a/tools.go b/tools.go index 3bac4c0..e16c0bd 100644 --- a/tools.go +++ b/tools.go @@ -1,5 +1,4 @@ //go:build tools -// +build tools package tools