This commit is contained in:
kyounghoonJang 2026-01-30 09:34:44 +01:00 committed by GitHub
commit c077464d3c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 306 additions and 29 deletions

View file

@ -411,6 +411,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
| <a id="opt-providers-nomad-allowemptyservices" href="#opt-providers-nomad-allowemptyservices" title="#opt-providers-nomad-allowemptyservices">providers.nomad.allowemptyservices</a> | Allow the creation of services without endpoints. | false |
| <a id="opt-providers-nomad-constraints" href="#opt-providers-nomad-constraints" title="#opt-providers-nomad-constraints">providers.nomad.constraints</a> | Constraints is an expression that Traefik matches against the Nomad service's tags to determine whether to create route(s) for that service. | |
| <a id="opt-providers-nomad-defaultrule" href="#opt-providers-nomad-defaultrule" title="#opt-providers-nomad-defaultrule">providers.nomad.defaultrule</a> | Default rule. | Host(`{{ normalize .Name }}`) |
| <a id="opt-providers-nomad-disablehealthcheck" href="#opt-providers-nomad-disablehealthcheck" title="#opt-providers-nomad-disablehealthcheck">providers.nomad.disablehealthcheck</a> | Disable health check filtering. Enable this for Nomad < 1.4 or for better performance. | false |
| <a id="opt-providers-nomad-endpoint-address" href="#opt-providers-nomad-endpoint-address" title="#opt-providers-nomad-endpoint-address">providers.nomad.endpoint.address</a> | The address of the Nomad server, including scheme and port. | http://127.0.0.1:4646 |
| <a id="opt-providers-nomad-endpoint-endpointwaittime" href="#opt-providers-nomad-endpoint-endpointwaittime" title="#opt-providers-nomad-endpoint-endpointwaittime">providers.nomad.endpoint.endpointwaittime</a> | WaitTime limits how long a Watch will block. If not provided, the agent default values will be used | 0 |
| <a id="opt-providers-nomad-endpoint-region" href="#opt-providers-nomad-endpoint-region" title="#opt-providers-nomad-endpoint-region">providers.nomad.endpoint.region</a> | Nomad region to use. If not provided, the local agent region is used. | |

View file

@ -172,7 +172,10 @@ func (p *Provider) keepItem(ctx context.Context, i item) bool {
return false
}
// TODO: filter on health when that information exists (nomad 1.4+)
if !i.Healthy {
logger.Debug().Msgf("Filtering unhealthy item: %q", i.Name)
return false
}
return true
}

View file

