Files
uncloud/internal/machine/caddyconfig/caddyfile_test.go
T

1021 lines
24 KiB
Go

package caddyconfig
import (
"context"
"errors"
"regexp"
"strings"
"testing"
"time"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/psviderski/uncloud/internal/machine/docker"
"github.com/psviderski/uncloud/internal/machine/store"
"github.com/psviderski/uncloud/pkg/api"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
// generatedTimestampRegex matches the "(DO NOT EDIT): <RFC3339 timestamp>" part of the header.
var generatedTimestampRegex = regexp.MustCompile(`\(DO NOT EDIT\): \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z`)
// normaliseGeneratedTimestamp replaces dynamic timestamps with a placeholder for test comparison.
func normaliseGeneratedTimestamp(caddyfile string) string {
return generatedTimestampRegex.ReplaceAllString(caddyfile, "(DO NOT EDIT): TIMESTAMP_PLACEHOLDER")
}
const testCaddyfileHeader = `# Caddyfile autogenerated by Uncloud (DO NOT EDIT): TIMESTAMP_PLACEHOLDER
# Automatically updated on service or health status changes.
# Docs: https://uncloud.run/docs/concepts/ingress/overview
# Health check endpoint to verify Caddy reachability on this machine.
http:// {
handle /.uncloud-verify {
respond "test-machine-id" 200
}
log
}
(common_proxy) {
# Retry failed requests up to lb_retries times against other available upstreams.
lb_retries 3
# Upstreams are marked unhealthy for fail_duration after a failed request (passive health checking).
fail_duration 30s
}
`
func TestCaddyfileGenerator(t *testing.T) {
tests := []struct {
name string
containers []store.ContainerRecord
want string
wantErr bool
}{
{
name: "empty containers",
containers: []store.ContainerRecord{},
want: testCaddyfileHeader,
},
{
name: "HTTP container",
containers: []store.ContainerRecord{
newContainerRecord(newContainer("10.210.0.2", "app.example.com:8080/http"), "mach1"),
},
want: testCaddyfileHeader + `
# Sites generated from service ports.
http://app.example.com {
reverse_proxy 10.210.0.2:8080 {
import common_proxy
}
log
}
`,
},
{
name: "load balancing multiple containers",
containers: []store.ContainerRecord{
newContainerRecord(newContainer("10.210.0.2", "app.example.com:8080/http"), "mach1"),
newContainerRecord(newContainer("10.210.0.3", "app.example.com:8080/http"), "mach1"),
},
want: testCaddyfileHeader + `
# Sites generated from service ports.
http://app.example.com {
reverse_proxy 10.210.0.2:8080 10.210.0.3:8080 {
import common_proxy
}
log
}
`,
},
{
name: "HTTPS container",
containers: []store.ContainerRecord{
newContainerRecord(newContainer("10.210.0.2", "secure.example.com:8000/https"), "mach1"),
},
want: testCaddyfileHeader + `
# Sites generated from service ports.
https://secure.example.com {
reverse_proxy 10.210.0.2:8000 {
import common_proxy
}
log
}
`,
},
{
name: "mixed HTTP and HTTPS",
containers: []store.ContainerRecord{
newContainerRecord(
newContainer("10.210.0.2",
"app.example.com:8080/http",
"web.example.com:8000/http"),
"mach1",
),
newContainerRecord(
newContainer("10.210.0.3",
"app.example.com:8080/http",
"secure.example.com:8888/https"),
"mach1",
),
newContainerRecord(
newContainer("10.210.0.4",
"web.example.com:8000/http",
"secure.example.com:8888/https"),
"mach1",
),
newContainerRecord(
newContainer("10.210.0.5",
"app.example.com:8080/http",
"web.example.com:8000/http",
"secure.example.com:8888/https"),
"mach1",
),
},
want: testCaddyfileHeader + `
# Sites generated from service ports.
http://app.example.com {
reverse_proxy 10.210.0.2:8080 10.210.0.3:8080 10.210.0.5:8080 {
import common_proxy
}
log
}
http://web.example.com {
reverse_proxy 10.210.0.2:8000 10.210.0.4:8000 10.210.0.5:8000 {
import common_proxy
}
log
}
https://secure.example.com {
reverse_proxy 10.210.0.3:8888 10.210.0.4:8888 10.210.0.5:8888 {
import common_proxy
}
log
}
`,
},
{
name: "container without uncloud network ignored",
containers: []store.ContainerRecord{
newContainerRecord(newContainerWithoutNetwork("ignored.example.com:8080/http"), "mach1"),
},
want: testCaddyfileHeader,
},
{
name: "container with invalid port ignored",
containers: []store.ContainerRecord{
newContainerRecord(newContainer("10.210.0.2", "invalid-port"), "mach1"),
},
want: testCaddyfileHeader,
},
{
name: "containers with unsupported protocols and host mode ignored",
containers: []store.ContainerRecord{
newContainerRecord(newContainer("10.210.0.2", "5000/tcp"), "mach1"),
newContainerRecord(newContainer("10.210.0.3", "5000/udp"), "mach1"),
newContainerRecord(newContainer("10.210.0.4", "80:8080/tcp@host"), "mach1"),
},
want: testCaddyfileHeader,
},
}
ctx := context.Background()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Validator is not expected to be called in these tests.
generator := NewCaddyfileGenerator("test-machine-id", nil, nil)
config, err := generator.Generate(ctx, tt.containers, true)
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tt.want, normaliseGeneratedTimestamp(config), "Generated Caddyfile doesn't match")
})
}
}
func TestCaddyfileGeneratorWithCustomConfigs(t *testing.T) {
tests := []struct {
name string
containers []store.ContainerRecord
want string
wantErr bool
}{
{
name: "caddy service with valid global config",
containers: []store.ContainerRecord{
newContainerRecordWithCaddyConfig(
"caddy",
"10.210.0.2",
`# Global Caddy configuration
{
global directive
}`,
"test-machine-id",
time.Now(),
),
},
want: `# Caddyfile autogenerated by Uncloud (DO NOT EDIT): TIMESTAMP_PLACEHOLDER
# Automatically updated on service or health status changes.
# Docs: https://uncloud.run/docs/concepts/ingress/overview
# User-defined global config from service 'caddy'.
# Global Caddy configuration
{
global directive
}
# Health check endpoint to verify Caddy reachability on this machine.
http:// {
handle /.uncloud-verify {
respond "test-machine-id" 200
}
log
}
(common_proxy) {
# Retry failed requests up to lb_retries times against other available upstreams.
lb_retries 3
# Upstreams are marked unhealthy for fail_duration after a failed request (passive health checking).
fail_duration 30s
}
`,
},
{
name: "regular service with valid custom config",
containers: []store.ContainerRecord{
newContainerRecordWithCaddyConfig(
"web",
"10.210.0.2",
`# Custom config for web service
web.example.com {
reverse_proxy web:3000
}`,
"test-machine-id",
time.Now(),
),
},
want: testCaddyfileHeader + `
# User-defined config for service 'web'.
# Custom config for web service
web.example.com {
reverse_proxy web:3000
}
`,
},
{
name: "service with invalid config is skipped",
containers: []store.ContainerRecord{
newContainerRecordWithCaddyConfig(
"bad-service",
"10.210.0.2",
`# test:invalid
bad.config.com {
respond "This config is invalid"
}`,
"test-machine-id",
time.Now(),
),
},
want: testCaddyfileHeader + `
# Skipped invalid user-defined configs:
# - service 'bad-service': validation failed: invalid config detected
`,
},
{
name: "service with invalid config template is skipped",
containers: []store.ContainerRecord{
newContainerRecordWithCaddyConfig(
"bad-template",
"10.210.0.2",
`
bad.template.com {
reverse_proxy {{upstreams
}`,
"test-machine-id",
time.Now(),
),
},
want: testCaddyfileHeader + `
# Skipped invalid user-defined configs:
# - service 'bad-template': failed to render template: parse config as Go template: template: Caddyfile:3: unexpected "}" in operand
`,
},
{
name: "caddy service with invalid global config is skipped",
containers: []store.ContainerRecord{
newContainerRecordWithCaddyConfig(
"caddy",
"10.210.0.2",
`# test:invalid
localhost {
respond "Invalid global config"
}`,
"test-machine-id",
time.Now(),
),
},
want: testCaddyfileHeader + `
# Skipped invalid user-defined configs:
# - service 'caddy': validation failed: invalid config detected
`,
},
{
name: "caddy service on different machine is ignored",
containers: []store.ContainerRecord{
newContainerRecordWithCaddyConfig(
"caddy",
"10.210.0.2",
`# Global config from other machine
{
global directive
}`,
"other-machine-id",
time.Now(),
),
},
want: testCaddyfileHeader,
},
{
name: "multiple services with mixed valid and invalid configs",
containers: []store.ContainerRecord{
newContainerRecordWithCaddyConfig(
"api",
"10.210.0.2",
`api.example.com {
reverse_proxy api:8080
}`,
"test-machine-id",
time.Now(),
),
newContainerRecordWithCaddyConfig(
"invalid-svc",
"10.210.0.3",
`# test:invalid
bad.example.com {
respond "Invalid"
}`,
"test-machine-id",
time.Now(),
),
newContainerRecordWithCaddyConfig(
"web",
"10.210.0.4",
`web.example.com {
reverse_proxy web:3000
}`,
"test-machine-id",
time.Now(),
),
},
want: testCaddyfileHeader + `
# User-defined config for service 'api'.
api.example.com {
reverse_proxy api:8080
}
# User-defined config for service 'web'.
web.example.com {
reverse_proxy web:3000
}
# Skipped invalid user-defined configs:
# - service 'invalid-svc': validation failed: invalid config detected
`,
},
{
name: "combined: caddy global config + service configs + ports",
containers: []store.ContainerRecord{
newContainerRecordWithCaddyConfig(
"caddy",
"10.210.0.1",
`# Global config
{
global directive
}`,
"test-machine-id",
time.Now(),
),
newContainerRecordWithPorts(
"app",
"10.210.0.2",
[]string{"app.example.com:8080/http"},
"test-machine-id",
),
newContainerRecordWithCaddyConfig(
"api",
"10.210.0.3",
`api.example.com {
reverse_proxy api:8000
}`,
"other-machine-id",
time.Now(),
),
},
want: `# Caddyfile autogenerated by Uncloud (DO NOT EDIT): TIMESTAMP_PLACEHOLDER
# Automatically updated on service or health status changes.
# Docs: https://uncloud.run/docs/concepts/ingress/overview
# User-defined global config from service 'caddy'.
# Global config
{
global directive
}
# Health check endpoint to verify Caddy reachability on this machine.
http:// {
handle /.uncloud-verify {
respond "test-machine-id" 200
}
log
}
(common_proxy) {
# Retry failed requests up to lb_retries times against other available upstreams.
lb_retries 3
# Upstreams are marked unhealthy for fail_duration after a failed request (passive health checking).
fail_duration 30s
}
# Sites generated from service ports.
http://app.example.com {
reverse_proxy 10.210.0.2:8080 {
import common_proxy
}
log
}
# User-defined config for service 'api'.
api.example.com {
reverse_proxy api:8000
}
`,
},
{
name: "service with template directives using upstreams",
containers: []store.ContainerRecord{
newContainerRecordWithCaddyConfig(
"web",
"10.210.0.2",
`web.example.com {
reverse_proxy {{upstreams}}
}`,
"test-machine-id",
time.Now(),
),
newContainerRecordWithPorts(
"api",
"10.210.0.3",
[]string{"api.example.com:8080/http"},
"test-machine-id",
),
},
want: testCaddyfileHeader + `
# Sites generated from service ports.
http://api.example.com {
reverse_proxy 10.210.0.3:8080 {
import common_proxy
}
log
}
# User-defined config for service 'web'.
web.example.com {
reverse_proxy 10.210.0.2
}
`,
},
{
name: "only most recent container config is used per service",
containers: []store.ContainerRecord{
newContainerRecordWithCaddyConfig(
"web",
"10.210.0.2",
`# Old config
old.example.com {
respond "Old"
}`,
"test-machine-id",
time.Now().Add(-1*time.Hour),
),
newContainerRecordWithCaddyConfig(
"web",
"10.210.0.3",
`# New config
new.example.com {
respond "New"
}`,
"test-machine-id",
time.Now(),
),
},
want: testCaddyfileHeader + `
# User-defined config for service 'web'.
# New config
new.example.com {
respond "New"
}
`,
},
{
name: "compound test: upstreams variants, global caddy, and multi-machine services",
containers: []store.ContainerRecord{
// Global Caddy service on test-machine-id
newContainerRecordWithCaddyConfig(
"caddy",
"10.210.1.1",
`# Global config from test machine
{
admin off
}
localhost:8080 {
respond "Admin panel"
}`,
"test-machine-id",
time.Now(),
),
// Another caddy on different machine (should be ignored)
newContainerRecordWithCaddyConfig(
"caddy",
"10.210.2.1",
`# Should be ignored
{
debug
}`,
"machine-2",
time.Now(),
),
// API service containers across different machines
newContainerRecordWithPorts("api", "10.210.1.2", []string{"api.example.com:8080/http"},
"test-machine-id"),
newContainerRecordWithPorts("api", "10.210.2.2", []string{"api.example.com:8080/http"}, "machine-2"),
newContainerRecordWithPorts("api", "10.210.3.2", []string{"api.example.com:8080/http"}, "machine-3"),
// Web service with different versions on different machines
newContainerRecordWithCaddyConfig(
"web",
"10.210.1.3",
`# Web service config v1 (older)
web-v1.example.com {
reverse_proxy web:3000
}`,
"test-machine-id",
time.Now().Add(-2*time.Hour),
),
newContainerRecordWithPorts("web", "10.210.3.3", []string{"web.example.com:3000/http"}, "machine-3"),
newContainerRecordWithCaddyConfig(
"web",
"10.210.2.3",
`# Web service config v2 (most recent)
web-v2.example.com {
reverse_proxy {{upstreams 8080}}
}`,
"machine-2",
time.Now().Add(1*time.Second),
),
// DB service with custom config
newContainerRecordWithCaddyConfig(
"db",
"10.210.1.4",
`# DB admin panel
dbadmin.example.com {
basicauth {
admin $2a$14$Zkx19XLiW6VYouLHR5NmfOFU0z2GTNmpkT/5qqR7hx4IjWJPDhjvG
}
reverse_proxy {{upstreams 5432}}
}`,
"test-machine-id",
time.Now(),
),
// Gateway service with various upstream template usages
newContainerRecordWithCaddyConfig(
"gateway",
"10.210.1.5",
`# Testing different upstream template functions
gateway.example.com {
# Current service upstreams (gateway)
handle /self {
reverse_proxy {{upstreams}}
}
# Named service upstreams without port
handle /api {
reverse_proxy {{upstreams "api"}}
}
# Named service upstreams with port
handle /api-custom {
reverse_proxy {{upstreams "api" 9000}}
}
# Current service with name and port
handle /self-port {
reverse_proxy {{upstreams .Name 8888}}
}
# Service with mixed containers (web) and advanced template
handle /web {
reverse_proxy {{- range $ip := index .Upstreams "web"}} https://{{$ip}}{{end}}
}
# Non-existent service
handle /missing {
reverse_proxy {{upstreams "nonexistent"}}
}
}`,
"test-machine-id",
time.Now(),
),
// App service with just ports (no custom config)
newContainerRecordWithPorts("app", "10.210.1.6", []string{"app.example.com:3000/http"},
"test-machine-id"),
newContainerRecordWithPorts("app", "10.210.2.6", []string{"app.example.com:3000/http"}, "machine-2"),
// Service with invalid config (should be ignored)
newContainerRecordWithCaddyConfig(
"invalid",
"10.210.1.7",
`# test:invalid
badconfig.com {
respond "This config is invalid"
}`,
"test-machine-id",
time.Now(),
),
},
want: `# Caddyfile autogenerated by Uncloud (DO NOT EDIT): TIMESTAMP_PLACEHOLDER
# Automatically updated on service or health status changes.
# Docs: https://uncloud.run/docs/concepts/ingress/overview
# User-defined global config from service 'caddy'.
# Global config from test machine
{
admin off
}
localhost:8080 {
respond "Admin panel"
}
# Health check endpoint to verify Caddy reachability on this machine.
http:// {
handle /.uncloud-verify {
respond "test-machine-id" 200
}
log
}
(common_proxy) {
# Retry failed requests up to lb_retries times against other available upstreams.
lb_retries 3
# Upstreams are marked unhealthy for fail_duration after a failed request (passive health checking).
fail_duration 30s
}
# Sites generated from service ports.
http://api.example.com {
reverse_proxy 10.210.1.2:8080 10.210.2.2:8080 10.210.3.2:8080 {
import common_proxy
}
log
}
http://app.example.com {
reverse_proxy 10.210.1.6:3000 10.210.2.6:3000 {
import common_proxy
}
log
}
http://web.example.com {
reverse_proxy 10.210.3.3:3000 {
import common_proxy
}
log
}
# User-defined config for service 'db'.
# DB admin panel
dbadmin.example.com {
basicauth {
admin $2a$14$Zkx19XLiW6VYouLHR5NmfOFU0z2GTNmpkT/5qqR7hx4IjWJPDhjvG
}
reverse_proxy 10.210.1.4:5432
}
# User-defined config for service 'gateway'.
# Testing different upstream template functions
gateway.example.com {
# Current service upstreams (gateway)
handle /self {
reverse_proxy 10.210.1.5
}
# Named service upstreams without port
handle /api {
reverse_proxy 10.210.1.2 10.210.2.2 10.210.3.2
}
# Named service upstreams with port
handle /api-custom {
reverse_proxy 10.210.1.2:9000 10.210.2.2:9000 10.210.3.2:9000
}
# Current service with name and port
handle /self-port {
reverse_proxy 10.210.1.5:8888
}
# Service with mixed containers (web) and advanced template
handle /web {
reverse_proxy https://10.210.1.3 https://10.210.3.3 https://10.210.2.3
}
# Non-existent service
handle /missing {
` + "\t\treverse_proxy " + `
}
}
# User-defined config for service 'web'.
# Web service config v2 (most recent)
web-v2.example.com {
reverse_proxy 10.210.1.3:8080 10.210.3.3:8080 10.210.2.3:8080
}
# Skipped invalid user-defined configs:
# - service 'invalid': validation failed: invalid config detected
`,
},
{
name: "multiple errors: invalid global, template error, and validation error",
containers: []store.ContainerRecord{
newContainerRecordWithCaddyConfig(
"caddy",
"10.210.0.1",
`# test:invalid
{
invalid global
}`,
"test-machine-id",
time.Now(),
),
newContainerRecordWithCaddyConfig(
"broken-template",
"10.210.0.2",
`broken.example.com {
reverse_proxy {{upstreams "missing
}`,
"test-machine-id",
time.Now(),
),
newContainerRecordWithCaddyConfig(
"invalid",
"10.210.0.3",
`# test:invalid
invalid.example.com {
respond "Invalid config"
}`,
"test-machine-id",
time.Now(),
),
newContainerRecordWithCaddyConfig(
"valid",
"10.210.0.4",
`valid.example.com {
respond "Valid config"
}`,
"test-machine-id",
time.Now(),
),
},
want: testCaddyfileHeader + `
# User-defined config for service 'valid'.
valid.example.com {
respond "Valid config"
}
# Skipped invalid user-defined configs:
# - service 'caddy': validation failed: invalid config detected
# - service 'broken-template': failed to render template: parse config as Go template: template: Caddyfile:2: unterminated quoted string
# - service 'invalid': validation failed: invalid config detected
`,
},
}
ctx := context.Background()
validator := NewMockCaddyfileValidator(t)
validator.EXPECT().Validate(mock.Anything, mock.Anything).RunAndReturn(
func(ctx context.Context, caddyfile string) error {
if strings.Contains(caddyfile, "# test:invalid") {
return errors.New("invalid config detected")
}
return nil
})
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
generator := NewCaddyfileGenerator("test-machine-id", validator, nil)
config, err := generator.Generate(ctx, tt.containers, true)
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tt.want, normaliseGeneratedTimestamp(config), "Generated Caddyfile doesn't match")
})
}
}
func newContainerRecord(ctr api.ServiceContainer, machineID string) store.ContainerRecord {
return store.ContainerRecord{
Container: ctr,
MachineID: machineID,
}
}
func newContainerRecordWithCaddyConfig(serviceName, ip, caddyConfig, machineID string, created time.Time) store.ContainerRecord {
return store.ContainerRecord{
Container: api.ServiceContainer{
Container: api.Container{
InspectResponse: container.InspectResponse{
ContainerJSONBase: &container.ContainerJSONBase{
ID: serviceName + "-" + ip, // Add ID for stable sorting
State: &container.State{
Running: true,
},
Created: created.UTC().Format(time.RFC3339Nano),
},
NetworkSettings: &container.NetworkSettings{
Networks: map[string]*network.EndpointSettings{
docker.NetworkName: {
IPAddress: ip,
},
},
},
Config: &container.Config{
Labels: map[string]string{
api.LabelServiceName: serviceName,
},
},
},
},
ServiceSpec: api.ServiceSpec{
Caddy: &api.CaddySpec{
Config: caddyConfig,
},
},
},
MachineID: machineID,
}
}
func TestCaddyfileGeneratorWithoutCustomConfigs(t *testing.T) {
// Test that when includeCustom is false (Caddy not available), x-caddy configs are skipped.
tests := []struct {
name string
containers []store.ContainerRecord
want string
}{
{
name: "x-caddy configs are skipped",
containers: []store.ContainerRecord{
newContainerRecordWithCaddyConfig(
"caddy",
"10.210.0.1",
`# Global config
{
global directive
}`,
"test-machine-id",
time.Now(),
),
newContainerRecordWithCaddyConfig(
"web",
"10.210.0.2",
`web.example.com {
reverse_proxy web:3000
}`,
"test-machine-id",
time.Now(),
),
newContainerRecordWithPorts(
"api",
"10.210.0.3",
[]string{"api.example.com:8080/http"},
"test-machine-id",
),
},
want: testCaddyfileHeader + `
# Sites generated from service ports.
http://api.example.com {
reverse_proxy 10.210.0.3:8080 {
import common_proxy
}
log
}
# NOTE: User-defined configs for services were skipped because Caddy is not running on this machine
# (not accessible via the shared admin socket /run/uncloud/caddy/admin.sock) or the latest
# generated config is invalid. Please check the service 'caddy' is running (uc inspect caddy)
# and its logs for more details (uc logs caddy).
`,
},
{
name: "no containers with x-caddy configs",
containers: []store.ContainerRecord{
newContainerRecordWithPorts(
"api",
"10.210.0.3",
[]string{"api.example.com:8080/http"},
"test-machine-id",
),
},
want: testCaddyfileHeader + `
# Sites generated from service ports.
http://api.example.com {
reverse_proxy 10.210.0.3:8080 {
import common_proxy
}
log
}
# NOTE: User-defined configs for services were skipped because Caddy is not running on this machine
# (not accessible via the shared admin socket /run/uncloud/caddy/admin.sock) or the latest
# generated config is invalid. Please check the service 'caddy' is running (uc inspect caddy)
# and its logs for more details (uc logs caddy).
`,
},
}
ctx := context.Background()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Validator is not expected to be called in these tests.
generator := NewCaddyfileGenerator("test-machine-id", nil, nil)
config, err := generator.Generate(ctx, tt.containers, false)
require.NoError(t, err)
assert.Equal(t, tt.want, normaliseGeneratedTimestamp(config), "Generated Caddyfile doesn't match")
})
}
}
func newContainerRecordWithPorts(serviceName, ip string, ports []string, machineID string) store.ContainerRecord {
portsLabel := strings.Join(ports, ",")
return store.ContainerRecord{
Container: api.ServiceContainer{
Container: api.Container{
InspectResponse: container.InspectResponse{
ContainerJSONBase: &container.ContainerJSONBase{
ID: serviceName + "-" + ip, // Add ID for stable sorting
State: &container.State{
Running: true,
},
Created: time.Now().UTC().Format(time.RFC3339Nano),
},
NetworkSettings: &container.NetworkSettings{
Networks: map[string]*network.EndpointSettings{
docker.NetworkName: {
IPAddress: ip,
},
},
},
Config: &container.Config{
Labels: map[string]string{
api.LabelServiceName: serviceName,
api.LabelServicePorts: portsLabel,
},
},
},
},
},
MachineID: machineID,
}
}