From 3903b63d70d56bfe2590526cc83934f1b4737d71 Mon Sep 17 00:00:00 2001 From: Lucas Lara Date: Thu, 12 Mar 2026 07:30:14 +0100 Subject: [PATCH] Support escaping dots in label keys with \. The dot character is used as a nesting separator in Docker label keys, making it impossible to use literal dots in directive names (e.g. domain names like mydomain.fr or Caddy's {blocks.name} syntax). This adds support for \. as an escaped literal dot, so: caddy.dynamic_dns.domains.mydomain\.fr produces: dynamic_dns { domains { mydomain.fr } } Fixes #767 Fixes #699 Co-Authored-By: Claude Opus 4.6 --- caddyfile/fromlabels.go | 7 ++++++- caddyfile/testdata/labels/escaped_dots.txt | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 caddyfile/testdata/labels/escaped_dots.txt diff --git a/caddyfile/fromlabels.go b/caddyfile/fromlabels.go index 61e7b98e..82d96396 100644 --- a/caddyfile/fromlabels.go +++ b/caddyfile/fromlabels.go @@ -5,12 +5,15 @@ import ( "math" "regexp" "strconv" + "strings" "text/template" ) var whitespaceRegex = regexp.MustCompile("\\s+") var labelParserRegex = regexp.MustCompile(`^(?:(.+)\.)?(?:(\d+)_)?([^.]+?)(?:_(\d+))?$`) +const escapedDotPlaceholder = "\x00" + // FromLabels converts key value labels into a caddyfile func FromLabels(labels map[string]string, templateData interface{}, templateFuncs template.FuncMap) (*Container, error) { container := CreateContainer() @@ -37,7 +40,9 @@ func getOrCreateBlock(container *Container, path string, blocksByPath map[string return block } - parentPath, order, name := parsePath(path) + parentPath, order, name := parsePath(strings.ReplaceAll(path, `\.`, escapedDotPlaceholder)) + parentPath = strings.ReplaceAll(parentPath, escapedDotPlaceholder, `\.`) + name = strings.ReplaceAll(name, escapedDotPlaceholder, ".") block := CreateBlock() block.Order = order diff --git a/caddyfile/testdata/labels/escaped_dots.txt b/caddyfile/testdata/labels/escaped_dots.txt new file mode 100644 index 00000000..90a8d802 --- /dev/null +++ b/caddyfile/testdata/labels/escaped_dots.txt @@ -0,0 +1,22 @@ +caddy = (extendable-proxy) +caddy.reverse_proxy.{blocks\.proxy_target} = +caddy.reverse_proxy.{blocks\.proxy_options} = + +caddy_1 = localhost +caddy_1.dynamic_dns.provider = cloudflare token123 +caddy_1.dynamic_dns.domains.mydomain\.fr = +---------- +(extendable-proxy) { + reverse_proxy { + {blocks.proxy_options} + {blocks.proxy_target} + } +} +localhost { + dynamic_dns { + domains { + mydomain.fr + } + provider cloudflare token123 + } +}