diff --git a/README.md b/README.md
index 9fab3f8..bdf288d 100644
--- a/README.md
+++ b/README.md
@@ -5,20 +5,21 @@
# 🔌 PlugNPiN
-**Plug and play your docker containers into Pi-Hole & Nginx Proxy Manager**
+**Plug and play your docker containers into Pi-Hole/AdGuard Home & Nginx Proxy Manager**
Automatically detect running Docker containers based on labels, add them
-as local DNS/CNAME records in **Pi-Hole** and create matching proxy hosts in
+as local DNS/CNAME records in **Pi-Hole** (or DNS Rewrites in **AdGuard Home**) and create matching proxy hosts in
**Nginx Proxy Manager**.
## Key Features
- Automatic Docker container detection.
-- Local DNS/CNAME record creation in Pi-hole.
+- Local DNS/CNAME record creation/deletion in Pi-hole.
+- DNS Rewrites creation/deletion in AdGuard Home.
- Nginx Proxy Manager host creation.
- Support for Docker socket proxy.
-**Due to popular request, Pi-Hole's functionality can be disabled.**
+**Pi-Hole's and AdGuard Home's functionality can be toggled individually. By default Pi-Hole is enabled and AdGuard Home is disabled.**
**[See the documentation site for full setup and configuration.](https://deepspace2.github.io/PlugNPiN)**
@@ -37,7 +38,7 @@ The application operates in two complementary modes to keep your services synchr
When a container is processed in either mode, PlugNPiN will:
-1. Create a DNS record pointing the specified `url` to the `ip` address on **Pi-Hole** (or a CNAME record pointing to a configurable target domain).
+1. Create a DNS record pointing the specified `url` to the `ip` address on **Pi-Hole/AdGuard Home** (or a CNAME record pointing to a configurable target domain).
2. Create a proxy host to route traffic from the `url` to the container's `ip` and `port` on **Nginx Proxy Manager**.
## Usage
diff --git a/docs/index.md b/docs/index.md
index f1286e8..37d3961 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -5,12 +5,15 @@
# 🔌 PlugNPiN
-**Plug and play your docker containers into Pi-Hole & Nginx Proxy Manager**
+**Plug and play your docker containers into Pi-Hole/AdGuard Home & Nginx Proxy Manager**
Automatically detect running Docker containers based on labels, add them
-as local DNS/[CNAME](#targetDomainLabel) records in **Pi-Hole** and create matching proxy hosts in
+as local DNS/[CNAME](#piholeTargetDomainLabel) records in **Pi-Hole** (or DNS Rewrites in **AdGuard Home**) and create matching proxy hosts in
**Nginx Proxy Manager**.
+**Pi-Hole's and AdGuard Home's functionality can be toggled individually. By default Pi-Hole is enabled and AdGuard Home is disabled.**
+See [Optional Environment Variables](#optional).
+
## How It Works
PlugNPiN discovers services by scanning for Docker containers that have the following labels:
@@ -26,13 +29,23 @@ The application operates in two complementary modes to keep your services synchr
When a container is processed in either mode, PlugNPiN will:
-1. Create a DNS record pointing the specified `url` to the `ip` address on **Pi-Hole** (or a [CNAME record](#targetDomainLabel) pointing to a configurable target domain).
+1. Create a DNS record pointing the specified `url` to the `ip` address on **Pi-Hole/AdGuard Home** (or a CNAME record pointing to a configurable target domain).
2. Create a proxy host to route traffic from the `url` to the container's `ip` and `port` on **Nginx Proxy Manager**.
### CNAME Records
-It is possible to force PlugNPiN to create CNAME records instead of local DNS records ("A record") in Pi-Hole by setting the `plugNPiN.piholeOptions.targetDomain` label.
-See [Per Container Configuration âž” Pi-Hole](#targetDomainLabel).
+#### AdGuard Home
+
+To create a DNS Rewrite as a CNAME, set the `plugNPiN.adguardHomeOptions.targetDomain` label.
+
+See [Per Container Configuration âž” AdGuard Home](#adguardHomeTargetDomainLabel).
+
+#### Pi-Hole
+
+To create A CNAME record instead of local DNS records ("A record"), set the `plugNPiN.piholeOptions.targetDomain` label.
+
+See [Per Container Configuration âž” Pi-Hole](#piholeTargetDomainLabel).
+
## Configuration
@@ -42,6 +55,9 @@ See [Per Container Configuration âž” Pi-Hole](#targetDomainLabel).
| Variable {: style="width:35%" } | Description | Notes |
|---|---|---|
+| `ADGUARD_HOME_HOST` | The URL of your AdGuard Home instance | Only required if [`ADGUARD_HOME_DISABLED`](#adguardHomeDisabledEnvVar) is set to `false` |
+| `ADGUARD_HOME_USERNAME` | Your AdGuard Home username | Only required if [`ADGUARD_HOME_DISABLED`](#adguardHomeDisabledEnvVar) is set to `false` |
+| `ADGUARD_HOME_PASSWORD` | Your AdGuard Home password | Only required if [`ADGUARD_HOME_DISABLED`](#adguardHomeDisabledEnvVar) is set to `false` |
| `NGINX_PROXY_MANAGER_HOST` | The URL of your Nginx Proxy Manager instance. | |
| `NGINX_PROXY_MANAGER_USERNAME` | Your Nginx Proxy Manager username. | |
| `NGINX_PROXY_MANAGER_PASSWORD` | Your Nginx Proxy Manager password.
**Important:** It is recommended to create a new non-admin user with only the "Proxy Hosts - Manage" permission. | |
@@ -52,6 +68,7 @@ See [Per Container Configuration âž” Pi-Hole](#targetDomainLabel).
| Variable {: style="width:35%" } | Description | Default {: style="width:10%" } |
|---|---|---|
+|
| Set to `false` to enable AdGuard Home functionality | `true` |
| `DEBUG` | Set to `true` to enable DEBUG level logs | `false` |
| `DOCKER_HOST` | The URL of a docker socket proxy. If set, you don't need to mount the docker socket as a volume. Querying containers must be allowed (typically done by setting the `CONTAINERS` environment variable to `1`). | *None* |
| | Set to `true` to disable Pi-Hole functionality | `false` |
@@ -66,6 +83,12 @@ See [Per Container Configuration âž” Pi-Hole](#targetDomainLabel).
### Per Container Configuration
+#### AdGuard Home
+
+| Label {: style="width:45%"} | Description | Default {: style="width:10%"} |
+|---|---|---|
+| `plugNPiN.adguardHomeOptions.targetDomain`
| If provided, a CNAME DNS Rewrite will be created | |
+
#### Nginx Proxy Manager
Use the following labels to configure Nginx Proxy Manager entries
@@ -87,7 +110,8 @@ Use the following labels to configure Nginx Proxy Manager entries
| Label {: style="width:35%"} | Description | Default {: style="width:10%"} |
|---|---|---|
-| `plugNPiN.piholeOptions.targetDomain`
| If provided, a CNAME record will be created **instead** of a DNS record | |
+| `plugNPiN.piholeOptions.targetDomain`
| If provided, a CNAME record will be created **instead** of a DNS record | |
+
## Usage
diff --git a/main.go b/main.go
index a732a46..85dd6f0 100644
--- a/main.go
+++ b/main.go
@@ -9,6 +9,7 @@ import (
"syscall"
"github.com/deepspace2/plugnpin/pkg/cli"
+ "github.com/deepspace2/plugnpin/pkg/clients/adguardhome"
"github.com/deepspace2/plugnpin/pkg/clients/docker"
"github.com/deepspace2/plugnpin/pkg/clients/npm"
"github.com/deepspace2/plugnpin/pkg/clients/pihole"
@@ -50,6 +51,7 @@ func main() {
log.Info(fmt.Sprintf("Will run every %v", conf.RunInterval))
}
+ var adguardHomeClient *adguardhome.Client
var piholeClient *pihole.Client
var npmClient *npm.Client
@@ -63,6 +65,10 @@ func main() {
}
}
+ if !conf.AdguardHomeDisabled {
+ adguardHomeClient = adguardhome.NewClient(conf.AdguardHomeHost, conf.AdguardHomeUsername, conf.AdguardHomePassword)
+ }
+
npmClient = npm.NewClient(conf.NpmHost, conf.NpmUsername, conf.NpmPassword)
err = npmClient.Login()
if err != nil {
@@ -78,7 +84,7 @@ func main() {
}
defer dockerClient.Close()
- proc := processor.New(dockerClient, piholeClient, npmClient, cliFlags.DryRun)
+ proc := processor.New(dockerClient, adguardHomeClient, piholeClient, npmClient, cliFlags.DryRun)
if conf.RunInterval == 0 {
log.Info("RUN_INTERVAL is 0, will run once")
diff --git a/pkg/clients/adguardhome/adguardhome.go b/pkg/clients/adguardhome/adguardhome.go
new file mode 100644
index 0000000..7515e07
--- /dev/null
+++ b/pkg/clients/adguardhome/adguardhome.go
@@ -0,0 +1,96 @@
+package adguardhome
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+
+ "github.com/deepspace2/plugnpin/pkg/clients"
+ "github.com/deepspace2/plugnpin/pkg/logging"
+)
+
+var log = logging.GetLogger()
+
+type Client struct {
+ http.Client
+ baseURL string
+}
+
+var headers map[string]string = map[string]string{
+ "accept": "application/json",
+ "content-type": "application/json",
+ "authorization": "",
+}
+
+func NewClient(baseURL, username, password string) *Client {
+ base64encodedCredentials := base64.StdEncoding.EncodeToString([]byte(fmt.Appendf([]byte{}, "%s:%s", username, password)))
+ headers["authorization"] = fmt.Sprintf("Basic %s", base64encodedCredentials)
+ return &Client{
+ http.Client{},
+ fmt.Sprintf("%v/control", baseURL),
+ }
+}
+
+func (ad *Client) getDnsRewrites() (DnsRewrites, error) {
+ dnsRewritesResponseString, _, err := clients.Get(&ad.Client, ad.baseURL+"/rewrite/list", headers)
+ if err != nil {
+ return nil, err
+ }
+ var resp []DnsRewrite
+ json.Unmarshal([]byte(dnsRewritesResponseString), &resp)
+
+ dnsRewrites := DnsRewrites{}
+ for _, rawDnsRewrite := range resp {
+ dnsRewrites[DomainName(rawDnsRewrite.Domain)] = IP(rawDnsRewrite.Answer)
+ }
+ return dnsRewrites, nil
+}
+
+func (ad *Client) AddDnsRewrite(domain, ip string) error {
+ existingRecords, err := ad.getDnsRewrites()
+ if err != nil {
+ return err
+ }
+ d := DomainName(domain)
+ _, exists := existingRecords[d]
+
+ if exists {
+ return nil
+ }
+
+ payload, err := json.Marshal(DnsRewrite{Answer: ip, Domain: domain, Enabled: true})
+ if err != nil {
+ return err
+ }
+ payloadString := string(payload)
+ _, statusCode, err := clients.Post(&ad.Client, ad.baseURL+"/rewrite/add", headers, &payloadString)
+ if err != nil {
+ return err
+ }
+
+ if statusCode == 401 {
+ return errors.New("Unauthorized")
+ }
+
+ return nil
+}
+
+func (ad *Client) DeleteDnsRewrite(domain, ip string) error {
+ payload, err := json.Marshal(DnsRewrite{Answer: ip, Domain: domain, Enabled: true})
+ if err != nil {
+ return err
+ }
+ payloadString := string(payload)
+ _, statusCode, err := clients.Post(&ad.Client, ad.baseURL+"/rewrite/delete", headers, &payloadString)
+ if err != nil {
+ return err
+ }
+
+ if statusCode == 401 {
+ return errors.New("Unauthorized")
+ }
+
+ return nil
+}
diff --git a/pkg/clients/adguardhome/adguardhome_test.go b/pkg/clients/adguardhome/adguardhome_test.go
new file mode 100644
index 0000000..545cf6a
--- /dev/null
+++ b/pkg/clients/adguardhome/adguardhome_test.go
@@ -0,0 +1,139 @@
+package adguardhome
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+// setupTestServer creates a new test server and a client pointing to it.
+func setupTestServer(username, password string, handler http.Handler) (*Client, *httptest.Server) {
+ server := httptest.NewServer(handler)
+ client := NewClient(server.URL, username, password)
+ client.Client = *server.Client()
+ return client, server
+}
+
+func TestAddDnsRewrite(t *testing.T) {
+ t.Run("successful add", func(t *testing.T) {
+ handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ auth := r.Header.Get("Authorization")
+ expectedAuth := "Basic " + base64.StdEncoding.EncodeToString([]byte("testuser:testpass"))
+ assert.Equal(t, expectedAuth, auth)
+ assert.Equal(t, "application/json", r.Header.Get("accept"))
+ assert.Equal(t, "application/json", r.Header.Get("content-type"))
+
+ if r.URL.Path == "/control/rewrite/list" && r.Method == http.MethodGet {
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, `[{"domain": "one.com", "answer": "1.1.1.1"}]`)
+ return
+ }
+
+ if r.URL.Path == "/control/rewrite/add" && r.Method == http.MethodPost {
+ var payload DnsRewrite
+ err := json.NewDecoder(r.Body).Decode(&payload)
+ assert.NoError(t, err)
+
+ assert.Equal(t, "test.com", payload.Domain)
+ assert.Equal(t, "1.2.3.4", payload.Answer)
+ assert.True(t, payload.Enabled)
+
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, `{}`)
+ return
+ }
+
+ t.Fatalf("Received unexpected request: %s %s", r.Method, r.URL.Path)
+ })
+
+ client, server := setupTestServer("testuser", "testpass", handler)
+ defer server.Close()
+
+ err := client.AddDnsRewrite("test.com", "1.2.3.4")
+ assert.NoError(t, err)
+ })
+}
+
+func TestDeleteDnsRewrite(t *testing.T) {
+ t.Run("successful delete", func(t *testing.T) {
+ var deleteCalled bool
+ handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ auth := r.Header.Get("Authorization")
+ expectedAuth := "Basic " + base64.StdEncoding.EncodeToString([]byte("testuser:testpass"))
+ assert.Equal(t, expectedAuth, auth)
+
+ if r.URL.Path == "/control/rewrite/delete" && r.Method == http.MethodPost {
+ deleteCalled = true
+ var payload DnsRewrite
+ err := json.NewDecoder(r.Body).Decode(&payload)
+ assert.NoError(t, err)
+
+ assert.Equal(t, "test.com", payload.Domain)
+ assert.Equal(t, "1.2.3.4", payload.Answer)
+ assert.True(t, payload.Enabled)
+
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, `{}`)
+ return
+ }
+
+ if r.URL.Path == "/control/rewrite/list" && r.Method == http.MethodGet {
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprint(w, `[]`)
+ return
+ }
+
+ t.Fatalf("Received unexpected request: %s %s", r.Method, r.URL.Path)
+ })
+
+ client, server := setupTestServer("testuser", "testpass", handler)
+ defer server.Close()
+
+ err := client.DeleteDnsRewrite("test.com", "1.2.3.4")
+ assert.NoError(t, err)
+ assert.True(t, deleteCalled, "Delete API endpoint was not called")
+
+ existingDnsRewrites, err := client.getDnsRewrites()
+ assert.NoError(t, err)
+ assert.Equal(t, 0, len(existingDnsRewrites))
+ })
+}
+
+func TestWrongCredentials(t *testing.T) {
+ t.Run("wrong credentials", func(t *testing.T) {
+ handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path == "/control/rewrite/delete" && r.Method == http.MethodPost {
+ auth := r.Header.Get("Authorization")
+ expectedAuth := "Basic " + base64.StdEncoding.EncodeToString([]byte("testuser:testpass"))
+ if auth != expectedAuth {
+ w.WriteHeader(http.StatusUnauthorized)
+ fmt.Fprint(w, `{}`)
+ }
+ return
+ }
+
+ if r.URL.Path == "/control/rewrite/list" && r.Method == http.MethodGet {
+ auth := r.Header.Get("Authorization")
+ expectedAuth := "Basic " + base64.StdEncoding.EncodeToString([]byte("testuser:testpass"))
+ if auth != expectedAuth {
+ w.WriteHeader(http.StatusUnauthorized)
+ fmt.Fprint(w, `{}`)
+ }
+ return
+ }
+
+ t.Fatalf("Received unexpected request: %s %s", r.Method, r.URL.Path)
+ })
+
+ client, server := setupTestServer("testuser", "wrongpass", handler)
+ defer server.Close()
+
+ err := client.DeleteDnsRewrite("test.com", "1.2.3.4")
+ assert.Error(t, err)
+ })
+}
diff --git a/pkg/clients/adguardhome/types.go b/pkg/clients/adguardhome/types.go
new file mode 100644
index 0000000..b993512
--- /dev/null
+++ b/pkg/clients/adguardhome/types.go
@@ -0,0 +1,18 @@
+package adguardhome
+
+type DnsRewrite struct {
+ Answer string `json:"answer"`
+ Domain string `json:"domain"`
+ Enabled bool `json:"enabled"`
+}
+
+type AdguardHomeOptions struct {
+ TargetDomain string
+}
+
+type (
+ DnsRewrites map[DomainName]IP
+ DomainName string
+ IP string
+ Target string
+)
diff --git a/pkg/clients/docker/docker.go b/pkg/clients/docker/docker.go
index 39c78e8..9dac520 100644
--- a/pkg/clients/docker/docker.go
+++ b/pkg/clients/docker/docker.go
@@ -11,29 +11,37 @@ import (
"github.com/docker/docker/api/types/filters"
dockerSdk "github.com/docker/go-sdk/client"
+ "github.com/deepspace2/plugnpin/pkg/clients/adguardhome"
"github.com/deepspace2/plugnpin/pkg/clients/npm"
"github.com/deepspace2/plugnpin/pkg/clients/pihole"
"github.com/deepspace2/plugnpin/pkg/errors"
"github.com/deepspace2/plugnpin/pkg/logging"
)
+type ClientOptions struct {
+ AdguardHome *adguardhome.AdguardHomeOptions
+ Pihole *pihole.PiHoleOptions
+ NPM *npm.NpmProxyHostOptions
+}
+
var log = logging.GetLogger()
const (
ipLabel = "plugNPiN.ip"
urlLabel = "plugNPiN.url"
- npmOptionsAdvancedConfigLabel = "plugNPiN.npmOptions.advancedConfig"
- npmOptionsBlockExploitsLabel = "plugNPiN.npmOptions.blockExploits"
- npmOptionsCachingEnabledLabel = "plugNPiN.npmOptions.cachingEnabled"
- npmOptionsCertificateNameLabel = "plugNPiN.npmOptions.certificateName"
- npmOptionsHTTP2SupportLabel = "plugNPiN.npmOptions.http2Support"
- npmOptionsHstsEnabledLabel = "plugNPiN.npmOptions.hstsEnabled"
- npmOptionsHstsSubdomainsLabel = "plugNPiN.npmOptions.hstsSubdomains"
- npmOptionsSchemeLabel = "plugNPiN.npmOptions.scheme"
- npmOptionsSslForcedLabel = "plugNPiN.npmOptions.forceSsl"
- npmOptionsWebsocketsSupportLabel = "plugNPiN.npmOptions.websocketsSupport"
- piholeOptionsTargetDomainLabel = "plugNPiN.piholeOptions.targetDomain"
+ adguardHomeOptionsTargetDomainLabel = "plugNPiN.adguardHomeOptions.targetDomain"
+ npmOptionsAdvancedConfigLabel = "plugNPiN.npmOptions.advancedConfig"
+ npmOptionsBlockExploitsLabel = "plugNPiN.npmOptions.blockExploits"
+ npmOptionsCachingEnabledLabel = "plugNPiN.npmOptions.cachingEnabled"
+ npmOptionsCertificateNameLabel = "plugNPiN.npmOptions.certificateName"
+ npmOptionsHTTP2SupportLabel = "plugNPiN.npmOptions.http2Support"
+ npmOptionsHstsEnabledLabel = "plugNPiN.npmOptions.hstsEnabled"
+ npmOptionsHstsSubdomainsLabel = "plugNPiN.npmOptions.hstsSubdomains"
+ npmOptionsSchemeLabel = "plugNPiN.npmOptions.scheme"
+ npmOptionsSslForcedLabel = "plugNPiN.npmOptions.forceSsl"
+ npmOptionsWebsocketsSupportLabel = "plugNPiN.npmOptions.websocketsSupport"
+ piholeOptionsTargetDomainLabel = "plugNPiN.piholeOptions.targetDomain"
)
var labels []string = []string{ipLabel, urlLabel}
@@ -67,28 +75,30 @@ func GetParsedContainerName(container container.Summary) string {
return strings.Trim(container.Names[0], "/")
}
-func GetValuesFromLabels(labels map[string]string) (ip, url string, port int, npmProxyHostOptions *npm.NpmProxyHostOptions, piholeOptions *pihole.PiHoleOptions, err error) {
+func GetValuesFromLabels(labels map[string]string) (ip, url string, port int, opts *ClientOptions, err error) {
ip, ok := labels[ipLabel]
if !ok {
- return "", "", 0, nil, nil, &errors.NonExistingLabelsError{Msg: fmt.Sprintf("missing %s label", ipLabel)}
+ return "", "", 0, nil, &errors.NonExistingLabelsError{Msg: fmt.Sprintf("missing %s label", ipLabel)}
}
url, ok = labels[urlLabel]
if !ok {
- return "", "", 0, nil, nil, &errors.NonExistingLabelsError{Msg: fmt.Sprintf("missing %s label", urlLabel)}
+ return "", "", 0, nil, &errors.NonExistingLabelsError{Msg: fmt.Sprintf("missing %s label", urlLabel)}
}
splitIPAndPort := strings.Split(ip, ":")
if len(splitIPAndPort) == 1 {
- return "", "", 0, nil, nil, &errors.MalformedIPLabelError{Msg: fmt.Sprintf("missing ':' in value of '%v' label", ipLabel)}
+ return "", "", 0, nil, &errors.MalformedIPLabelError{Msg: fmt.Sprintf("missing ':' in value of '%v' label", ipLabel)}
}
ip = splitIPAndPort[0]
port, err = strconv.Atoi(splitIPAndPort[1])
if err != nil {
- return "", "", 0, nil, nil, &errors.MalformedIPLabelError{
+ return "", "", 0, nil, &errors.MalformedIPLabelError{
Msg: fmt.Sprintf("value after ':' in value of '%v' label must be an integer, got '%v'", ipLabel, splitIPAndPort[1]),
}
}
+ opts = &ClientOptions{}
+
npmOptionsBlockExploitsLabelValue, exists := labels[npmOptionsBlockExploitsLabel]
if !exists {
npmOptionsBlockExploitsLabelValue = "true"
@@ -104,7 +114,7 @@ func GetValuesFromLabels(labels map[string]string) (ip, url string, port int, np
}
npmOptionsScheme = strings.ToLower(npmOptionsScheme)
if !slices.Contains([]string{"http", "https"}, npmOptionsScheme) {
- return "", "", 0, nil, nil, &errors.InvalidSchemeError{
+ return "", "", 0, nil, &errors.InvalidSchemeError{
Msg: fmt.Sprintf("value of '%v' label must be one of 'http', 'https', got '%v'", npmOptionsSchemeLabel, npmOptionsScheme),
}
}
@@ -116,7 +126,7 @@ func GetValuesFromLabels(labels map[string]string) (ip, url string, port int, np
npmOptionsHstsSubdomains, _ := strconv.ParseBool(labels[npmOptionsHstsSubdomainsLabel])
npmOptionsSslForced, _ := strconv.ParseBool(labels[npmOptionsSslForcedLabel])
- npmProxyHostOptions = &npm.NpmProxyHostOptions{
+ opts.NPM = &npm.NpmProxyHostOptions{
AdvancedConfig: npmOptionsAdvancedConfig,
AllowWebsocketUpgrade: npmOptionsWebsocketsSupport,
BlockExploits: npmOptionsBlockExploits,
@@ -131,9 +141,15 @@ func GetValuesFromLabels(labels map[string]string) (ip, url string, port int, np
piholeOptionsTargetDomain := labels[piholeOptionsTargetDomainLabel]
- piholeOptions = &pihole.PiHoleOptions{
+ opts.Pihole = &pihole.PiHoleOptions{
TargetDomain: piholeOptionsTargetDomain,
}
- return ip, url, port, npmProxyHostOptions, piholeOptions, nil
+ adguardHomeOptionsTargetDomain := labels[adguardHomeOptionsTargetDomainLabel]
+
+ opts.AdguardHome = &adguardhome.AdguardHomeOptions{
+ TargetDomain: adguardHomeOptionsTargetDomain,
+ }
+
+ return ip, url, port, opts, nil
}
diff --git a/pkg/clients/docker/docker_test.go b/pkg/clients/docker/docker_test.go
index 75c40c8..9c387e2 100644
--- a/pkg/clients/docker/docker_test.go
+++ b/pkg/clients/docker/docker_test.go
@@ -60,17 +60,18 @@ func TestGetParsedContainerName(t *testing.T) {
func TestGetValuesFromContainerLabels(t *testing.T) {
testCases := []struct {
- name string
- container container.Summary
- expectedIP string
- expectedURL string
- expectedPort int
- expectedErr error
- expectedNpmOptionsBlockExploits bool
- expectedNpmOptionsCachingEnabled bool
- expectedNpmOptionsScheme string
- expectedNpmOptionsWebsocketsSupport bool
- expectedPiholeOptionsTargetDomain string
+ name string
+ container container.Summary
+ expectedIP string
+ expectedURL string
+ expectedPort int
+ expectedErr error
+ expectedAdguardHomeOptionsTargetDomain string
+ expectedNpmOptionsBlockExploits bool
+ expectedNpmOptionsCachingEnabled bool
+ expectedNpmOptionsScheme string
+ expectedNpmOptionsWebsocketsSupport bool
+ expectedPiholeOptionsTargetDomain string
}{
{
name: "Happy path",
@@ -261,22 +262,46 @@ func TestGetValuesFromContainerLabels(t *testing.T) {
expectedNpmOptionsBlockExploits: true,
expectedPiholeOptionsTargetDomain: "custom.domain",
},
+ {
+ name: "AdguardHome options - target domain",
+ container: container.Summary{
+ Labels: map[string]string{
+ ipLabel: "192.168.1.10:8080",
+ urlLabel: "my-service.example.com",
+ adguardHomeOptionsTargetDomainLabel: "custom.domain.adguard",
+ },
+ },
+ expectedIP: "192.168.1.10",
+ expectedURL: "my-service.example.com",
+ expectedPort: 8080,
+ expectedErr: nil,
+ expectedNpmOptionsScheme: "http",
+ expectedNpmOptionsBlockExploits: true,
+ expectedAdguardHomeOptionsTargetDomain: "custom.domain.adguard",
+ },
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
- ip, url, port, npmOptions, piholeOptions, err := GetValuesFromLabels(tc.container.Labels)
+ ip, url, port, opts, err := GetValuesFromLabels(tc.container.Labels)
assert.Equal(t, tc.expectedIP, ip)
assert.Equal(t, tc.expectedURL, url)
assert.Equal(t, tc.expectedPort, port)
assert.Equal(t, tc.expectedErr, err)
if err == nil {
- assert.Equal(t, tc.expectedNpmOptionsBlockExploits, npmOptions.BlockExploits)
- assert.Equal(t, tc.expectedNpmOptionsCachingEnabled, npmOptions.CachingEnabled)
- assert.Equal(t, tc.expectedNpmOptionsScheme, npmOptions.ForwardScheme)
- assert.Equal(t, tc.expectedNpmOptionsWebsocketsSupport, npmOptions.AllowWebsocketUpgrade)
- assert.Equal(t, tc.expectedPiholeOptionsTargetDomain, piholeOptions.TargetDomain)
+ assert.NotNil(t, opts)
+ assert.NotNil(t, opts.NPM)
+ assert.NotNil(t, opts.Pihole)
+ assert.NotNil(t, opts.AdguardHome)
+ assert.Equal(t, tc.expectedNpmOptionsBlockExploits, opts.NPM.BlockExploits)
+ assert.Equal(t, tc.expectedNpmOptionsCachingEnabled, opts.NPM.CachingEnabled)
+ assert.Equal(t, tc.expectedNpmOptionsScheme, opts.NPM.ForwardScheme)
+ assert.Equal(t, tc.expectedNpmOptionsWebsocketsSupport, opts.NPM.AllowWebsocketUpgrade)
+ assert.Equal(t, tc.expectedPiholeOptionsTargetDomain, opts.Pihole.TargetDomain)
+ assert.Equal(t, tc.expectedAdguardHomeOptionsTargetDomain, opts.AdguardHome.TargetDomain)
+ } else {
+ assert.Nil(t, opts)
}
})
}
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 7324bee..6e84c92 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -16,6 +16,11 @@ var log = logging.GetLogger()
type Config struct {
Debug bool `env:"DEBUG" envDefault:"false"`
+ AdguardHomeDisabled bool `env:"ADGUARD_HOME_DISABLED" envDefault:"true"`
+ AdguardHomeHost string `env:"ADGUARD_HOME_HOST"`
+ AdguardHomePassword string `env:"ADGUARD_HOME_PASSWORD"`
+ AdguardHomeUsername string `env:"ADGUARD_HOME_USERNAME"`
+
NpmHost string `env:"NGINX_PROXY_MANAGER_HOST,notEmpty"`
NpmPassword string `env:"NGINX_PROXY_MANAGER_PASSWORD,notEmpty"`
NpmUsername string `env:"NGINX_PROXY_MANAGER_USERNAME,notEmpty"`
@@ -34,7 +39,7 @@ func GetEnvVars() (*Config, error) {
if err := env.ParseWithOptions(&config, env.Options{
OnSet: func(tag string, value any, isDefault bool) {
if isDefault {
- log.Info(fmt.Sprintf(`env: environment variable "%v" is not set, using default value "%v"`, tag, value))
+ log.Info(fmt.Sprintf(`env: environment variable '%v' is not set, using default value '%v'`, tag, value))
}
},
}); err != nil {
@@ -42,11 +47,15 @@ func GetEnvVars() (*Config, error) {
}
if !validateRunInterval(config.RunInterval) {
- return nil, errors.New(`env: environment variable "RUN_INTERVAL" must be >= 0`)
+ return nil, errors.New(`env: environment variable 'RUN_INTERVAL' must be >= 0`)
}
if !config.PiholeDisabled && (config.PiholeHost == "" || config.PiholePassword == "") {
- return nil, errors.New(`env: PIHOLE_HOST or PIHOLE_PASSWORD is not set`)
+ return nil, errors.New(`env: 'PIHOLE_HOST' or 'PIHOLE_PASSWORD' is not set`)
+ }
+
+ if !config.AdguardHomeDisabled && (config.AdguardHomeHost == "" || config.AdguardHomePassword == "" || config.AdguardHomeUsername == "") {
+ return nil, errors.New(`env: 'ADGUARD_HOME_HOST', 'ADGUARD_HOME_PASSWORD' or 'ADGUARD_HOME_USERNAME' is not set`)
}
return &config, nil
diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go
index 19ff64d..7c387a4 100644
--- a/pkg/config/config_test.go
+++ b/pkg/config/config_test.go
@@ -37,14 +37,15 @@ func TestGetEnvVars(t *testing.T) {
"RUN_INTERVAL": "5m",
},
expectedConfig: &Config{
- NpmHost: "npm.example.com",
- NpmPassword: "password",
- NpmUsername: "user",
- PiholeDisabled: false,
- PiholeHost: "pihole.example.com",
- PiholePassword: "pihole_pass",
- DockerHost: "unix:///var/run/docker.sock",
- RunInterval: 5 * time.Minute,
+ AdguardHomeDisabled: true,
+ NpmHost: "npm.example.com",
+ NpmPassword: "password",
+ NpmUsername: "user",
+ PiholeDisabled: false,
+ PiholeHost: "pihole.example.com",
+ PiholePassword: "pihole_pass",
+ DockerHost: "unix:///var/run/docker.sock",
+ RunInterval: 5 * time.Minute,
},
expectErr: false,
},
@@ -70,13 +71,14 @@ func TestGetEnvVars(t *testing.T) {
"PIHOLE_PASSWORD": "pihole_pass",
},
expectedConfig: &Config{
- NpmHost: "npm.example.com",
- NpmPassword: "password",
- NpmUsername: "user",
- PiholeDisabled: false,
- PiholeHost: "pihole.example.com",
- PiholePassword: "pihole_pass",
- RunInterval: 1 * time.Hour, // Default value
+ AdguardHomeDisabled: true,
+ NpmHost: "npm.example.com",
+ NpmPassword: "password",
+ NpmUsername: "user",
+ PiholeDisabled: false,
+ PiholeHost: "pihole.example.com",
+ PiholePassword: "pihole_pass",
+ RunInterval: 1 * time.Hour, // Default value
},
expectErr: false,
},
@@ -104,12 +106,13 @@ func TestGetEnvVars(t *testing.T) {
"RUN_INTERVAL": "5m",
},
expectedConfig: &Config{
- NpmHost: "npm.example.com",
- NpmPassword: "password",
- NpmUsername: "user",
- PiholeDisabled: true,
- DockerHost: "unix:///var/run/docker.sock",
- RunInterval: 5 * time.Minute,
+ AdguardHomeDisabled: true,
+ NpmHost: "npm.example.com",
+ NpmPassword: "password",
+ NpmUsername: "user",
+ PiholeDisabled: true,
+ DockerHost: "unix:///var/run/docker.sock",
+ RunInterval: 5 * time.Minute,
},
expectErr: false,
},
@@ -125,6 +128,42 @@ func TestGetEnvVars(t *testing.T) {
expectedConfig: nil,
expectErr: true,
},
+ {
+ name: "No need to set AdguardHome env vars if AdguardHome is disabled",
+ envVars: map[string]string{
+ "ADGUARD_HOME_DISABLED": "true",
+ "NGINX_PROXY_MANAGER_HOST": "npm.example.com",
+ "NGINX_PROXY_MANAGER_PASSWORD": "password",
+ "NGINX_PROXY_MANAGER_USERNAME": "user",
+ "PIHOLE_DISABLED": "true",
+ "DOCKER_HOST": "unix:///var/run/docker.sock",
+ "RUN_INTERVAL": "5m",
+ },
+ expectedConfig: &Config{
+ AdguardHomeDisabled: true,
+ NpmHost: "npm.example.com",
+ NpmPassword: "password",
+ NpmUsername: "user",
+ PiholeDisabled: true,
+ DockerHost: "unix:///var/run/docker.sock",
+ RunInterval: 5 * time.Minute,
+ },
+ expectErr: false,
+ },
+ {
+ name: "Need to set AdguardHome env vars if AdguardHome is enabled",
+ envVars: map[string]string{
+ "ADGUARD_HOME_DISABLED": "false",
+ "PIHOLE_DISABLED": "true",
+ "NGINX_PROXY_MANAGER_HOST": "npm.example.com",
+ "NGINX_PROXY_MANAGER_PASSWORD": "password",
+ "NGINX_PROXY_MANAGER_USERNAME": "user",
+ "DOCKER_HOST": "unix:///var/run/docker.sock",
+ "RUN_INTERVAL": "5m",
+ },
+ expectedConfig: nil,
+ expectErr: true,
+ },
}
for _, tc := range testCases {
diff --git a/pkg/processor/processor.go b/pkg/processor/processor.go
index f84545d..b0c32b0 100644
--- a/pkg/processor/processor.go
+++ b/pkg/processor/processor.go
@@ -8,6 +8,7 @@ import (
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/events"
+ "github.com/deepspace2/plugnpin/pkg/clients/adguardhome"
"github.com/deepspace2/plugnpin/pkg/clients/docker"
"github.com/deepspace2/plugnpin/pkg/clients/npm"
"github.com/deepspace2/plugnpin/pkg/clients/pihole"
@@ -18,18 +19,20 @@ import (
var log = logging.GetLogger()
type Processor struct {
- dockerClient *docker.Client
- piholeClient *pihole.Client
- npmClient *npm.Client
- dryRun bool
+ dockerClient *docker.Client
+ adguardHomeClient *adguardhome.Client
+ piholeClient *pihole.Client
+ npmClient *npm.Client
+ dryRun bool
}
-func New(dockerClient *docker.Client, piholeClient *pihole.Client, npmClient *npm.Client, dryRun bool) *Processor {
+func New(dockerClient *docker.Client, adguardHomeClient *adguardhome.Client, piholeClient *pihole.Client, npmClient *npm.Client, dryRun bool) *Processor {
return &Processor{
- dockerClient: dockerClient,
- piholeClient: piholeClient,
- npmClient: npmClient,
- dryRun: dryRun,
+ dockerClient: dockerClient,
+ adguardHomeClient: adguardHomeClient,
+ piholeClient: piholeClient,
+ npmClient: npmClient,
+ dryRun: dryRun,
}
}
@@ -84,7 +87,7 @@ func (p *Processor) RunOnce() {
func (p *Processor) preprocessContainer(container container.Summary) {
parsedContainerName := docker.GetParsedContainerName(container)
- ip, url, port, npmProxyHostOptions, piholeOptions, err := docker.GetValuesFromLabels(container.Labels)
+ ip, url, port, opts, err := docker.GetValuesFromLabels(container.Labels)
if err != nil {
switch err.(type) {
case *errors.NonExistingLabelsError:
@@ -94,7 +97,7 @@ func (p *Processor) preprocessContainer(container container.Summary) {
}
return
}
- p.processContainer(docker.ContainerEvent.Start, parsedContainerName, ip, url, port, *piholeOptions, *npmProxyHostOptions)
+ p.processContainer(docker.ContainerEvent.Start, parsedContainerName, ip, url, port, opts)
}
func (p *Processor) handleDockerEvent(event events.Message) {
@@ -104,7 +107,7 @@ func (p *Processor) handleDockerEvent(event events.Message) {
return
}
- ip, url, port, npmProxyHostOptions, piholeOptions, err := docker.GetValuesFromLabels(event.Actor.Attributes)
+ ip, url, port, opts, err := docker.GetValuesFromLabels(event.Actor.Attributes)
if err != nil {
switch err.(type) {
case *errors.NonExistingLabelsError:
@@ -116,7 +119,31 @@ func (p *Processor) handleDockerEvent(event events.Message) {
return
}
containerEvent, _ := docker.ContainerEvent.ParseString(string(event.Action))
- p.processContainer(containerEvent, containerName, ip, url, port, *piholeOptions, *npmProxyHostOptions)
+ p.processContainer(containerEvent, containerName, ip, url, port, opts)
+}
+
+func (p *Processor) handleAdguardHome(containerEvent docker.EventType, containerName, url, ip string, adguardHomeOptions adguardhome.AdguardHomeOptions) {
+ if p.adguardHomeClient != nil {
+ if adguardHomeOptions.TargetDomain != "" {
+ // quick "workaround" for the fact that adguard unifies "local DNS records" and "CNAME records"
+ ip = adguardHomeOptions.TargetDomain
+ }
+
+ switch containerEvent {
+ case docker.ContainerEvent.Start:
+ log.Info("Adding a DNS rewrite to AdGuard Home", "container", containerName, "domain", url, "answer", ip)
+ err := p.adguardHomeClient.AddDnsRewrite(url, ip)
+ if err != nil {
+ log.Error("Failed to add a DNS rewrite to AdGuard Home", "container", containerName, "domain", url, "answer", ip, "error", err)
+ }
+ case docker.ContainerEvent.Die:
+ log.Info("Deleting DNS rewrite from AdGuard Home", "container", containerName, "domain", url)
+ err := p.adguardHomeClient.DeleteDnsRewrite(url, ip)
+ if err != nil {
+ log.Error("Failed to delete DNS rewrite from AdGuard Home", "container", containerName, "domain", url, "error", err)
+ }
+ }
+ }
}
func (p *Processor) handlePiHole(containerEvent docker.EventType, containerName, url, ip string, piholeOptions pihole.PiHoleOptions) {
@@ -197,7 +224,7 @@ func (p *Processor) handleNpm(containerEvent docker.EventType, containerName, ur
}
}
-func (p *Processor) processContainer(containerEvent docker.EventType, containerName, ip, url string, port int, piholeOptions pihole.PiHoleOptions, npmProxyHostOptions npm.NpmProxyHostOptions) {
+func (p *Processor) processContainer(containerEvent docker.EventType, containerName, ip, url string, port int, opts *docker.ClientOptions) {
msg := "Handling container"
if p.dryRun {
@@ -210,7 +237,14 @@ func (p *Processor) processContainer(containerEvent docker.EventType, containerN
if p.npmClient != nil {
npmHost := p.npmClient.GetIP()
- p.handlePiHole(containerEvent, containerName, url, npmHost, piholeOptions)
- p.handleNpm(containerEvent, containerName, url, ip, port, npmProxyHostOptions)
+ if opts.AdguardHome != nil {
+ p.handleAdguardHome(containerEvent, containerName, url, npmHost, *opts.AdguardHome)
+ }
+ if opts.Pihole != nil {
+ p.handlePiHole(containerEvent, containerName, url, npmHost, *opts.Pihole)
+ }
+ if opts.NPM != nil {
+ p.handleNpm(containerEvent, containerName, url, ip, port, *opts.NPM)
+ }
}
}