@ -29,6 +29,7 @@ func Test_defaultRule(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
rule: "Host(`example.com`)",
@ -88,6 +89,7 @@ func Test_defaultRule(t *testing.T) {
},
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
rule: `Host("{{ .Name }}.{{ index .Labels "traefik.domain" }}")`,
@ -144,6 +146,7 @@ func Test_defaultRule(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
rule: `Host"{{ .Invalid }}")`,
@ -194,6 +197,7 @@ func Test_defaultRule(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
rule: defaultTemplateRule,
@ -273,6 +277,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -328,6 +333,7 @@ func Test_buildConfig(t *testing.T) {
Address: "192.168.1.101",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
{
ID: "id2",
@ -336,6 +342,7 @@ func Test_buildConfig(t *testing.T) {
Address: "192.168.1.102",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -411,6 +418,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
{
ID: "id2",
@ -420,6 +428,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.2",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -479,6 +488,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
{
ID: "id1",
@ -488,6 +498,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.2",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -544,6 +555,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
{
ID: "id1",
@ -553,6 +565,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.2",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -613,6 +626,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -672,6 +686,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -728,6 +743,7 @@ func Test_buildConfig(t *testing.T) {
Address: "",
Port: -1,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -764,6 +780,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -821,6 +838,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -879,6 +897,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -944,6 +963,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
{
ID: "id2",
@ -954,6 +974,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.2",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -996,6 +1017,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
{
ID: "id2",
@ -1006,6 +1028,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
{
ID: "id3",
@ -1016,6 +1039,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.2",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -1058,6 +1082,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
{
ID: "id2",
@ -1068,6 +1093,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.2",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -1128,6 +1154,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -1191,6 +1218,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
{
ID: "id2",
@ -1201,6 +1229,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.2",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -1267,6 +1296,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
{
ID: "id2",
@ -1277,6 +1307,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.2",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -1337,6 +1368,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
{
ID: "id2",
@ -1347,6 +1379,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.2",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -1401,6 +1434,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
{
ID: "id2",
@ -1411,6 +1445,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.2",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -1470,6 +1505,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -1528,6 +1564,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -1585,6 +1622,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -1643,6 +1681,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -1702,6 +1741,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -1739,6 +1779,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -1776,6 +1817,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -1839,6 +1881,7 @@ func Test_buildConfig(t *testing.T) {
Tags: []string{},
Address: "127.0.0.2",
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -1874,6 +1917,7 @@ func Test_buildConfig(t *testing.T) {
},
Address: "127.0.0.2",
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -1910,6 +1954,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: false},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -1946,6 +1991,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
constraints: `Tag("traefik.tags=bar")`,
@ -1983,6 +2029,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
constraints: `Tag("traefik.tags=foo")`,
@ -2042,6 +2089,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -2111,6 +2159,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -2170,6 +2219,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -2222,6 +2272,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -2273,6 +2324,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -2321,6 +2373,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -2376,6 +2429,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -2430,6 +2484,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
{
ID: "id2",
@ -2443,6 +2498,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.2",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -2524,6 +2580,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
{
ID: "id2",
@ -2536,6 +2593,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.2",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -2614,6 +2672,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -2660,6 +2719,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 9999,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -2707,6 +2767,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 80,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -2754,6 +2815,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 80,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
{
ID: "2",
@ -2770,6 +2832,7 @@ func Test_buildConfig(t *testing.T) {
Enable: true,
Canary: true,
},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -2849,6 +2912,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 80,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
{
ID: "2",
@ -2866,6 +2930,7 @@ func Test_buildConfig(t *testing.T) {
Enable: true,
Canary: true,
},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -2929,6 +2994,7 @@ func Test_buildConfig(t *testing.T) {
Address: "127.0.0.1",
Port: 80,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
{
ID: "2",
@ -2946,6 +3012,7 @@ func Test_buildConfig(t *testing.T) {
Enable: true,
Canary: true,
},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -3009,6 +3076,7 @@ func Test_buildConfig(t *testing.T) {
"traefik.tls.stores.default.defaultgeneratedcert.domain.main = foobar",
"traefik.tls.stores.default.defaultgeneratedcert.domain.sans = foobar, fiibar",
},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -3100,6 +3168,7 @@ func Test_buildConfigAllowEmptyServicesTrue(t *testing.T) {
Address: "",
Port: -1,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -3153,6 +3222,7 @@ func Test_buildConfigAllowEmptyServicesTrue(t *testing.T) {
Address: "",
Port: -1,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -3198,6 +3268,7 @@ func Test_buildConfigAllowEmptyServicesTrue(t *testing.T) {
Address: "",
Port: -1,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -3268,6 +3339,7 @@ func Test_buildConfigAllowEmptyServicesFalseDefault(t *testing.T) {
Address: "",
Port: -1,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -3304,6 +3376,7 @@ func Test_buildConfigAllowEmptyServicesFalseDefault(t *testing.T) {
Address: "",
Port: -1,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -3340,6 +3413,7 @@ func Test_buildConfigAllowEmptyServicesFalseDefault(t *testing.T) {
Address: "",
Port: -1,
ExtraConf: configuration{Enable: true},
Healthy: true,
},
},
expected: &dynamic.Configuration{
@ -3390,12 +3464,12 @@ func Test_keepItem(t *testing.T) {
}{
{
name: "enable true",
i: item{ExtraConf: configuration{Enable: true}},
i: item{ExtraConf: configuration{Enable: true}, Healthy: true},
exp: true,
},
{
name: "enable false",
i: item{ExtraConf: configuration{Enable: false}},
i: item{ExtraConf: configuration{Enable: false}, Healthy: true},
exp: false,
},
{
@ -3403,6 +3477,7 @@ func Test_keepItem(t *testing.T) {
i: item{
Tags: []string{"traefik.tags=foo"},
ExtraConf: configuration{Enable: true},
Healthy: true,
},
constraints: `Tag("traefik.tags=foo")`,
exp: true,
@ -3412,6 +3487,7 @@ func Test_keepItem(t *testing.T) {
i: item{
Tags: []string{"traefik.tags=foo"},
ExtraConf: configuration{Enable: true},
Healthy: true,
},
constraints: `Tag("traefik.tags=bar")`,
exp: false,

View file

@ -0,0 +1,12 @@
{
"ID": "03c7270c-f475-5981-1932-87c0a8a5aa24",
"ClientStatus": "running",
"DeploymentStatus": {
"Healthy": true
},
"TaskStates": {
"job1-http": {
"State": "running"
}
}
}

View file

@ -47,6 +47,7 @@ type item struct {
Tags []string // service tags
ExtraConf configuration // global options
Healthy bool
}
// configuration contains information from the service's tags that are globals
@ -63,6 +64,12 @@ type ProviderBuilder struct {
Namespaces []string `description:"Sets the Nomad namespaces used to discover services." json:"namespaces,omitempty" toml:"namespaces,omitempty" yaml:"namespaces,omitempty"`
}
// serviceWithHealth holds a service registration along with its health status.
type serviceWithHealth struct {
Service *api.ServiceRegistration
Healthy bool
}
// BuildProviders builds Nomad provider instances for the given namespaces configuration.
func (p *ProviderBuilder) BuildProviders() []*Provider {
if len(p.Namespaces) == 0 {
@ -96,6 +103,7 @@ type Configuration struct {
AllowEmptyServices bool `description:"Allow the creation of services without endpoints." json:"allowEmptyServices,omitempty" toml:"allowEmptyServices,omitempty" yaml:"allowEmptyServices,omitempty" export:"true"`
Watch bool `description:"Watch Nomad Service events." json:"watch,omitempty" toml:"watch,omitempty" yaml:"watch,omitempty" export:"true"`
ThrottleDuration ptypes.Duration `description:"Watch throttle duration." json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"`
DisableHealthCheck bool `description:"Disable health check filtering. Enable this for Nomad < 1.4 or for better performance." json:"disableHealthCheck,omitempty" toml:"disableHealthCheck,omitempty" yaml:"disableHealthCheck,omitempty" export:"true"`
}
// SetDefaults sets the default values for the Nomad Traefik Provider Configuration.
@ -344,6 +352,13 @@ func (p *Provider) getNomadServiceData(ctx context.Context) ([]item, error) {
return nil, err
}
// Fetch all allocations once for health checking
allocMap, err := p.fetchAllAllocations(ctx)
if err != nil {
log.Ctx(ctx).Warn().Err(err).Msg("Failed to fetch allocations for health filtering, all services will be marked as healthy")
allocMap = nil // Will mark all services as healthy
}
var items []item
for _, stub := range stubs {
@ -367,22 +382,23 @@ func (p *Provider) getNomadServiceData(ctx context.Context) ([]item, error) {
continue
}
instances, err := p.fetchService(ctx, service.ServiceName)
instances, err := p.fetchService(ctx, service.ServiceName, allocMap)
if err != nil {
return nil, err
}
for _, i := range instances {
items = append(items, item{
ID: i.ID,
Name: i.ServiceName,
Namespace: i.Namespace,
Node: i.NodeID,
Datacenter: i.Datacenter,
Address: i.Address,
Port: i.Port,
Tags: i.Tags,
ExtraConf: p.getExtraConf(i.Tags),
ID: i.Service.ID,
Name: i.Service.ServiceName,
Namespace: i.Service.Namespace,
Node: i.Service.NodeID,
Datacenter: i.Service.Datacenter,
Address: i.Service.Address,
Port: i.Service.Port,
Tags: i.Service.Tags,
ExtraConf: p.getExtraConf(i.Service.Tags),
Healthy: i.Healthy,
})
}
}
@ -400,6 +416,13 @@ func (p *Provider) getNomadServiceDataWithEmptyServices(ctx context.Context) ([]
return nil, err
}
// Fetch all allocations once for health checking
allocMap, err := p.fetchAllAllocations(ctx)
if err != nil {
log.Ctx(ctx).Warn().Err(err).Msg("Failed to fetch allocations for health filtering, all services will be marked as healthy")
allocMap = nil
}
var items []item
// Get Services even when they are scaled down to zero. Currently the nomad service interface does not support this. https://github.com/hashicorp/nomad/issues/19731
@ -443,7 +466,6 @@ func (p *Provider) getNomadServiceDataWithEmptyServices(ctx context.Context) ([]
}
if nil != taskGroup.Scaling && *taskGroup.Scaling.Enabled && *taskGroup.Count == 0 {
// Add items without address
items = append(items, item{
// Create a unique id for non registered services
ID: fmt.Sprintf("%s-%s-%s-%s-%s", *job.Namespace, *job.Name, *taskGroup.Name, service.TaskName, service.Name),
@ -455,24 +477,26 @@ func (p *Provider) getNomadServiceDataWithEmptyServices(ctx context.Context) ([]
Port: -1,
Tags: service.Tags,
ExtraConf: p.getExtraConf(service.Tags),
Healthy: true,
})
} else {
instances, err := p.fetchService(ctx, service.Name)
instances, err := p.fetchService(ctx, service.Name, allocMap)
if err != nil {
return nil, err
}
for _, i := range instances {
items = append(items, item{
ID: i.ID,
Name: i.ServiceName,
Namespace: i.Namespace,
Node: i.NodeID,
Datacenter: i.Datacenter,
Address: i.Address,
Port: i.Port,
Tags: i.Tags,
ExtraConf: p.getExtraConf(i.Tags),
ID: i.Service.ID,
Name: i.Service.ServiceName,
Namespace: i.Service.Namespace,
Node: i.Service.NodeID,
Datacenter: i.Service.Datacenter,
Address: i.Service.Address,
Port: i.Service.Port,
Tags: i.Service.Tags,
ExtraConf: p.getExtraConf(i.Service.Tags),
Healthy: i.Healthy,
})
}
}
@ -483,6 +507,28 @@ func (p *Provider) getNomadServiceDataWithEmptyServices(ctx context.Context) ([]
return items, nil
}
// fetchAllAllocations fetches all allocations from Nomad API once.
func (p *Provider) fetchAllAllocations(ctx context.Context) (map[string]*api.AllocationListStub, error) {
if p.DisableHealthCheck {
return nil, nil
}
opts := &api.QueryOptions{AllowStale: p.Stale}
opts = opts.WithContext(ctx)
allocs, _, err := p.client.Allocations().List(opts)
if err != nil {
return nil, fmt.Errorf("failed to fetch allocations: %w", err)
}
allocMap := make(map[string]*api.AllocationListStub, len(allocs))
for _, alloc := range allocs {
allocMap[alloc.ID] = alloc
}
return allocMap, nil
}
// getExtraConf returns a configuration with settings which are not part of the dynamic configuration (e.g. "<prefix>.enable").
func (p *Provider) getExtraConf(tags []string) configuration {
labels := tagsToLabels(tags, p.Prefix)
@ -502,15 +548,12 @@ func (p *Provider) getExtraConf(tags []string) configuration {
// fetchService queries Nomad API for services matching name,
// that also have the <prefix>.enable=true set in its tags.
func (p *Provider) fetchService(ctx context.Context, name string) ([]*api.ServiceRegistration, error) {
func (p *Provider) fetchService(ctx context.Context, name string, allocMap map[string]*api.AllocationListStub) ([]serviceWithHealth, error) {
var tagFilter string
if !p.ExposedByDefault {
tagFilter = fmt.Sprintf(`Tags contains %q`, fmt.Sprintf("%s.enable=true", p.Prefix))
}
// TODO: Nomad currently (v1.3.0) does not support health checks,
// and as such does not yet return health status information.
// When it does, refactor this section to include health status.
opts := &api.QueryOptions{AllowStale: p.Stale, Filter: tagFilter}
opts = opts.WithContext(ctx)
@ -518,7 +561,61 @@ func (p *Provider) fetchService(ctx context.Context, name string) ([]*api.Servic
if err != nil {
return nil, fmt.Errorf("failed to fetch services: %w", err)
}
return services, nil
// If health check is disabled or allocMap is nil, mark all services as healthy
if p.DisableHealthCheck || allocMap == nil {
result := make([]serviceWithHealth, 0, len(services))
for _, svc := range services {
result = append(result, serviceWithHealth{Service: svc, Healthy: true})
}
return result, nil
}
result := make([]serviceWithHealth, 0, len(services))
for _, service := range services {
healthy := p.isServiceHealthy(ctx, service, allocMap, name)
result = append(result, serviceWithHealth{Service: service, Healthy: healthy})
}
return result, nil
}
func (p *Provider) isServiceHealthy(ctx context.Context, service *api.ServiceRegistration, allocMap map[string]*api.AllocationListStub, serviceName string) bool {
logger := log.Ctx(ctx)
if service.AllocID == "" {
logger.Warn().Str("serviceID", service.ID).Msg("Service has no allocation ID, marking as unhealthy")
return false
}
alloc, exists := allocMap[service.AllocID]
if !exists {
logger.Warn().Str("allocID", service.AllocID).Str("serviceName", serviceName).Msg("Allocation not found, marking as unhealthy")
return false
}
if alloc.ClientStatus != "running" {
logger.Debug().Str("allocID", service.AllocID).Str("clientStatus", alloc.ClientStatus).Msg("Allocation not running")
return false
}
if alloc.DeploymentStatus != nil {
if alloc.DeploymentStatus.Healthy != nil && !*alloc.DeploymentStatus.Healthy {
logger.Debug().Str("allocID", service.AllocID).Msg("Deployment marked unhealthy")
return false
}
}
if alloc.TaskStates != nil {
for taskName, taskState := range alloc.TaskStates {
if taskState.State != "running" {
logger.Debug().Str("allocID", service.AllocID).Str("task", taskName).Str("state", taskState.State).Msg("Task not running")
return false
}
}
}
return true
}
func createClient(namespace string, endpoint *EndpointConfig) (*api.Client, error) {

View file

@ -9,6 +9,7 @@ import (
"strings"
"testing"
"github.com/hashicorp/nomad/api"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/types"
@ -153,6 +154,8 @@ func Test_getNomadServiceDataWithEmptyServices_GroupService_Scaling1(t *testing.
_, _ = w.Write(responses["job_job1_WithGroupService_Scaling1"])
case strings.HasSuffix(r.RequestURI, "/v1/service/job1"):
_, _ = w.Write(responses["service_job1"])
case strings.Contains(r.RequestURI, "/v1/allocation/"):
_, _ = w.Write(responses["alloc_healthy"])
}
}))
@ -213,6 +216,8 @@ func Test_getNomadServiceDataWithEmptyServices_GroupService_ScalingDisabled(t *t
_, _ = w.Write(responses["job_job3_WithGroupService_ScalingDisabled"])
case strings.HasSuffix(r.RequestURI, "/v1/service/job3"):
_, _ = w.Write(responses["service_job3"])
case strings.Contains(r.RequestURI, "/v1/allocation/"):
_, _ = w.Write(responses["alloc_healthy"])
}
}))
@ -277,6 +282,8 @@ func Test_getNomadServiceDataWithEmptyServices_GroupTaskService_Scaling1(t *test
_, _ = w.Write(responses["service_job5task1"])
case strings.HasSuffix(r.RequestURI, "/v1/service/job5task2"):
_, _ = w.Write(responses["service_job5task2"])
case strings.Contains(r.RequestURI, "/v1/allocation/"):
_, _ = w.Write(responses["alloc_healthy"])
}
}))
@ -339,6 +346,8 @@ func Test_getNomadServiceDataWithEmptyServices_TCP(t *testing.T) {
_, _ = w.Write(responses["job_job7_TCP"])
case strings.HasSuffix(r.RequestURI, "/v1/service/job7"):
_, _ = w.Write(responses["service_job7"])
case strings.Contains(r.RequestURI, "/v1/allocation/"):
_, _ = w.Write(responses["alloc_healthy"])
}
}))
@ -369,6 +378,8 @@ func Test_getNomadServiceDataWithEmptyServices_UDP(t *testing.T) {
_, _ = w.Write(responses["job_job8_UDP"])
case strings.HasSuffix(r.RequestURI, "/v1/service/job8"):
_, _ = w.Write(responses["service_job8"])
case strings.Contains(r.RequestURI, "/v1/allocation/"):
_, _ = w.Write(responses["alloc_healthy"])
}
}))
@ -449,6 +460,8 @@ func Test_getNomadServiceData(t *testing.T) {
_, _ = w.Write(responses["service_redis"])
case strings.HasSuffix(r.RequestURI, "/v1/service/hello-nomad"):
_, _ = w.Write(responses["service_hello"])
case strings.Contains(r.RequestURI, "/v1/allocation/"):
_, _ = w.Write(responses["alloc_healthy"])
}
}))
t.Cleanup(ts.Close)
@ -468,3 +481,78 @@ func Test_getNomadServiceData(t *testing.T) {
require.NoError(t, err)
require.Len(t, items, 2)
}
func Test_fetchService_HealthChecks(t *testing.T) {
serviceListJSON := []byte(`[
{"ID": "s1", "ServiceName": "app", "Tags": ["traefik.enable=true"], "AllocID": "alloc-ok"},
{"ID": "s2", "ServiceName": "app", "Tags": ["traefik.enable=true"], "AllocID": "alloc-dead-client"},
{"ID": "s3", "ServiceName": "app", "Tags": ["traefik.enable=true"], "AllocID": "alloc-bad-deploy"},
{"ID": "s4", "ServiceName": "app", "Tags": ["traefik.enable=true"], "AllocID": "alloc-dead-task"}
]`)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.RequestURI, "/v1/service/app") {
_, _ = w.Write(serviceListJSON)
}
}))
t.Cleanup(ts.Close)
p := new(Provider)
p.SetDefaults()
p.Endpoint.Address = ts.URL
p.client, _ = createClient(p.namespace, p.Endpoint)
// Create allocation map with different health states
healthyPtr := true
unhealthyPtr := false
allocMap := map[string]*api.AllocationListStub{
"alloc-ok": {
ID: "alloc-ok",
ClientStatus: "running",
DeploymentStatus: &api.AllocDeploymentStatus{
Healthy: &healthyPtr,
},
TaskStates: map[string]*api.TaskState{
"task1": {State: "running"},
},
},
"alloc-dead-client": {
ID: "alloc-dead-client",
ClientStatus: "complete",
},
"alloc-bad-deploy": {
ID: "alloc-bad-deploy",
ClientStatus: "running",
DeploymentStatus: &api.AllocDeploymentStatus{
Healthy: &unhealthyPtr,
},
},
"alloc-dead-task": {
ID: "alloc-dead-task",
ClientStatus: "running",
DeploymentStatus: &api.AllocDeploymentStatus{
Healthy: &healthyPtr,
},
TaskStates: map[string]*api.TaskState{
"task1": {State: "dead"},
},
},
}
services, err := p.fetchService(t.Context(), "app", allocMap)
require.NoError(t, err)
assert.Len(t, services, 4, "Should return all services")
// Count healthy services
healthyCount := 0
var healthyAllocID string
for _, svc := range services {
if svc.Healthy {
healthyCount++
healthyAllocID = svc.Service.AllocID
}
}
assert.Equal(t, 1, healthyCount, "Must filter out ClientDead, DeployBad, and TaskDead")
assert.Equal(t, "alloc-ok", healthyAllocID)
}