From c6b5c1a485c155530e6487b1121c0a58dc000b4b Mon Sep 17 00:00:00 2001 From: JamBalaya56562 Date: Mon, 13 Jul 2026 19:25:40 +0900 Subject: [PATCH 1/2] feat: expose current container to templates Co-Authored-By: Claude Opus 4.8 --- README.md | 2 + internal/context/context.go | 13 ++ internal/context/context_test.go | 20 +++ internal/generator/generator.go | 225 +++++++++++++++------------ internal/generator/generator_test.go | 56 +++++++ 5 files changed, 218 insertions(+), 98 deletions(-) diff --git a/README.md b/README.md index a9cd8787..6ecd683d 100644 --- a/README.md +++ b/README.md @@ -370,6 +370,8 @@ type Docker struct { // Host environment variables accessible from root in templates as .Env ``` +The root also exposes `.CurrentContainer`, the `RuntimeContainer` of the docker-gen container itself (or `nil` if it cannot be determined). Like `.Docker`, it is resolved separately from the container list and does not appear in the containers the templates iterate over, so it stays available even when `-only-exposed`/`-only-published` filters the docker-gen container out. + For example, this is a JSON version of an emitted RuntimeContainer struct: ```json diff --git a/internal/context/context.go b/internal/context/context.go index f4bf8c61..0536beab 100644 --- a/internal/context/context.go +++ b/internal/context/context.go @@ -16,6 +16,7 @@ var ( mu sync.RWMutex dockerInfo Docker dockerEnv *docker.Env + currentContainer *RuntimeContainer hostnameRegex = regexp.MustCompilePOSIX("^[[:alnum:]]{12}$") mountinfoPrefixRegex = regexp.MustCompilePOSIX("^[0-9]+ [0-9]+ [0-9]+:[0-9]+ /") ) @@ -32,6 +33,12 @@ func (c *Context) Docker() Docker { return dockerInfo } +func (c *Context) CurrentContainer() *RuntimeContainer { + mu.RLock() + defer mu.RUnlock() + return currentContainer +} + func SetServerInfo(d *docker.DockerInfo) { mu.Lock() defer mu.Unlock() @@ -54,6 +61,12 @@ func SetDockerEnv(d *docker.Env) { dockerEnv = d } +func SetCurrentContainer(c *RuntimeContainer) { + mu.Lock() + defer mu.Unlock() + currentContainer = c +} + type Network struct { IP string Name string diff --git a/internal/context/context_test.go b/internal/context/context_test.go index bb629066..206b73bf 100644 --- a/internal/context/context_test.go +++ b/internal/context/context_test.go @@ -246,6 +246,26 @@ func TestGetCurrentContainerEmpty(t *testing.T) { assert.Equal(t, "", GetCurrentContainerID()) } +func TestCurrentContainer(t *testing.T) { + mu.Lock() + saved := currentContainer + mu.Unlock() + t.Cleanup(func() { + mu.Lock() + currentContainer = saved + mu.Unlock() + }) + + var c Context + + rc := &RuntimeContainer{ID: "abc"} + SetCurrentContainer(rc) + assert.Same(t, rc, c.CurrentContainer()) + + SetCurrentContainer(nil) + assert.Nil(t, c.CurrentContainer()) +} + func TestRuntimeContainerEquals(t *testing.T) { rc1 := &RuntimeContainer{ ID: "baz", diff --git a/internal/generator/generator.go b/internal/generator/generator.go index e9c7e254..42f3e55d 100644 --- a/internal/generator/generator.go +++ b/internal/generator/generator.go @@ -396,6 +396,8 @@ func sortNetworks(networks []context.Network) { }) } +var getCurrentContainerID = context.GetCurrentContainerID + func (g *generator) getContainers(config config.Config) ([]*context.RuntimeContainer, error) { apiInfo, err := g.Client.Info() if err != nil { @@ -424,122 +426,149 @@ func (g *generator) getContainers(config config.Config) ([]*context.RuntimeConta containers := []*context.RuntimeContainer{} for _, apiContainer := range apiContainers { - opts := docker.InspectContainerOptions{ID: apiContainer.ID} - container, err := g.Client.InspectContainerWithOptions(opts) + runtimeContainer, err := g.inspectContainer(apiContainer.ID, networks) if err != nil { log.Printf("Error inspecting container: %s: %s\n", apiContainer.ID, err) continue } + containers = append(containers, runtimeContainer) + } - // Inspect may return nil pointers for these structs; copy into zero values to avoid a panic. - var containerConfig docker.Config - if container.Config != nil { - containerConfig = *container.Config - } - var containerNetSettings docker.NetworkSettings - if container.NetworkSettings != nil { - containerNetSettings = *container.NetworkSettings - } - var containerHostConfig docker.HostConfig - if container.HostConfig != nil { - containerHostConfig = *container.HostConfig - } + context.SetCurrentContainer(g.currentContainer(containers, networks)) + return containers, nil +} - registry, repository, tag := dockerclient.SplitDockerImage(containerConfig.Image) - runtimeContainer := &context.RuntimeContainer{ - ID: container.ID, - Created: container.Created, - Image: context.DockerImage{ - Registry: registry, - Repository: repository, - Tag: tag, - }, - State: context.State{ - Running: container.State.Running, - Health: context.Health{ - Status: container.State.Health.Status, - }, - }, - Name: strings.TrimLeft(container.Name, "/"), - Hostname: containerConfig.Hostname, - Gateway: containerNetSettings.Gateway, - NetworkMode: containerHostConfig.NetworkMode, - Addresses: []context.Address{}, - Networks: []context.Network{}, - Devices: []context.Device{}, - Env: make(map[string]string), - Volumes: make(map[string]context.Volume), - Node: context.SwarmNode{}, - Labels: make(map[string]string), - IP: containerNetSettings.IPAddress, - IP6LinkLocal: containerNetSettings.LinkLocalIPv6Address, - IP6Global: containerNetSettings.GlobalIPv6Address, +func (g *generator) currentContainer(containers []*context.RuntimeContainer, networks map[string]docker.Network) *context.RuntimeContainer { + currentID := getCurrentContainerID() + if currentID == "" { + return nil + } + for _, c := range containers { + if c.ID == currentID { + return c } + } + runtimeContainer, err := g.inspectContainer(currentID, networks) + if err != nil { + log.Printf("Error inspecting current container: %s: %s\n", currentID, err) + return nil + } + return runtimeContainer +} - addresses := context.GetContainerAddresses(container) - runtimeContainer.Addresses = append(runtimeContainer.Addresses, addresses...) - - for k, v := range containerNetSettings.Networks { - network := context.Network{ - IP: v.IPAddress, - Name: k, - Aliases: append([]string{}, v.Aliases...), - Gateway: v.Gateway, - EndpointID: v.EndpointID, - IPv6Gateway: v.IPv6Gateway, - GlobalIPv6Address: v.GlobalIPv6Address, - MacAddress: v.MacAddress, - GlobalIPv6PrefixLen: v.GlobalIPv6PrefixLen, - IPPrefixLen: v.IPPrefixLen, - Internal: networks[k].Internal, - } +func (g *generator) inspectContainer(id string, networks map[string]docker.Network) (*context.RuntimeContainer, error) { + opts := docker.InspectContainerOptions{ID: id} + container, err := g.Client.InspectContainerWithOptions(opts) + if err != nil { + return nil, err + } - runtimeContainer.Networks = append(runtimeContainer.Networks, - network) - } + // Inspect may return nil pointers for these structs; copy into zero values to avoid a panic. + var containerConfig docker.Config + if container.Config != nil { + containerConfig = *container.Config + } + var containerNetSettings docker.NetworkSettings + if container.NetworkSettings != nil { + containerNetSettings = *container.NetworkSettings + } + var containerHostConfig docker.HostConfig + if container.HostConfig != nil { + containerHostConfig = *container.HostConfig + } - sortNetworks(runtimeContainer.Networks) + registry, repository, tag := dockerclient.SplitDockerImage(containerConfig.Image) + runtimeContainer := &context.RuntimeContainer{ + ID: container.ID, + Created: container.Created, + Image: context.DockerImage{ + Registry: registry, + Repository: repository, + Tag: tag, + }, + State: context.State{ + Running: container.State.Running, + Health: context.Health{ + Status: container.State.Health.Status, + }, + }, + Name: strings.TrimLeft(container.Name, "/"), + Hostname: containerConfig.Hostname, + Gateway: containerNetSettings.Gateway, + NetworkMode: containerHostConfig.NetworkMode, + Addresses: []context.Address{}, + Networks: []context.Network{}, + Devices: []context.Device{}, + Env: make(map[string]string), + Volumes: make(map[string]context.Volume), + Node: context.SwarmNode{}, + Labels: make(map[string]string), + IP: containerNetSettings.IPAddress, + IP6LinkLocal: containerNetSettings.LinkLocalIPv6Address, + IP6Global: containerNetSettings.GlobalIPv6Address, + } - for k, v := range container.Volumes { - runtimeContainer.Volumes[k] = context.Volume{ - Path: k, - HostPath: v, - ReadWrite: container.VolumesRW[k], - } - } - if container.Node != nil { - runtimeContainer.Node.ID = container.Node.ID - runtimeContainer.Node.Name = container.Node.Name - runtimeContainer.Node.Address = context.Address{ - IP: container.Node.IP, - } + addresses := context.GetContainerAddresses(container) + runtimeContainer.Addresses = append(runtimeContainer.Addresses, addresses...) + + for k, v := range containerNetSettings.Networks { + network := context.Network{ + IP: v.IPAddress, + Name: k, + Aliases: append([]string{}, v.Aliases...), + Gateway: v.Gateway, + EndpointID: v.EndpointID, + IPv6Gateway: v.IPv6Gateway, + GlobalIPv6Address: v.GlobalIPv6Address, + MacAddress: v.MacAddress, + GlobalIPv6PrefixLen: v.GlobalIPv6PrefixLen, + IPPrefixLen: v.IPPrefixLen, + Internal: networks[k].Internal, } - for _, v := range container.Mounts { - runtimeContainer.Mounts = append(runtimeContainer.Mounts, context.Mount{ - Name: v.Name, - Source: v.Source, - Destination: v.Destination, - Driver: v.Driver, - Mode: v.Mode, - RW: v.RW, - }) - } + runtimeContainer.Networks = append(runtimeContainer.Networks, + network) + } + + sortNetworks(runtimeContainer.Networks) - for _, v := range containerHostConfig.Devices { - runtimeContainer.Devices = append(runtimeContainer.Devices, context.Device{ - PathOnHost: v.PathOnHost, - PathInContainer: v.PathInContainer, - Permissions: v.CgroupPermissions, - }) + for k, v := range container.Volumes { + runtimeContainer.Volumes[k] = context.Volume{ + Path: k, + HostPath: v, + ReadWrite: container.VolumesRW[k], } + } + if container.Node != nil { + runtimeContainer.Node.ID = container.Node.ID + runtimeContainer.Node.Name = container.Node.Name + runtimeContainer.Node.Address = context.Address{ + IP: container.Node.IP, + } + } - runtimeContainer.Env = utils.SplitKeyValueSlice(containerConfig.Env) - runtimeContainer.Labels = containerConfig.Labels - containers = append(containers, runtimeContainer) + for _, v := range container.Mounts { + runtimeContainer.Mounts = append(runtimeContainer.Mounts, context.Mount{ + Name: v.Name, + Source: v.Source, + Destination: v.Destination, + Driver: v.Driver, + Mode: v.Mode, + RW: v.RW, + }) + } + + for _, v := range containerHostConfig.Devices { + runtimeContainer.Devices = append(runtimeContainer.Devices, context.Device{ + PathOnHost: v.PathOnHost, + PathInContainer: v.PathInContainer, + Permissions: v.CgroupPermissions, + }) } - return containers, nil + runtimeContainer.Env = utils.SplitKeyValueSlice(containerConfig.Env) + runtimeContainer.Labels = containerConfig.Labels + return runtimeContainer, nil } func newSignalChannel() (<-chan os.Signal, func()) { diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go index 845078f3..fa25538c 100644 --- a/internal/generator/generator_test.go +++ b/internal/generator/generator_test.go @@ -248,3 +248,59 @@ func TestGetContainersDevices(t *testing.T) { {PathOnHost: "/dev/ttyACM0", PathInContainer: "/dev/ttyUSB0", Permissions: "rwm"}, }, containers[0].Devices) } + +func TestGetContainersSetsCurrentContainer(t *testing.T) { + orig := log.Writer() + log.SetOutput(io.Discard) + t.Cleanup(func() { log.SetOutput(orig) }) + + currentID := "current123456789" + saved := getCurrentContainerID + getCurrentContainerID = func(...string) string { return currentID } + t.Cleanup(func() { getCurrentContainerID = saved }) + t.Cleanup(func() { context.SetCurrentContainer(nil) }) + + server, err := dockertest.NewServer("127.0.0.1:0", nil, nil) + if err != nil { + t.Fatalf("failed to create test server: %s", err) + } + t.Cleanup(server.Stop) + server.CustomHandler("/info", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"Containers":0,"Images":0,"NFd":11,"NGoroutines":21}`)) + })) + server.CustomHandler("/version", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"Version":"19.03.12","Os":"Linux","GoVersion":"go1.13.14","Arch":"amd64","ApiVersion":"1.40"}`)) + })) + server.CustomHandler("/networks", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("[]")) + })) + server.CustomHandler("/containers/json", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("[]")) + })) + server.CustomHandler(fmt.Sprintf("/containers/%s/json", currentID), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(docker.Container{ID: currentID, Name: "/self"}) + })) + + serverURL := fmt.Sprintf("tcp://%s", strings.TrimRight(strings.TrimPrefix(server.URL(), "http://"), "/")) + client, err := dockerclient.NewDockerClient(serverURL, false, "", "", "") + if err != nil { + t.Fatalf("failed to create client: %s", err) + } + client.SkipServerVersionCheck = true + + apiVersion, err := client.Version() + if err != nil { + t.Fatalf("failed to retrieve version: %s", err) + } + context.SetDockerEnv(apiVersion) + + g := &generator{Client: client, Endpoint: serverURL} + containers, err := g.getContainers(config.Config{}) + assert.NoError(t, err) + assert.Empty(t, containers) + + var emptyCtx context.Context + current := emptyCtx.CurrentContainer() + assert.NotNil(t, current) + assert.Equal(t, currentID, current.ID) +} From 7310506aa581d4e42a5d2479c9cf2419d049e7b6 Mon Sep 17 00:00:00 2001 From: Nicolas Duchon Date: Tue, 14 Jul 2026 11:05:46 +0200 Subject: [PATCH 2/2] docs: reword .CurrentContainer explanation Co-authored-by: Nicolas Duchon --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6ecd683d..d4007c7b 100644 --- a/README.md +++ b/README.md @@ -370,7 +370,7 @@ type Docker struct { // Host environment variables accessible from root in templates as .Env ``` -The root also exposes `.CurrentContainer`, the `RuntimeContainer` of the docker-gen container itself (or `nil` if it cannot be determined). Like `.Docker`, it is resolved separately from the container list and does not appear in the containers the templates iterate over, so it stays available even when `-only-exposed`/`-only-published` filters the docker-gen container out. +The root also exposes `.CurrentContainer`, the `RuntimeContainer` of the docker-gen container itself (or `nil` if it cannot be determined). Like `.Docker`, it is resolved independently from the container list, so it remains available even when `-only-exposed`/`-only-published` would filter the docker-gen container out; depending on filters, it may also be present in the containers the templates iterate over. For example, this is a JSON version of an emitted RuntimeContainer struct: