Repository: lucaslorentz/caddy-docker-proxy (go)

Issue:
Container id extraction logic breaks on docker with cgroup2
[Docker 20.10 gained support for cgroup v2](https://docs.docker.com/engine/release-notes/#runtime-1).

However when running this caddy plugin in a container on Docker with cgroup2 the current logic for container ID extraction fails.

As far as I can tell the problem seems to be at this part of the code:

https://github.com/lucaslorentz/caddy-docker-proxy/blob/b1b2a9d72a825ff0bf535afe41bc9558f3aa99b5/plugin/docker/utils.go#L24-L51

This seems to be based of [a know hack](https://stackoverflow.com/q/20995351#answer-25729598) for getting container information from within the container itself.

The problem is that with cgroup v2, the file `/proc/self/cgroup` doesn't contain the previously available information.

Below is its content in a container running on a Docker with cgroup v2

```shell
$> cat /proc/self/cgroup
0::/
```

Digging through the other files in `/proc/self`, there seems to be another (hacky) way for retrieving the container id.

The file `/proc/self/mountinfo` contain the id in the mount paths for the `/etc/resolv.conf`, `/etc/hostname`, `/etc/hosts` files:

```shell
$> cat /proc/self/mountinfo
1582 1433 0:29 /@/var/lib/docker/btrfs/subvolumes/3388737b99462f26ff76d17105b237d2c2f312338f3c6585af52d8745add9af1 / rw master:1 - btrfs /dev/sda2 rw,lazytime,compress=zstd:3,ssd,discard=async,space_cache,subvolid=4409,subvol=/@/var/lib/docker/btrfs/subvolumes/3388737b99462f26ff76d17105b237d2c2f312338f3c6585af52d8745add9af1
1583 1582 0:189 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw
1584 1582 0:190 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64
1585 1584 0:191 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666
1586 1582 0:192 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro
1587 1586 0:25 / /sys/fs/cgroup ro,nosuid,nodev,noexec,relatime - cgroup2 cgroup rw,nsdelegate,memory_recursiveprot
1588 1584 0:188 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw
1589 1584 0:193 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=65536k,inode64
1590 1582 0:29 /@/usr/bin/docker-init /sbin/docker-init ro master:1 - btrfs /dev/sda2 rw,lazytime,compress=zstd:3,ssd,discard=async,space_cache,subvolid=256,subvol=/@
1591 1582 0:187 / /tmp rw,nosuid,nodev,noexec,relatime master:645 - tmpfs tmpfs rw,size=262144k,mode=777,inode64
1592 1582 0:75 / /config rw,relatime master:666 - nfs4 :/caddy/config rw,vers=4.2,rsize=1048576,wsize=1048576,namlen=255,soft,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=10.0.0.2,local_lock=none,addr=10.0.0.2
1593 1582 0:75 / /data rw,relatime master:673 - nfs4 :/caddy/data rw,vers=4.2,rsize=1048576,wsize=1048576,namlen=255,soft,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=10.0.0.2,local_lock=none,addr=10.0.0.2
1594 1582 0:75 / /var/log rw,relatime master:659 - nfs4 :/logs/caddy rw,vers=4.2,rsize=1048576,wsize=1048576,namlen=255,soft,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=10.0.0.2,local_lock=none,addr=10.0.0.2
1595 1582 0:29 /@/var/lib/docker/containers/fcc626d54fda7061cc76bbdb0a0d992bc7ea4864b88340336857c43fd37b3df0/resolv.conf /etc/resolv.conf rw - btrfs /dev/sda2 rw,lazytime,compress=zstd:3,ssd,discard=async,space_cache,subvolid=256,subvol=/@
1596 1582 0:29 /@/var/lib/docker/containers/fcc626d54fda7061cc76bbdb0a0d992bc7ea4864b88340336857c43fd37b3df0/hostname /etc/hostname rw - btrfs /dev/sda2 rw,lazytime,compress=zstd:3,ssd,discard=async,space_cache,subvolid=256,subvol=/@
1597 1582 0:29 /@/var/lib/docker/containers/fcc626d54fda7061cc76bbdb0a0d992bc7ea4864b88340336857c43fd37b3df0/hosts /etc/hosts rw - btrfs /dev/sda2 rw,lazytime,compress=zstd:3,ssd,discard=async,space_cache,subvolid=256,subvol=/@
1598 1582 0:75 / /usr/share/caddy rw,relatime master:652 - nfs4 :/caddy/static rw,vers=4.2,rsize=1048576,wsize=1048576,namlen=255,soft,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=10.0.0.2,local_lock=none,addr=10.0.0.2
1434 1583 0:189 /bus /proc/bus ro,relatime - proc proc rw
1453 1583 0:189 /fs /proc/fs ro,relatime - proc proc rw
1496 1583 0:189 /irq /proc/irq ro,relatime - proc proc rw
1497 1583 0:189 /sys /proc/sys ro,relatime - proc proc rw
1498 1583 0:189 /sysrq-trigger /proc/sysrq-trigger ro,relatime - proc proc rw
1499 1583 0:194 / /proc/asound ro,relatime - tmpfs tmpfs ro,inode64
1500 1583 0:195 / /proc/acpi ro,relatime - tmpfs tmpfs ro,inode64
1501 1583 0:190 /null /proc/kcore rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64
1502 1583 0:190 /null /proc/keys rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64
1503 1583 0:190 /null /proc/latency_stats rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64
1504 1583 0:190 /null /proc/timer_list rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64
1505 1583 0:190 /null /proc/sched_debug rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64
1506 1583 0:196 / /proc/scsi ro,relatime - tmpfs tmpfs ro,inode64
1507 1586 0:197 / /sys/firmware ro,relatime - tmpfs tmpfs ro,inode64
```

The logic for extracting the container id could be extended to parse this file in the case the information isn't available in `/proc/self/cgroup`. However, I don't how reliable this solution can be, as I didn't test enough to be certain this mount paths are always available no matter the options a container is run with.

Pull request:
Improve logic to find self container id
Use mountinfo, fallback to cgroups, fallback to hostname.
Fixes #207

Patch:
diff --git a/docker/utils.go b/docker/utils.go
index 4d7b3ae..2da7606 100644
--- a/docker/utils.go
+++ b/docker/utils.go
@@ -1,8 +1,6 @@
 package docker
 
 import (
-	"errors"
-	"fmt"
 	"io/ioutil"
 	"os"
 	"regexp"
@@ -23,29 +21,54 @@ func CreateUtils() Utils {
 
 // GetCurrentContainerID returns the id of the container running this application
 func (wrapper *dockerUtils) GetCurrentContainerID() (string, error) {
-	if runtime.GOOS == "windows" {
-		return os.Hostname()
+	var containerID string
+	var err error
+	if runtime.GOOS == "linux" {
+		if containerID == "" && err == nil {
+			containerID, err = wrapper.getCurrentContainerIDFromCGroup()
+		}
+		if containerID == "" && err == nil {
+			containerID, err = wrapper.getCurrentContainerIDFromMountInfo()
+		}
 	}
+	if containerID == "" && err == nil {
+		containerID, err = os.Hostname()
+	}
+	return containerID, err
+}
 
-	bytes, err := ioutil.ReadFile("/proc/self/cgroup")
+func (wrapper *dockerUtils) getCurrentContainerIDFromMountInfo() (string, error) {
+	bytes, err := ioutil.ReadFile("/proc/self/mountinfo")
 	if err != nil {
 		return "", err
 	}
-	if len(bytes) == 0 {
-		return "", errors.New("Cannot read /proc/self/cgroup")
-	}
+	containerID := wrapper.extractContainerIDFromMountInfo(string(bytes))
+	return containerID, nil
+}
 
-	return wrapper.ExtractContainerID(string(bytes))
+func (wrapper *dockerUtils) getCurrentContainerIDFromCGroup() (string, error) {
+	bytes, err := ioutil.ReadFile("/proc/self/cgroup")
+	if err != nil {
+		return "", err
+	}
+	containerID := wrapper.extractContainerIDFromCGroups(string(bytes))
+	return containerID, nil
+}
 
+func (wrapper *dockerUtils) extractContainerIDFromMountInfo(cgroups string) string {
+	idRegex := regexp.MustCompile(`containers/([[:alnum:]]{64})/`)
+	matches := idRegex.FindStringSubmatch(cgroups)
+	if len(matches) == 0 {
+		return ""
+	}
+	return matches[len(matches)-1]
 }
 
-func (wrapper *dockerUtils) ExtractContainerID(cgroups string) (string, error) {
+func (wrapper *dockerUtils) extractContainerIDFromCGroups(cgroups string) string {
 	idRegex := regexp.MustCompile(`(?im)^[^:]*:[^:]*:.*\b([[:alnum:]]{64})\b`)
 	matches := idRegex.FindStringSubmatch(cgroups)
-
 	if len(matches) == 0 {
-		return "", fmt.Errorf("Cannot find container id in cgroups: %v", cgroups)
+		return ""
 	}
-
-	return matches[len(matches)-1], nil
+	return matches[len(matches)-1]
 }


Test patch:
diff --git a/docker/utils_test.go b/docker/utils_test.go
index c033be5..4adaf37 100644
--- a/docker/utils_test.go
+++ b/docker/utils_test.go
@@ -2,20 +2,52 @@ package docker
 
 import (
 	"testing"
+
+	"github.com/stretchr/testify/assert"
 )
 
+func TestExtratFromMountInfo(t *testing.T) {
+	read :=
+		`982 811 0:188 / / rw,relatime master:211 - overlay overlay rw,lowerdir=/var/lib/docker/overlay2/l/JITAD3AQIAPDR63API26SHX5CQ:/var/lib/docker/overlay2/l/OD4G2XK3EBQC7UCYC2MKN2VU4N:/var/lib/docker/overlay2/l/XCYRLTZ7FPAFABA6UPAECHCUFM:/var/lib/docker/overlay2/l/2UTXO3KIF3I7EQFKXPOYQO6WGN,upperdir=/var/lib/docker/overlay2/11a7a30cc374c98491c15185334a99f07e2761a9759c2c5b3ba1b4122ec9fbf7/diff,workdir=/var/lib/docker/overlay2/11a7a30cc374c98491c15185334a99f07e2761a9759c2c5b3ba1b4122ec9fbf7/work
+		984 982 0:205 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw
+		986 982 0:207 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755
+		988 986 0:209 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666
+		990 982 0:211 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro
+		993 990 0:33 / /sys/fs/cgroup ro,nosuid,nodev,noexec,relatime - cgroup2 cgroup rw
+		994 986 0:202 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw
+		996 986 0:213 / /dev/shm rw,nosuid,nodev,noexec,relatime - tmpfs shm rw,size=65536k
+		999 982 254:1 /docker/containers/d39fa516d8377ecddf9bf8ef33f81cbf58b4d604d85293ced7cdb0c7fc52442b/resolv.conf /etc/resolv.conf rw,relatime - ext4 /dev/vda1 rw
+		1001 982 254:1 /docker/containers/d39fa516d8377ecddf9bf8ef33f81cbf58b4d604d85293ced7cdb0c7fc52442b/hostname /etc/hostname rw,relatime - ext4 /dev/vda1 rw
+		1002 982 254:1 /docker/containers/d39fa516d8377ecddf9bf8ef33f81cbf58b4d604d85293ced7cdb0c7fc52442b/hosts /etc/hosts rw,relatime - ext4 /dev/vda1 rw
+		1003 982 0:23 /host-services/docker.proxy.sock /run/docker.sock rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,size=608152k,mode=755
+		576 984 0:205 /bus /proc/bus ro,nosuid,nodev,noexec,relatime - proc proc rw
+		587 984 0:205 /fs /proc/fs ro,nosuid,nodev,noexec,relatime - proc proc rw
+		588 984 0:205 /irq /proc/irq ro,nosuid,nodev,noexec,relatime - proc proc rw
+		589 984 0:205 /sys /proc/sys ro,nosuid,nodev,noexec,relatime - proc proc rw
+		590 984 0:205 /sysrq-trigger /proc/sysrq-trigger ro,nosuid,nodev,noexec,relatime - proc proc rw
+		591 984 0:207 /null /proc/kcore rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755
+		592 984 0:207 /null /proc/keys rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755
+		593 984 0:207 /null /proc/timer_list rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755
+		594 990 0:216 / /sys/firmware ro,relatime - tmpfs tmpfs ro`
+
+	expected := "d39fa516d8377ecddf9bf8ef33f81cbf58b4d604d85293ced7cdb0c7fc52442b"
+
+	utils := dockerUtils{}
+
+	actual := utils.extractContainerIDFromMountInfo(read)
+
+	assert.Equal(t, expected, actual)
+}
+
 func TestFailExtractBasicDockerId(t *testing.T) {
 	read :=
 		`1:cpu:/not_an_id`
 
 	utils := dockerUtils{}
 
-	actual, err := utils.ExtractContainerID(read)
-
-	if err == nil {
-		t.Fatalf("Got unexpected container id %v", actual)
-	}
+	actual := utils.extractContainerIDFromCGroups(read)
 
+	assert.Empty(t, actual)
 }
 
 func TestExtractBasicDockerId(t *testing.T) {
@@ -30,15 +62,9 @@ func TestExtractBasicDockerId(t *testing.T) {
 
 	utils := dockerUtils{}
 
-	actual, err := utils.ExtractContainerID(read)
-
-	if err != nil {
-		t.Fatalf("Could not extract container id : %v", err)
-	}
+	actual := utils.extractContainerIDFromCGroups(read)
 
-	if actual != expected {
-		t.Fatalf("id mismatch: actual %v, expected %v", actual, expected)
-	}
+	assert.Equal(t, expected, actual)
 }
 
 func TestExtractScopedDockerId(t *testing.T) {
@@ -53,15 +79,9 @@ func TestExtractScopedDockerId(t *testing.T) {
 
 	utils := dockerUtils{}
 
-	actual, err := utils.ExtractContainerID(read)
+	actual := utils.extractContainerIDFromCGroups(read)
 
-	if err != nil {
-		t.Fatalf("Could not extract container id : %v", err)
-	}
-
-	if actual != expected {
-		t.Fatalf("id mismatch: actual %v, expected %v", actual, expected)
-	}
+	assert.Equal(t, expected, actual)
 }
 
 func TestExtractSlashPrefixedDockerId(t *testing.T) {
@@ -76,15 +96,9 @@ func TestExtractSlashPrefixedDockerId(t *testing.T) {
 
 	utils := dockerUtils{}
 
-	actual, err := utils.ExtractContainerID(read)
-
-	if err != nil {
-		t.Fatalf("Could not extract container id : %v", err)
-	}
+	actual := utils.extractContainerIDFromCGroups(read)
 
-	if actual != expected {
-		t.Fatalf("id mismatch: actual %v, expected %v", actual, expected)
-	}
+	assert.Equal(t, expected, actual)
 }
 
 func TestExtractNestedDockerId(t *testing.T) {
@@ -105,15 +119,9 @@ func TestExtractNestedDockerId(t *testing.T) {
 
 	utils := dockerUtils{}
 
-	actual, err := utils.ExtractContainerID(read)
+	actual := utils.extractContainerIDFromCGroups(read)
 
-	if err != nil {
-		t.Fatalf("Could not extract container id : %v", err)
-	}
-
-	if actual != expected {
-		t.Fatalf("id mismatch: actual %v, expected %v", actual, expected)
-	}
+	assert.Equal(t, expected, actual)
 }
 
 func TestExtractAKSDockerId(t *testing.T) {
@@ -136,15 +144,9 @@ func TestExtractAKSDockerId(t *testing.T) {
 
 	utils := dockerUtils{}
 
-	actual, err := utils.ExtractContainerID(read)
-
-	if err != nil {
-		t.Fatalf("Could not extract container id : %v", err)
-	}
+	actual := utils.extractContainerIDFromCGroups(read)
 
-	if actual != expected {
-		t.Fatalf("id mismatch: actual %v, expected %v", actual, expected)
-	}
+	assert.Equal(t, expected, actual)
 }
 
 func TestExtractECSDockerId(t *testing.T) {
@@ -164,15 +166,9 @@ func TestExtractECSDockerId(t *testing.T) {
 
 	utils := dockerUtils{}
 
-	actual, err := utils.ExtractContainerID(read)
+	actual := utils.extractContainerIDFromCGroups(read)
 
-	if err != nil {
-		t.Fatalf("Could not extract container id : %v", err)
-	}
-
-	if actual != expected {
-		t.Fatalf("id mismatch: actual %v, expected %v", actual, expected)
-	}
+	assert.Equal(t, expected, actual)
 }
 
 func TestExtractRootlessDockerId(t *testing.T) {
@@ -195,13 +191,7 @@ func TestExtractRootlessDockerId(t *testing.T) {
 
 	utils := dockerUtils{}
 
-	actual, err := utils.ExtractContainerID(read)
-
-	if err != nil {
-		t.Fatalf("Could not extract container id : %v", err)
-	}
+	actual := utils.extractContainerIDFromCGroups(read)
 
-	if actual != expected {
-		t.Fatalf("id mismatch: actual %v, expected %v", actual, expected)
-	}
+	assert.Equal(t, expected, actual)
 }