mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 19:13:34 +00:00
feat: validate and append custom per-service Caddy configs to generated Caddyfile
This commit is contained in:
@@ -17,6 +17,7 @@ import (
|
|||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO: change upstreams from 'to' to the directive arguments.
|
||||||
const caddyfileTemplate = `http:// {
|
const caddyfileTemplate = `http:// {
|
||||||
handle {{.VerifyPath}} {
|
handle {{.VerifyPath}} {
|
||||||
respond "{{.VerifyResponse}}" 200
|
respond "{{.VerifyResponse}}" 200
|
||||||
@@ -75,6 +76,7 @@ func NewCaddyfileGenerator(machineID string, validator CaddyfileValidator, log *
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate creates a Caddyfile configuration based on the provided service containers.
|
// Generate creates a Caddyfile configuration based on the provided service containers.
|
||||||
|
// The Caddyfile is generated from the service ports of the healthy containers.
|
||||||
// If a 'caddy' service container is running on this machine and defines a custom Caddy config (x-caddy) in its service
|
// If a 'caddy' service container is running on this machine and defines a custom Caddy config (x-caddy) in its service
|
||||||
// spec, it will be validated and prepended to the generated Caddyfile. Custom Caddy configs (x-caddy) defined in other
|
// spec, it will be validated and prepended to the generated Caddyfile. Custom Caddy configs (x-caddy) defined in other
|
||||||
// service specs are validated and appended to the generated Caddyfile. Invalid configs are logged and skipped to ensure
|
// service specs are validated and appended to the generated Caddyfile. Invalid configs are logged and skipped to ensure
|
||||||
@@ -92,11 +94,11 @@ func (g *CaddyfileGenerator) Generate(ctx context.Context, records []store.Conta
|
|||||||
for i, cr := range records {
|
for i, cr := range records {
|
||||||
containers[i] = cr.Container
|
containers[i] = cr.Container
|
||||||
}
|
}
|
||||||
// Sort containers by service name and container ID to generate a stable Caddyfile.
|
// Sort containers by service name and creation time to generate a stable Caddyfile.
|
||||||
slices.SortFunc(containers, func(a, b api.ServiceContainer) int {
|
slices.SortStableFunc(containers, func(a, b api.ServiceContainer) int {
|
||||||
return cmp.Or(
|
return cmp.Or(
|
||||||
strings.Compare(a.ServiceName(), b.ServiceName()),
|
strings.Compare(a.ServiceName(), b.ServiceName()),
|
||||||
strings.Compare(a.ID, b.ID),
|
a.CreatedTime().Compare(b.CreatedTime()),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -105,26 +107,38 @@ func (g *CaddyfileGenerator) Generate(ctx context.Context, records []store.Conta
|
|||||||
return "", fmt.Errorf("generate base Caddyfile from service ports: %w", err)
|
return "", fmt.Errorf("generate base Caddyfile from service ports: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
upstreams := serviceUpstreams(containers)
|
||||||
|
|
||||||
// Find the 'caddy' service container on this machine. Use the most recent one if multiple exist.
|
// Find the 'caddy' service container on this machine. Use the most recent one if multiple exist.
|
||||||
var caddyCtr *api.ServiceContainer
|
var caddyCtr *api.ServiceContainer
|
||||||
for _, cr := range records {
|
for _, cr := range records {
|
||||||
if cr.MachineID == g.machineID && cr.Container.ServiceName() == CaddyServiceName &&
|
if cr.MachineID == g.machineID && cr.Container.ServiceName() == CaddyServiceName &&
|
||||||
(caddyCtr == nil || cr.Container.Created > caddyCtr.Created) {
|
(caddyCtr == nil || cr.Container.CreatedTime().Compare(caddyCtr.CreatedTime()) > 0) {
|
||||||
caddyCtr = &cr.Container
|
caddyCtr = &cr.Container
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the caddy container is running on this machine and has a custom Caddy config, prepend it to the generated
|
// If the caddy container is running on this machine and has a custom Caddy config (global),
|
||||||
// Caddyfile and validate it.
|
// prepend it to the generated Caddyfile and validate it.
|
||||||
if caddyCtr != nil && caddyCtr.ServiceSpec.CaddyConfig() != "" {
|
if caddyCtr != nil && caddyCtr.ServiceSpec.CaddyConfig() != "" {
|
||||||
// TODO: render the template actions in the Caddy config.
|
// Render the custom global Caddy config as a Go template with the upstreams.
|
||||||
caddyfileCandidate := caddyCtr.ServiceSpec.CaddyConfig() + "\n\n" + caddyfile
|
tmplCtx := templateContext{
|
||||||
|
Name: caddyCtr.ServiceName(),
|
||||||
if err = g.validator.Validate(ctx, caddyfileCandidate); err != nil {
|
Upstreams: upstreams,
|
||||||
g.log.Error("Custom Caddy config on the caddy container on this machine is invalid, skipping it.",
|
}
|
||||||
|
renderedConfig, err := renderCaddyfile(tmplCtx, caddyCtr.ServiceSpec.CaddyConfig())
|
||||||
|
if err != nil {
|
||||||
|
g.log.Error("Failed to render template directives in custom global Caddy config, skipping it.",
|
||||||
"service", caddyCtr.ServiceName(), "container", caddyCtr.ID, "err", err)
|
"service", caddyCtr.ServiceName(), "container", caddyCtr.ID, "err", err)
|
||||||
} else {
|
} else {
|
||||||
caddyfile = caddyfileCandidate
|
caddyfileCandidate := renderedConfig + "\n\n" + caddyfile
|
||||||
|
|
||||||
|
if err = g.validator.Validate(ctx, caddyfileCandidate); err != nil {
|
||||||
|
g.log.Error("Custom global Caddy config is invalid, skipping it.",
|
||||||
|
"service", caddyCtr.ServiceName(), "container", caddyCtr.ID, "err", err)
|
||||||
|
} else {
|
||||||
|
caddyfile = caddyfileCandidate
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +148,7 @@ func (g *CaddyfileGenerator) Generate(ctx context.Context, records []store.Conta
|
|||||||
latestServiceContainers := make(map[string]api.ServiceContainer, len(containers))
|
latestServiceContainers := make(map[string]api.ServiceContainer, len(containers))
|
||||||
for _, ctr := range containers {
|
for _, ctr := range containers {
|
||||||
if latest, ok := latestServiceContainers[ctr.ServiceName()]; ok {
|
if latest, ok := latestServiceContainers[ctr.ServiceName()]; ok {
|
||||||
if ctr.Created > latest.Created {
|
if ctr.CreatedTime().Compare(latest.CreatedTime()) > 0 {
|
||||||
latestServiceContainers[ctr.ServiceName()] = ctr
|
latestServiceContainers[ctr.ServiceName()] = ctr
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -143,6 +157,8 @@ func (g *CaddyfileGenerator) Generate(ctx context.Context, records []store.Conta
|
|||||||
}
|
}
|
||||||
sortedServiceNames := slices.Sorted(maps.Keys(latestServiceContainers))
|
sortedServiceNames := slices.Sorted(maps.Keys(latestServiceContainers))
|
||||||
|
|
||||||
|
// Append a custom Caddy config for each service to the Caddyfile and validate it. If the config for a service
|
||||||
|
// is invalid, skip it but continue processing other services to ensure the Caddyfile remains valid.
|
||||||
for _, serviceName := range sortedServiceNames {
|
for _, serviceName := range sortedServiceNames {
|
||||||
// Skip the caddy container as we already processed it.
|
// Skip the caddy container as we already processed it.
|
||||||
if serviceName == CaddyServiceName {
|
if serviceName == CaddyServiceName {
|
||||||
@@ -154,29 +170,37 @@ func (g *CaddyfileGenerator) Generate(ctx context.Context, records []store.Conta
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: render the template actions in the Caddy config.
|
// Render the template actions in the service's Caddy config.
|
||||||
caddyfileCandidate := fmt.Sprintf("%s\n# Service: %s\n%s\n",
|
tmplCtx := templateContext{
|
||||||
caddyfile, ctr.ServiceName(), ctr.ServiceSpec.CaddyConfig())
|
Name: serviceName,
|
||||||
|
Upstreams: upstreams,
|
||||||
if err = g.validator.Validate(ctx, caddyfileCandidate); err != nil {
|
}
|
||||||
g.log.Error("Custom Caddy config for service is invalid, skipping it.",
|
renderedConfig, err := renderCaddyfile(tmplCtx, ctr.ServiceSpec.CaddyConfig())
|
||||||
"service", ctr.ServiceName(), "err", err)
|
if err != nil {
|
||||||
|
g.log.Error("Failed to render template directives in custom Caddy config for service, skipping it.",
|
||||||
|
"service", serviceName, "err", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
caddyfile = caddyfileCandidate
|
caddyfileCandidate := fmt.Sprintf("%s\n# Service: %s\n%s\n", caddyfile, serviceName, renderedConfig)
|
||||||
|
if err = g.validator.Validate(ctx, caddyfileCandidate); err != nil {
|
||||||
|
g.log.Error("Custom Caddy config for service is invalid, skipping it.",
|
||||||
|
"service", serviceName, "err", err)
|
||||||
|
} else {
|
||||||
|
caddyfile = caddyfileCandidate
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return caddyfile, nil
|
return caddyfile, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *CaddyfileGenerator) generateBaseFromPorts(containers []api.ServiceContainer) (string, error) {
|
func (g *CaddyfileGenerator) generateBaseFromPorts(containers []api.ServiceContainer) (string, error) {
|
||||||
httpHostUpstreams, httpsHostUpstreams := httpUpstreamsFromContainers(containers)
|
httpHostUpstreams, httpsHostUpstreams := httpUpstreamsFromPorts(containers)
|
||||||
|
|
||||||
funcs := template.FuncMap{"join": strings.Join}
|
funcs := template.FuncMap{"join": strings.Join}
|
||||||
tmpl, err := template.New("Caddyfile").Funcs(funcs).Parse(caddyfileTemplate)
|
tmpl, err := template.New("Caddyfile").Funcs(funcs).Parse(caddyfileTemplate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to parse Caddyfile template: %w", err)
|
return "", fmt.Errorf("parse Caddyfile template: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
data := struct {
|
data := struct {
|
||||||
@@ -193,15 +217,15 @@ func (g *CaddyfileGenerator) generateBaseFromPorts(containers []api.ServiceConta
|
|||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
if err = tmpl.Execute(&buf, data); err != nil {
|
if err = tmpl.Execute(&buf, data); err != nil {
|
||||||
return "", fmt.Errorf("failed to execute Caddyfile template: %w", err)
|
return "", fmt.Errorf("execute Caddyfile template: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return buf.String(), nil
|
return buf.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// httpUpstreamsFromContainers extracts upstreams for HTTP and HTTPS protocols from the published ports of the provided
|
// httpUpstreamsFromPorts extracts upstreams for HTTP and HTTPS protocols from the published ports of the provided
|
||||||
// service containers. It's expected that all containers are healthy.
|
// service containers. It's expected that all containers are healthy.
|
||||||
func httpUpstreamsFromContainers(containers []api.ServiceContainer) (map[string][]string, map[string][]string) {
|
func httpUpstreamsFromPorts(containers []api.ServiceContainer) (map[string][]string, map[string][]string) {
|
||||||
// Maps hostnames to lists of upstreams (container IP:port pairs).
|
// Maps hostnames to lists of upstreams (container IP:port pairs).
|
||||||
httpHostUpstreams := make(map[string][]string)
|
httpHostUpstreams := make(map[string][]string)
|
||||||
httpsHostUpstreams := make(map[string][]string)
|
httpsHostUpstreams := make(map[string][]string)
|
||||||
@@ -241,3 +265,40 @@ func httpUpstreamsFromContainers(containers []api.ServiceContainer) (map[string]
|
|||||||
|
|
||||||
return httpHostUpstreams, httpsHostUpstreams
|
return httpHostUpstreams, httpsHostUpstreams
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// serviceUpstreams creates a map of service names to their container IPs.
|
||||||
|
// Only includes containers connected to the uncloud Docker network.
|
||||||
|
func serviceUpstreams(containers []api.ServiceContainer) map[string][]string {
|
||||||
|
upstreams := make(map[string][]string)
|
||||||
|
for _, ctr := range containers {
|
||||||
|
ip := ctr.UncloudNetworkIP()
|
||||||
|
if !ip.IsValid() {
|
||||||
|
// Container is not connected to the uncloud Docker network (could be host network).
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceName := ctr.ServiceName()
|
||||||
|
upstreams[serviceName] = append(upstreams[serviceName], ip.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return upstreams
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderCaddyfile renders a Caddyfile template with the upstreams function and data.
|
||||||
|
func renderCaddyfile(tmplCtx templateContext, caddyfile string) (string, error) {
|
||||||
|
funcs := template.FuncMap{
|
||||||
|
"upstreams": upstreamsTemplateFn(tmplCtx),
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl, err := template.New("Caddyfile").Funcs(funcs).Parse(caddyfile)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("parse config as Go template: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err = tmpl.Execute(&buf, tmplCtx); err != nil {
|
||||||
|
return "", fmt.Errorf("execute template: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.String(), nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,11 +2,19 @@ package caddyconfig
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"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/internal/machine/store"
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -26,9 +34,6 @@ func TestCaddyfileGenerator(t *testing.T) {
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
// TODO: mock validator
|
|
||||||
generator := NewCaddyfileGenerator("test-machine-id", nil, nil)
|
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
containers []store.ContainerRecord
|
containers []store.ContainerRecord
|
||||||
@@ -169,6 +174,541 @@ https://secure.example.com {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
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)
|
||||||
|
|
||||||
|
if tt.wantErr {
|
||||||
|
assert.Error(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, tt.want, config, "Generated Caddyfile doesn't match")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCaddyfileGeneratorWithCustomConfigs(t *testing.T) {
|
||||||
|
caddyfileBase := `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
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
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: `# Global Caddy configuration
|
||||||
|
{
|
||||||
|
global directive
|
||||||
|
}
|
||||||
|
|
||||||
|
` + caddyfileBase,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: caddyfileBase + `
|
||||||
|
# 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: caddyfileBase,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: caddyfileBase,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: caddyfileBase,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: caddyfileBase,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: caddyfileBase + `
|
||||||
|
# Service: api
|
||||||
|
api.example.com {
|
||||||
|
reverse_proxy api:8080
|
||||||
|
}
|
||||||
|
|
||||||
|
# Service: web
|
||||||
|
web.example.com {
|
||||||
|
reverse_proxy web:3000
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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: `# Global config
|
||||||
|
{
|
||||||
|
global directive
|
||||||
|
}
|
||||||
|
|
||||||
|
` + caddyfileBase + `
|
||||||
|
http://app.example.com {
|
||||||
|
reverse_proxy {
|
||||||
|
to 10.210.0.2:8080
|
||||||
|
import common_proxy
|
||||||
|
}
|
||||||
|
log
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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: caddyfileBase + `
|
||||||
|
http://api.example.com {
|
||||||
|
reverse_proxy {
|
||||||
|
to 10.210.0.3:8080
|
||||||
|
import common_proxy
|
||||||
|
}
|
||||||
|
log
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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: caddyfileBase + `
|
||||||
|
# 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 $up := index .Upstreams "web"}} {{$up}}{{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: `# Global config from test machine
|
||||||
|
{
|
||||||
|
admin off
|
||||||
|
}
|
||||||
|
|
||||||
|
localhost:8080 {
|
||||||
|
respond "Admin panel"
|
||||||
|
}
|
||||||
|
|
||||||
|
` + caddyfileBase + `
|
||||||
|
http://api.example.com {
|
||||||
|
reverse_proxy {
|
||||||
|
to 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 {
|
||||||
|
to 10.210.1.6:3000 10.210.2.6:3000
|
||||||
|
import common_proxy
|
||||||
|
}
|
||||||
|
log
|
||||||
|
}
|
||||||
|
|
||||||
|
http://web.example.com {
|
||||||
|
reverse_proxy {
|
||||||
|
to 10.210.3.3:3000
|
||||||
|
import common_proxy
|
||||||
|
}
|
||||||
|
log
|
||||||
|
}
|
||||||
|
|
||||||
|
# Service: db
|
||||||
|
# DB admin panel
|
||||||
|
dbadmin.example.com {
|
||||||
|
basicauth {
|
||||||
|
admin $2a$14$Zkx19XLiW6VYouLHR5NmfOFU0z2GTNmpkT/5qqR7hx4IjWJPDhjvG
|
||||||
|
}
|
||||||
|
reverse_proxy 10.210.1.4:5432
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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 10.210.1.3 10.210.3.3 10.210.2.3
|
||||||
|
}
|
||||||
|
|
||||||
|
# Non-existent service
|
||||||
|
handle /missing {
|
||||||
|
` + "\t\treverse_proxy " + `
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
config, err := generator.Generate(ctx, tt.containers)
|
||||||
|
|
||||||
if tt.wantErr {
|
if tt.wantErr {
|
||||||
@@ -188,3 +728,72 @@ func newContainerRecord(ctr api.ServiceContainer, machineID string) store.Contai
|
|||||||
MachineID: machineID,
|
MachineID: machineID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newContainerRecordWithCaddyConfig(serviceName, ip, caddyConfig, machineID string, created time.Time) store.ContainerRecord {
|
||||||
|
return store.ContainerRecord{
|
||||||
|
Container: api.ServiceContainer{
|
||||||
|
Container: api.Container{
|
||||||
|
ContainerJSON: types.ContainerJSON{
|
||||||
|
ContainerJSONBase: &types.ContainerJSONBase{
|
||||||
|
ID: serviceName + "-" + ip, // Add ID for stable sorting
|
||||||
|
State: &types.ContainerState{
|
||||||
|
Running: true,
|
||||||
|
},
|
||||||
|
Created: created.UTC().Format(time.RFC3339Nano),
|
||||||
|
},
|
||||||
|
NetworkSettings: &types.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 newContainerRecordWithPorts(serviceName, ip string, ports []string, machineID string) store.ContainerRecord {
|
||||||
|
portsLabel := strings.Join(ports, ",")
|
||||||
|
return store.ContainerRecord{
|
||||||
|
Container: api.ServiceContainer{
|
||||||
|
Container: api.Container{
|
||||||
|
ContainerJSON: types.ContainerJSON{
|
||||||
|
ContainerJSONBase: &types.ContainerJSONBase{
|
||||||
|
ID: serviceName + "-" + ip, // Add ID for stable sorting
|
||||||
|
State: &types.ContainerState{
|
||||||
|
Running: true,
|
||||||
|
},
|
||||||
|
Created: time.Now().UTC().Format(time.RFC3339Nano),
|
||||||
|
},
|
||||||
|
NetworkSettings: &types.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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func GenerateJSONConfig(containers []api.ServiceContainer, verifyResponse string) (*caddy.Config, error) {
|
func GenerateJSONConfig(containers []api.ServiceContainer, verifyResponse string) (*caddy.Config, error) {
|
||||||
httpHostUpstreams, httpsHostUpstreams := httpUpstreamsFromContainers(containers)
|
httpHostUpstreams, httpsHostUpstreams := httpUpstreamsFromPorts(containers)
|
||||||
|
|
||||||
var warnings []caddyconfig.Warning
|
var warnings []caddyconfig.Warning
|
||||||
servers := make(map[string]*caddyhttp.Server)
|
servers := make(map[string]*caddyhttp.Server)
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
package caddyconfig
|
package caddyconfig
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
mock "github.com/stretchr/testify/mock"
|
mock "github.com/stretchr/testify/mock"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -36,16 +38,16 @@ func (_m *MockCaddyfileValidator) EXPECT() *MockCaddyfileValidator_Expecter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate provides a mock function for the type MockCaddyfileValidator
|
// Validate provides a mock function for the type MockCaddyfileValidator
|
||||||
func (_mock *MockCaddyfileValidator) Validate(caddyfile string) error {
|
func (_mock *MockCaddyfileValidator) Validate(ctx context.Context, caddyfile string) error {
|
||||||
ret := _mock.Called(caddyfile)
|
ret := _mock.Called(ctx, caddyfile)
|
||||||
|
|
||||||
if len(ret) == 0 {
|
if len(ret) == 0 {
|
||||||
panic("no return value specified for Validate")
|
panic("no return value specified for Validate")
|
||||||
}
|
}
|
||||||
|
|
||||||
var r0 error
|
var r0 error
|
||||||
if returnFunc, ok := ret.Get(0).(func(string) error); ok {
|
if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok {
|
||||||
r0 = returnFunc(caddyfile)
|
r0 = returnFunc(ctx, caddyfile)
|
||||||
} else {
|
} else {
|
||||||
r0 = ret.Error(0)
|
r0 = ret.Error(0)
|
||||||
}
|
}
|
||||||
@@ -58,19 +60,25 @@ type MockCaddyfileValidator_Validate_Call struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate is a helper method to define mock.On call
|
// Validate is a helper method to define mock.On call
|
||||||
|
// - ctx context.Context
|
||||||
// - caddyfile string
|
// - caddyfile string
|
||||||
func (_e *MockCaddyfileValidator_Expecter) Validate(caddyfile interface{}) *MockCaddyfileValidator_Validate_Call {
|
func (_e *MockCaddyfileValidator_Expecter) Validate(ctx interface{}, caddyfile interface{}) *MockCaddyfileValidator_Validate_Call {
|
||||||
return &MockCaddyfileValidator_Validate_Call{Call: _e.mock.On("Validate", caddyfile)}
|
return &MockCaddyfileValidator_Validate_Call{Call: _e.mock.On("Validate", ctx, caddyfile)}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (_c *MockCaddyfileValidator_Validate_Call) Run(run func(caddyfile string)) *MockCaddyfileValidator_Validate_Call {
|
func (_c *MockCaddyfileValidator_Validate_Call) Run(run func(ctx context.Context, caddyfile string)) *MockCaddyfileValidator_Validate_Call {
|
||||||
_c.Call.Run(func(args mock.Arguments) {
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
var arg0 string
|
var arg0 context.Context
|
||||||
if args[0] != nil {
|
if args[0] != nil {
|
||||||
arg0 = args[0].(string)
|
arg0 = args[0].(context.Context)
|
||||||
|
}
|
||||||
|
var arg1 string
|
||||||
|
if args[1] != nil {
|
||||||
|
arg1 = args[1].(string)
|
||||||
}
|
}
|
||||||
run(
|
run(
|
||||||
arg0,
|
arg0,
|
||||||
|
arg1,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
return _c
|
return _c
|
||||||
@@ -81,7 +89,7 @@ func (_c *MockCaddyfileValidator_Validate_Call) Return(err error) *MockCaddyfile
|
|||||||
return _c
|
return _c
|
||||||
}
|
}
|
||||||
|
|
||||||
func (_c *MockCaddyfileValidator_Validate_Call) RunAndReturn(run func(caddyfile string) error) *MockCaddyfileValidator_Validate_Call {
|
func (_c *MockCaddyfileValidator_Validate_Call) RunAndReturn(run func(ctx context.Context, caddyfile string) error) *MockCaddyfileValidator_Validate_Call {
|
||||||
_c.Call.Return(run)
|
_c.Call.Return(run)
|
||||||
return _c
|
return _c
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package caddyconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// templateContext holds the data available to Caddyfile templates.
|
||||||
|
type templateContext struct {
|
||||||
|
// Name is the current service name.
|
||||||
|
Name string
|
||||||
|
// Upstreams maps service names to their container IPs.
|
||||||
|
Upstreams map[string][]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// upstreamsTemplateFn returns a template function that generates a space separated string of upstreams for the service.
|
||||||
|
// It optionally accepts a service name and a port number: {{upstreams [service-name] [port]}}.
|
||||||
|
func upstreamsTemplateFn(tmplCtx templateContext) func(args ...any) (string, error) {
|
||||||
|
return func(args ...any) (string, error) {
|
||||||
|
var serviceName string
|
||||||
|
var port int
|
||||||
|
|
||||||
|
// Parse arguments.
|
||||||
|
switch len(args) {
|
||||||
|
case 0:
|
||||||
|
// Current service, default port.
|
||||||
|
serviceName = tmplCtx.Name
|
||||||
|
case 1:
|
||||||
|
// Either port (int) for current service or service name (string).
|
||||||
|
switch arg := args[0].(type) {
|
||||||
|
case int:
|
||||||
|
serviceName = tmplCtx.Name
|
||||||
|
port = arg
|
||||||
|
case string:
|
||||||
|
serviceName = arg
|
||||||
|
port = 0
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("upstreams function: invalid argument type: %T", arg)
|
||||||
|
}
|
||||||
|
case 2:
|
||||||
|
// Service name and port.
|
||||||
|
name, ok := args[0].(string)
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("upstreams function: first argument must be service name (string)")
|
||||||
|
}
|
||||||
|
serviceName = name
|
||||||
|
|
||||||
|
p, ok := args[1].(int)
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("upstreams function: second argument must be port (int)")
|
||||||
|
}
|
||||||
|
port = p
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("upstreams function: too many arguments; expected 0-2, got %d", len(args))
|
||||||
|
}
|
||||||
|
|
||||||
|
ips, ok := tmplCtx.Upstreams[serviceName]
|
||||||
|
if !ok || len(ips) == 0 {
|
||||||
|
// No upstreams available.
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the space separated upstreams string.
|
||||||
|
var upstreams []string
|
||||||
|
for _, ip := range ips {
|
||||||
|
if port > 0 {
|
||||||
|
upstreams = append(upstreams, net.JoinHostPort(ip, strconv.Itoa(port)))
|
||||||
|
} else {
|
||||||
|
upstreams = append(upstreams, ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(upstreams, " "), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user