Skip to content

Commit 5444b34

Browse files
authored
feat: pi-hole cname records (#18)
closes #12
1 parent 7a451ab commit 5444b34

File tree

8 files changed

+524
-163
lines changed

8 files changed

+524
-163
lines changed

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
**Plug and play your docker containers into Pi-Hole & Nginx Proxy Manager**
88

99
Automatically detect running Docker containers based on labels, add them
10-
as local DNS records in **Pi-Hole** and create matching proxy hosts in
10+
as local DNS/CNAME records in **Pi-Hole** and create matching proxy hosts in
1111
**Nginx Proxy Manager**.
1212

1313
## Key Features
1414

1515
- Automatic Docker container detection.
16-
- Local DNS record creation in Pi-hole.
16+
- Local DNS/CNAME record creation in Pi-hole.
1717
- Nginx Proxy Manager host creation.
1818
- Support for Docker socket proxy.
1919

@@ -28,14 +28,14 @@ PlugNPiN discovers services by scanning for Docker containers that have the foll
2828

2929
The application operates in two complementary modes to keep your services synchronized:
3030

31-
1. **Real-Time Event Listening**: The application actively listens for Docker container events. When a container with the required labels is **started**, **stopped**, or **killed**, the tool immediately adds or removes the corresponding DNS and proxy host entries. This ensures that your services are updated in real-time as containers change state.
31+
1. **Real-Time Event Listening**: The application actively listens for Docker container events. When a container with the required labels is **started**, **stopped**, or **killed**, the tool immediately adds or removes the corresponding DNS and proxy host entries. This ensures that your services are updated in real-time as containers change state.
3232

33-
2. **Periodic Synchronization**: In addition to real-time events, the tool performs a full synchronization at a regular interval, defined by the `RUN_INTERVAL` environment variable. During this periodic run, it scans all running containers and ensures that their DNS and proxy configurations are correct. This acts as a self-healing mechanism, correcting any entries that might have been missed or become inconsistent.
33+
2. **Periodic Synchronization**: In addition to real-time events, the tool performs a full synchronization at a regular interval, defined by the `RUN_INTERVAL` environment variable. During this periodic run, it scans all running containers and ensures that their DNS and proxy configurations are correct. This acts as a self-healing mechanism, correcting any entries that might have been missed or become inconsistent.
3434

3535
When a container is processed in either mode, PlugNPiN will:
3636

37-
1. Create a DNS record pointing the specified `url` to the `ip` address on **Pi-Hole**.
38-
2. Create a proxy host to route traffic from the `url` to the container's `ip` and `port` on **Nginx Proxy Manager**.
37+
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).
38+
2. Create a proxy host to route traffic from the `url` to the container's `ip` and `port` on **Nginx Proxy Manager**.
3939

4040
## Usage
4141

docs/index.md

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
**Plug and play your docker containers into Pi-Hole & Nginx Proxy Manager**
88

99
Automatically detect running Docker containers based on labels, add them
10-
as local DNS records in **Pi-Hole** and create matching proxy hosts in
10+
as local DNS/[CNAME](#targetDomainLabel) records in **Pi-Hole** and create matching proxy hosts in
1111
**Nginx Proxy Manager**.
1212

1313
## How It Works
@@ -25,9 +25,14 @@ The application operates in two complementary modes to keep your services synchr
2525

2626
When a container is processed in either mode, PlugNPiN will:
2727

28-
1. Create a DNS record pointing the specified `url` to the `ip` address on **Pi-Hole**.
28+
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).
2929
2. Create a proxy host to route traffic from the `url` to the container's `ip` and `port` on **Nginx Proxy Manager**.
3030

