From 0ac64ab56413a595f3ebcfc99bc133d9118b688b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D7=A0=CF=85=CE=B1=CE=B7=20=D7=A0=CF=85=CE=B1=CE=B7=D1=95?= =?UTF-8?q?=CF=83=CE=B7?= Date: Thu, 2 Apr 2026 19:07:00 -0700 Subject: [PATCH] feat(docker): add hostname and dns to container create MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add optional hostname and dns fields to the Docker container create flow across all layers: OpenAPI spec, job types, provider, agent processor, API handler, SDK types, and SDK client. Both fields are validated at the API layer (hostname max 253 chars, dns entries must be valid IPs). Tests added at every layer. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../api/post-node-container-docker.api.mdx | 20 ++- internal/agent/processor_docker.go | 2 + .../agent/processor_docker_public_test.go | 34 +++++ internal/controller/api/gen/api.yaml | 16 +++ .../api/node/docker/container_create.go | 6 + .../docker/container_create_public_test.go | 67 +++++++++ .../controller/api/node/docker/gen/api.yaml | 14 ++ .../api/node/docker/gen/docker.gen.go | 6 + internal/job/types.go | 2 + internal/provider/container/docker/docker.go | 8 ++ .../container/docker/docker_public_test.go | 128 ++++++++++++++++++ internal/provider/container/docker/types.go | 4 + pkg/sdk/client/docker.go | 6 + pkg/sdk/client/docker_public_test.go | 29 ++++ pkg/sdk/client/docker_types.go | 4 + pkg/sdk/client/gen/client.gen.go | 6 + 16 files changed, 351 insertions(+), 1 deletion(-) diff --git a/docs/docs/gen/api/post-node-container-docker.api.mdx b/docs/docs/gen/api/post-node-container-docker.api.mdx index a6f68a74b..faa7ae9c9 100644 --- a/docs/docs/gen/api/post-node-container-docker.api.mdx +++ b/docs/docs/gen/api/post-node-container-docker.api.mdx @@ -5,7 +5,7 @@ description: "Create a new container on the target node. Returns a job ID for tr sidebar_label: "Create a container" hide_title: true hide_table_of_contents: true -api: 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 +api: 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 sidebar_class_name: "post api-method" info_path: gen/api/agent-management-api custom_edit_url: null @@ -134,6 +134,24 @@ Create a new container on the target node. Returns a job ID for tracking the asy schema={{"type":"string","description":"Optional name for the container.","example":"my-nginx","x-oapi-codegen-extra-tags":{"validate":"omitempty,min=1"}}} > + + + + 0 { config.Cmd = params.Command } @@ -88,6 +92,10 @@ func (d *Client) Create( // Build host configuration hostConfig := &container.HostConfig{} + if len(params.DNS) > 0 { + hostConfig.DNS = params.DNS + } + // Convert port mappings if len(params.Ports) > 0 { portBindings := nat.PortMap{} diff --git a/internal/provider/container/docker/docker_public_test.go b/internal/provider/container/docker/docker_public_test.go index cac9c1283..1345ac929 100644 --- a/internal/provider/container/docker/docker_public_test.go +++ b/internal/provider/container/docker/docker_public_test.go @@ -432,6 +432,134 @@ func (s *DockerDriverPublicTestSuite) TestCreate() { s.Equal("vols-id", c.ID) }, }, + { + name: "with hostname", + setupMock: func( + ctrl *gomock.Controller, + ) *dockermocks.MockAPIClient { + m := dockermocks.NewMockAPIClient(ctrl) + m.EXPECT(). + ContainerCreate( + gomock.Any(), + gomock.AssignableToTypeOf(&container.Config{}), + gomock.Any(), + gomock.Any(), + gomock.Any(), + gomock.Any(), + ). + DoAndReturn(func( + _ context.Context, + config *container.Config, + _ *container.HostConfig, + _ *network.NetworkingConfig, + _ *ocispec.Platform, + _ string, + ) (container.CreateResponse, error) { + s.Equal("web-01", config.Hostname) + return container.CreateResponse{ID: "hostname-id"}, nil + }) + return m + }, + params: dockerprov.CreateParams{ + Image: "nginx:latest", + Name: "test-hostname", + Hostname: "web-01", + }, + validateFunc: func( + c *dockerprov.Container, + err error, + ) { + s.NoError(err) + s.NotNil(c) + s.Equal("hostname-id", c.ID) + }, + }, + { + name: "with dns", + setupMock: func( + ctrl *gomock.Controller, + ) *dockermocks.MockAPIClient { + m := dockermocks.NewMockAPIClient(ctrl) + m.EXPECT(). + ContainerCreate( + gomock.Any(), + gomock.Any(), + gomock.AssignableToTypeOf(&container.HostConfig{}), + gomock.Any(), + gomock.Any(), + gomock.Any(), + ). + DoAndReturn(func( + _ context.Context, + _ *container.Config, + hostConfig *container.HostConfig, + _ *network.NetworkingConfig, + _ *ocispec.Platform, + _ string, + ) (container.CreateResponse, error) { + s.Equal([]string{"8.8.8.8", "8.8.4.4"}, hostConfig.DNS) + return container.CreateResponse{ID: "dns-id"}, nil + }) + return m + }, + params: dockerprov.CreateParams{ + Image: "nginx:latest", + Name: "test-dns", + DNS: []string{"8.8.8.8", "8.8.4.4"}, + }, + validateFunc: func( + c *dockerprov.Container, + err error, + ) { + s.NoError(err) + s.NotNil(c) + s.Equal("dns-id", c.ID) + }, + }, + { + name: "with hostname and dns", + setupMock: func( + ctrl *gomock.Controller, + ) *dockermocks.MockAPIClient { + m := dockermocks.NewMockAPIClient(ctrl) + m.EXPECT(). + ContainerCreate( + gomock.Any(), + gomock.AssignableToTypeOf(&container.Config{}), + gomock.AssignableToTypeOf(&container.HostConfig{}), + gomock.Any(), + gomock.Any(), + gomock.Any(), + ). + DoAndReturn(func( + _ context.Context, + config *container.Config, + hostConfig *container.HostConfig, + _ *network.NetworkingConfig, + _ *ocispec.Platform, + _ string, + ) (container.CreateResponse, error) { + s.Equal("web-01", config.Hostname) + s.Equal([]string{"1.1.1.1"}, hostConfig.DNS) + return container.CreateResponse{ID: "both-id"}, nil + }) + return m + }, + params: dockerprov.CreateParams{ + Image: "nginx:latest", + Name: "test-both", + Hostname: "web-01", + DNS: []string{"1.1.1.1"}, + }, + validateFunc: func( + c *dockerprov.Container, + err error, + ) { + s.NoError(err) + s.NotNil(c) + s.Equal("both-id", c.ID) + }, + }, { name: "returns error when container create fails", setupMock: func( diff --git a/internal/provider/container/docker/types.go b/internal/provider/container/docker/types.go index ee3625775..fafcd6d6d 100644 --- a/internal/provider/container/docker/types.go +++ b/internal/provider/container/docker/types.go @@ -70,6 +70,10 @@ type CreateParams struct { Image string `json:"image"` // Name is an optional container name. Name string `json:"name,omitempty"` + // Hostname sets the container hostname. + Hostname string `json:"hostname,omitempty"` + // DNS sets custom DNS servers for the container. + DNS []string `json:"dns,omitempty"` // Command overrides the image's default command. Command []string `json:"command,omitempty"` // Env sets environment variables. diff --git a/pkg/sdk/client/docker.go b/pkg/sdk/client/docker.go index 56d7f2bac..97c6896bf 100644 --- a/pkg/sdk/client/docker.go +++ b/pkg/sdk/client/docker.go @@ -44,6 +44,12 @@ func (s *DockerService) Create( if opts.Name != "" { body.Name = &opts.Name } + if opts.Hostname != "" { + body.Hostname = &opts.Hostname + } + if len(opts.DNS) > 0 { + body.Dns = &opts.DNS + } if len(opts.Command) > 0 { body.Command = &opts.Command } diff --git a/pkg/sdk/client/docker_public_test.go b/pkg/sdk/client/docker_public_test.go index 165f332db..193a339eb 100644 --- a/pkg/sdk/client/docker_public_test.go +++ b/pkg/sdk/client/docker_public_test.go @@ -117,6 +117,35 @@ func (suite *DockerPublicTestSuite) TestCreate() { suite.Equal("my-app", resp.Data.Results[0].Name) }, }, + { + name: "when creating container with hostname and dns returns result", + handler: func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + _, _ = w.Write( + []byte( + `{"job_id":"00000000-0000-0000-0000-000000000003","results":[{"hostname":"web-01","id":"ghi789","name":"my-dns","image":"nginx:latest","state":"running","created":"2026-01-01T00:00:00Z","changed":true}]}`, + ), + ) + }, + opts: client.DockerCreateOpts{ + Image: "nginx:latest", + Name: "my-dns", + Hostname: "web-01", + DNS: []string{"8.8.8.8", "8.8.4.4"}, + }, + validateFunc: func( + resp *client.Response[client.Collection[client.DockerResult]], + err error, + ) { + suite.NoError(err) + suite.NotNil(resp) + suite.Equal("00000000-0000-0000-0000-000000000003", resp.Data.JobID) + suite.Len(resp.Data.Results, 1) + suite.Equal("ghi789", resp.Data.Results[0].ID) + suite.Equal("my-dns", resp.Data.Results[0].Name) + }, + }, { name: "when server returns 403 returns AuthError", handler: func(w http.ResponseWriter, _ *http.Request) { diff --git a/pkg/sdk/client/docker_types.go b/pkg/sdk/client/docker_types.go index 890239af1..758813403 100644 --- a/pkg/sdk/client/docker_types.go +++ b/pkg/sdk/client/docker_types.go @@ -30,6 +30,10 @@ type DockerCreateOpts struct { Image string // Name is an optional container name. Name string + // Hostname sets the container hostname. + Hostname string + // DNS sets custom DNS servers for the container. + DNS []string // Command overrides the image's default command. Command []string // Env is environment variables in KEY=VALUE format. diff --git a/pkg/sdk/client/gen/client.gen.go b/pkg/sdk/client/gen/client.gen.go index d4d5952d0..c0ebea1d3 100644 --- a/pkg/sdk/client/gen/client.gen.go +++ b/pkg/sdk/client/gen/client.gen.go @@ -1107,9 +1107,15 @@ type DockerCreateRequest struct { // Command Command to run in the container. Command *[]string `json:"command,omitempty"` + // Dns Custom DNS servers for the container. + Dns *[]string `json:"dns,omitempty" validate:"omitempty,dive,ip"` + // Env Environment variables in KEY=VALUE format. Env *[]string `json:"env,omitempty"` + // Hostname Container hostname. + Hostname *string `json:"hostname,omitempty" validate:"omitempty,min=1,max=253"` + // Image Container image reference (e.g., "nginx:latest"). Image string `json:"image" validate:"required,min=1"`