31+
### CNAME Records
32+
33+
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.
34+
See [Per Container Configuration ➔ Pi-Hole](#targetDomainLabel).
35+
3136
## Configuration
3237

3338
### Environment Variables
@@ -39,8 +44,8 @@ When a container is processed in either mode, PlugNPiN will:
3944
| `NGINX_PROXY_MANAGER_HOST` | The URL of your Nginx Proxy Manager instance. |
4045
| `NGINX_PROXY_MANAGER_USERNAME` | Your Nginx Proxy Manager username. |
4146
| `NGINX_PROXY_MANAGER_PASSWORD` | Your Nginx Proxy Manager password. <br> **Important:** It is recommended to create a new non-admin user with only the "Proxy Hosts - Manage" permission. |
42-
| `PIHOLE_HOST` | The URL of your Pi-hole instance. |
43-
| `PIHOLE_PASSWORD` | Your Pi-hole password. <br> **Important:** It is recommended to create an 'application password' rather than using your actual admin password. |
47+
| `PIHOLE_HOST` | The URL of your Pi-Hole instance. |
48+
| `PIHOLE_PASSWORD` | Your Pi-Hole password. <br> **Important:** It is recommended to create an 'application password' rather than using your actual admin password. |
4449

4550
#### Optional
4651

@@ -54,7 +59,7 @@ When a container is processed in either mode, PlugNPiN will:
5459

5560
| Flag {: style="width:35%" } | Description |
5661
|---|---|
57-
| `--dry-run`, `-d` | Simulates the process of adding DNS records and proxy hosts without making any actual changes to Pi-hole or Nginx Proxy Manager. |
62+
| `--dry-run`, `-d` | Simulates the process of adding DNS/CNAME records and proxy hosts without making any actual changes to Pi-Hole or Nginx Proxy Manager. |
5863

5964
### Per Container Configuration
6065

@@ -74,6 +79,12 @@ Use the following labels to configure Nginx Proxy Manager entries
7479
| `plugNPiN.npmOptions.scheme` | The scheme used to forward traffic to the container. Can be `http` or `https` | `http` |
7580
| `plugNPiN.npmOptions.websocketsSupport` | Enables or disables the "Allow Websocket Upgrade" option on the proxy host. Set to `true` or `false` | `false` |
7681

82+
#### Pi-Hole
83+
84+
| Label {: style="width:35%"} | Description | Default {: style="width:10%"} |
85+
|---|---|---|
86+
| <a name="targetDomainLabel"></a>`plugNPiN.piholeOptions.targetDomain` | If provided, a CNAME record will be created **instead** of a DNS record | |
87+
7788
## Usage
7889

7990
### Docker Compose

pkg/clients/docker/docker.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"strings"
1010

1111
"github.com/deepspace2/plugnpin/pkg/clients/npm"
12+
"github.com/deepspace2/plugnpin/pkg/clients/pihole"
1213
"github.com/deepspace2/plugnpin/pkg/errors"
1314
"github.com/docker/docker/api/types/container"
1415
"github.com/docker/docker/api/types/filters"
@@ -28,6 +29,7 @@ const (
2829
npmOptionsSchemeLabel = "plugNPiN.npmOptions.scheme"
2930
npmOptionsSslForcedLabel = "plugNPiN.npmOptions.forceSsl"
3031
npmOptionsWebsocketsSupportLabel = "plugNPiN.npmOptions.websocketsSupport"
32+
piholeOptionsTargetDomainLabel = "plugNPiN.piholeOptions.targetDomain"
3133
)
3234

3335
var labels []string = []string{ipLabel, urlLabel}
@@ -61,24 +63,24 @@ func GetParsedContainerName(container container.Summary) string {
6163
return strings.Trim(container.Names[0], "/")
6264
}
6365

64-
func GetValuesFromLabels(labels map[string]string) (ip, url string, port int, npmProxyHostOptions *npm.NpmProxyHostOptions, err error) {
66+
func GetValuesFromLabels(labels map[string]string) (ip, url string, port int, npmProxyHostOptions *npm.NpmProxyHostOptions, piholeOptions *pihole.PiHoleOptions, err error) {
6567
ip, ok := labels[ipLabel]
6668
if !ok {
67-
return "", "", 0, nil, &errors.NonExistingLabelsError{Msg: fmt.Sprintf("missing %s label", ipLabel)}
69+
return "", "", 0, nil, nil, &errors.NonExistingLabelsError{Msg: fmt.Sprintf("missing %s label", ipLabel)}
6870
}
6971
url, ok = labels[urlLabel]
7072
if !ok {
71-
return "", "", 0, nil, &errors.NonExistingLabelsError{Msg: fmt.Sprintf("missing %s label", urlLabel)}
73+
return "", "", 0, nil, nil, &errors.NonExistingLabelsError{Msg: fmt.Sprintf("missing %s label", urlLabel)}
7274
}
7375

7476
splitIPAndPort := strings.Split(ip, ":")
7577
if len(splitIPAndPort) == 1 {
76-
return "", "", 0, nil, &errors.MalformedIPLabelError{Msg: fmt.Sprintf("missing ':' in value of '%v' label", ipLabel)}
78+
return "", "", 0, nil, nil, &errors.MalformedIPLabelError{Msg: fmt.Sprintf("missing ':' in value of '%v' label", ipLabel)}
7779
}
7880
ip = splitIPAndPort[0]
7981
port, err = strconv.Atoi(splitIPAndPort[1])
8082
if err != nil {
81-
return "", "", 0, nil, &errors.MalformedIPLabelError{
83+
return "", "", 0, nil, nil, &errors.MalformedIPLabelError{
8284
Msg: fmt.Sprintf("value after ':' in value of '%v' label must be an integer, got '%v'", ipLabel, splitIPAndPort[1]),
8385
}
8486
}
@@ -93,7 +95,7 @@ func GetValuesFromLabels(labels map[string]string) (ip, url string, port int, np
9395
}
9496
npmOptionsScheme = strings.ToLower(npmOptionsScheme)
9597
if !slices.Contains([]string{"http", "https"}, npmOptionsScheme) {
96-
return "", "", 0, nil, &errors.InvalidSchemeError{
98+
return "", "", 0, nil, nil, &errors.InvalidSchemeError{
9799
Msg: fmt.Sprintf("value of '%v' label must be one of 'http', 'https', got '%v'", npmOptionsSchemeLabel, npmOptionsScheme),
98100
}
99101
}
@@ -116,5 +118,11 @@ func GetValuesFromLabels(labels map[string]string) (ip, url string, port int, np
116118
SslForced: npmOptionsSslForced,
117119
}
118120

119-
return ip, url, port, npmProxyHostOptions, nil
121+
piholeOptionsTargetDomain := labels[piholeOptionsTargetDomainLabel]
122+
123+
piholeOptions = &pihole.PiHoleOptions{
124+
TargetDomain: piholeOptionsTargetDomain,
125+
}
126+
127+
return ip, url, port, npmProxyHostOptions, piholeOptions, nil
120128
}

pkg/clients/docker/docker_test.go

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ func TestGetValuesFromContainerLabels(t *testing.T) {
7070
expectedNpmOptionsCachingEnabled bool
7171
expectedNpmOptionsScheme string
7272
expectedNpmOptionsWebsocketsSupport bool
73+
expectedPiholeOptionsTargetDomain string
7374
}{
7475
{
7576
name: "Happy path",
@@ -227,11 +228,42 @@ func TestGetValuesFromContainerLabels(t *testing.T) {
227228
expectedNpmOptionsScheme: "https",
228229
expectedNpmOptionsWebsocketsSupport: false,
229230
},
231+
{
232+
name: "Pi-Hole options - no target domain",
233+
container: container.Summary{
234+
Labels: map[string]string{
235+
ipLabel: "192.168.1.10:8080",
236+
urlLabel: "my-service.example.com",
237+
},
238+
},
239+
expectedIP: "192.168.1.10",
240+
expectedURL: "my-service.example.com",
241+
expectedPort: 8080,
242+
expectedErr: nil,
243+
expectedNpmOptionsScheme: "http",
244+
expectedPiholeOptionsTargetDomain: "",
245+
},
246+
{
247+
name: "Pi-Hole options - target domain",
248+
container: container.Summary{
249+
Labels: map[string]string{
250+
ipLabel: "192.168.1.10:8080",
251+
urlLabel: "my-service.example.com",
252+
piholeOptionsTargetDomainLabel: "custom.domain",
253+
},
254+
},
255+
expectedIP: "192.168.1.10",
256+
expectedURL: "my-service.example.com",
257+
expectedPort: 8080,
258+
expectedErr: nil,
259+
expectedNpmOptionsScheme: "http",
260+
expectedPiholeOptionsTargetDomain: "custom.domain",
261+
},
230262
}
231263

232264
for _, tc := range testCases {
233265
t.Run(tc.name, func(t *testing.T) {
234-
ip, url, port, npmOptions, err := GetValuesFromLabels(tc.container.Labels)
266+
ip, url, port, npmOptions, piholeOptions, err := GetValuesFromLabels(tc.container.Labels)
235267

236268
assert.Equal(t, tc.expectedIP, ip)
237269
assert.Equal(t, tc.expectedURL, url)
@@ -242,6 +274,7 @@ func TestGetValuesFromContainerLabels(t *testing.T) {
242274
assert.Equal(t, tc.expectedNpmOptionsCachingEnabled, npmOptions.CachingEnabled)
243275
assert.Equal(t, tc.expectedNpmOptionsScheme, npmOptions.ForwardScheme)
244276
assert.Equal(t, tc.expectedNpmOptionsWebsocketsSupport, npmOptions.AllowWebsocketUpgrade)
277+
assert.Equal(t, tc.expectedPiholeOptionsTargetDomain, piholeOptions.TargetDomain)
245278
}
246279
})
247280
}

0 commit comments

Comments
 (0)