diff --git a/.golangci.toml b/.golangci.toml index 318e5cace..f867578b0 100644 --- a/.golangci.toml +++ b/.golangci.toml @@ -178,6 +178,7 @@ "SA1019: cfg.FeaturePolicy is deprecated", "SA1019: c.Providers.ConsulCatalog.Namespace is deprecated", "SA1019: c.Providers.Consul.Namespace is deprecated", + "SA1019: c.Providers.Nomad.Namespace is deprecated", ] [[issues.exclude-rules]] path = "(.+)_test.go" diff --git a/docs/content/deprecation/features.md b/docs/content/deprecation/features.md index 8837af814..5b32fc1bf 100644 --- a/docs/content/deprecation/features.md +++ b/docs/content/deprecation/features.md @@ -7,6 +7,7 @@ This page is maintained and updated periodically to reflect our roadmap and any | [Pilot](#pilot) | 2.7 | 2.8 | 2.9 | | [Consul Enterprise Namespace](#consul-enterprise-namespace) | 2.8 | N/A | 3.0 | | [TLS 1.0 and 1.1 Support](#tls-10-and-11) | N/A | 2.8 | N/A | +| [Nomad Namespace](#nomad-namespace) | 2.10 | N/A | 3.0 | ## Impact @@ -26,3 +27,8 @@ please use the `namespaces` options instead. ### TLS 1.0 and 1.1 Starting on 2.8 the default TLS options will use the minimum version of TLS 1.2. Of course, it can still be overridden with custom configuration. + +### Nomad Namespace + +Starting on 2.10 the `namespace` option of the Nomad provider is deprecated, +please use the `namespaces` options instead. diff --git a/docs/content/migration/v2.md b/docs/content/migration/v2.md index 00827cb28..d76910753 100644 --- a/docs/content/migration/v2.md +++ b/docs/content/migration/v2.md @@ -490,3 +490,9 @@ In `v2.8.2`, Traefik now reject certificates signed with the SHA-1 hash function ### Traefik Pilot In `v2.9`, Traefik Pilot support has been removed. + +## v2.10 + +### Nomad Namespace + +In `v2.10`, the `namespace` option of the Nomad provider is deprecated, please use the `namespaces` options instead. diff --git a/docs/content/providers/nomad.md b/docs/content/providers/nomad.md index 92ce542ad..572ccc0d6 100644 --- a/docs/content/providers/nomad.md +++ b/docs/content/providers/nomad.md @@ -442,24 +442,65 @@ For additional information, refer to [Restrict the Scope of Service Discovery](. ### `namespace` +??? warning "Deprecated in favor of the [`namespaces`](#namespaces) option." + + _Optional, Default=""_ + + The `namespace` option defines the namespace in which the Nomad services will be discovered. + + !!! warning + + One should only define either the `namespaces` option or the `namespace` option. + + ```yaml tab="File (YAML)" + providers: + nomad: + namespace: "production" + # ... + ``` + + ```toml tab="File (TOML)" + [providers.nomad] + namespace = "production" + # ... + ``` + + ```bash tab="CLI" + --providers.nomad.namespace=production + # ... + ``` + +### `namespaces` + _Optional, Default=""_ -The `namespace` option defines the namespace in which the Nomad services will be discovered. +The `namespaces` option defines the namespaces in which the nomad services will be discovered. +When using the `namespaces` option, the discovered object names will be suffixed as shown below: + +```text +@nomad- +``` + +!!! warning + + One should only define either the `namespaces` option or the `namespace` option. ```yaml tab="File (YAML)" providers: nomad: - namespace: "production" + namespaces: + - "ns1" + - "ns2" # ... ``` ```toml tab="File (TOML)" [providers.nomad] - namespace = "production" + namespaces = ["ns1", "ns2"] # ... ``` ```bash tab="CLI" ---providers.nomad.namespace=production +--providers.nomad.namespaces=ns1,ns2 # ... ``` diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index aa0638d08..d1c60130b 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -855,6 +855,9 @@ Expose Nomad services by default. (Default: ```true```) `--providers.nomad.namespace`: Sets the Nomad namespace used to discover services. +`--providers.nomad.namespaces`: +Sets the Nomad namespaces used to discover services. + `--providers.nomad.prefix`: Prefix for nomad service tags. (Default: ```traefik```) diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md index b496ce97a..5b2313d21 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -855,6 +855,9 @@ Expose Nomad services by default. (Default: ```true```) `TRAEFIK_PROVIDERS_NOMAD_NAMESPACE`: Sets the Nomad namespace used to discover services. +`TRAEFIK_PROVIDERS_NOMAD_NAMESPACES`: +Sets the Nomad namespaces used to discover services. + `TRAEFIK_PROVIDERS_NOMAD_PREFIX`: Prefix for nomad service tags. (Default: ```traefik```) diff --git a/docs/content/reference/static-configuration/file.toml b/docs/content/reference/static-configuration/file.toml index 3ad3d95c5..33297ecc9 100644 --- a/docs/content/reference/static-configuration/file.toml +++ b/docs/content/reference/static-configuration/file.toml @@ -181,6 +181,7 @@ prefix = "foobar" stale = true namespace = "foobar" + namespaces = ["foobar", "foobar"] exposedByDefault = true refreshInterval = "42s" [providers.nomad.endpoint] diff --git a/docs/content/reference/static-configuration/file.yaml b/docs/content/reference/static-configuration/file.yaml index dbe316d4a..fc9363f85 100644 --- a/docs/content/reference/static-configuration/file.yaml +++ b/docs/content/reference/static-configuration/file.yaml @@ -195,6 +195,9 @@ providers: prefix: foobar stale: true namespace: foobar + namespaces: + - foobar + - foobar exposedByDefault: true refreshInterval: 42s endpoint: diff --git a/pkg/config/static/static_config.go b/pkg/config/static/static_config.go index 090a1e058..03b6eae45 100644 --- a/pkg/config/static/static_config.go +++ b/pkg/config/static/static_config.go @@ -186,7 +186,7 @@ type Providers struct { Rest *rest.Provider `description:"Enable Rest backend with default settings." json:"rest,omitempty" toml:"rest,omitempty" yaml:"rest,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"` Rancher *rancher.Provider `description:"Enable Rancher backend with default settings." json:"rancher,omitempty" toml:"rancher,omitempty" yaml:"rancher,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"` ConsulCatalog *consulcatalog.ProviderBuilder `description:"Enable ConsulCatalog backend with default settings." json:"consulCatalog,omitempty" toml:"consulCatalog,omitempty" yaml:"consulCatalog,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` - Nomad *nomad.Provider `description:"Enable Nomad backend with default settings." json:"nomad,omitempty" toml:"nomad,omitempty" yaml:"nomad,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` + Nomad *nomad.ProviderBuilder `description:"Enable Nomad backend with default settings." json:"nomad,omitempty" toml:"nomad,omitempty" yaml:"nomad,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` Ecs *ecs.Provider `description:"Enable AWS ECS backend with default settings." json:"ecs,omitempty" toml:"ecs,omitempty" yaml:"ecs,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` Consul *consul.ProviderBuilder `description:"Enable Consul backend with default settings." json:"consul,omitempty" toml:"consul,omitempty" yaml:"consul,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"` @@ -326,11 +326,15 @@ func (c *Configuration) ValidateConfiguration() error { } if c.Providers.ConsulCatalog != nil && c.Providers.ConsulCatalog.Namespace != "" && len(c.Providers.ConsulCatalog.Namespaces) > 0 { - return fmt.Errorf("consul catalog provider cannot have both namespace and namespaces options configured") + return fmt.Errorf("Consul Catalog provider cannot have both namespace and namespaces options configured") } if c.Providers.Consul != nil && c.Providers.Consul.Namespace != "" && len(c.Providers.Consul.Namespaces) > 0 { - return fmt.Errorf("consul provider cannot have both namespace and namespaces options configured") + return fmt.Errorf("Consul provider cannot have both namespace and namespaces options configured") + } + + if c.Providers.Nomad != nil && c.Providers.Nomad.Namespace != "" && len(c.Providers.Nomad.Namespaces) > 0 { + return fmt.Errorf("Nomad provider cannot have both namespace and namespaces options configured") } return nil diff --git a/pkg/provider/aggregator/aggregator.go b/pkg/provider/aggregator/aggregator.go index 07d4488fb..3ddb1c3f2 100644 --- a/pkg/provider/aggregator/aggregator.go +++ b/pkg/provider/aggregator/aggregator.go @@ -115,7 +115,9 @@ func NewProviderAggregator(conf static.Providers) ProviderAggregator { } if conf.Nomad != nil { - p.quietAddProvider(conf.Nomad) + for _, pvd := range conf.Nomad.BuildProviders() { + p.quietAddProvider(pvd) + } } if conf.Consul != nil { diff --git a/pkg/provider/nomad/config_test.go b/pkg/provider/nomad/config_test.go index 4c72ee854..9d0aa21b8 100644 --- a/pkg/provider/nomad/config_test.go +++ b/pkg/provider/nomad/config_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/traefik/traefik/v2/pkg/config/dynamic" ) @@ -2509,5 +2510,57 @@ func Test_keepItem(t *testing.T) { } } +func TestNamespaces(t *testing.T) { + testCases := []struct { + desc string + namespace string + namespaces []string + expectedNamespaces []string + }{ + { + desc: "no defined namespaces", + expectedNamespaces: []string{""}, + }, + { + desc: "deprecated: use of defined namespace", + namespace: "test-ns", + expectedNamespaces: []string{"test-ns"}, + }, + { + desc: "use of 1 defined namespaces", + namespaces: []string{"test-ns"}, + expectedNamespaces: []string{"test-ns"}, + }, + { + desc: "use of multiple defined namespaces", + namespaces: []string{"test-ns1", "test-ns2", "test-ns3", "test-ns4"}, + expectedNamespaces: []string{"test-ns1", "test-ns2", "test-ns3", "test-ns4"}, + }, + } + + for _, test := range testCases { + test := test + + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + pb := &ProviderBuilder{ + Namespace: test.namespace, + Namespaces: test.namespaces, + } + + assert.Equal(t, test.expectedNamespaces, extractNamespacesFromProvider(pb.BuildProviders())) + }) + } +} + +func extractNamespacesFromProvider(providers []*Provider) []string { + res := make([]string, len(providers)) + for i, p := range providers { + res[i] = p.namespace + } + return res +} + func Int(v int) *int { return &v } func Bool(v bool) *bool { return &v } diff --git a/pkg/provider/nomad/nomad.go b/pkg/provider/nomad/nomad.go index 34877fb91..e321c05a5 100644 --- a/pkg/provider/nomad/nomad.go +++ b/pkg/provider/nomad/nomad.go @@ -2,6 +2,7 @@ package nomad import ( "context" + "errors" "fmt" "strings" "text/template" @@ -46,17 +47,68 @@ type item struct { ExtraConf configuration // global options } -// Provider holds configurations of the provider. -type Provider struct { +// ProviderBuilder is responsible for constructing namespaced instances of the Nomad provider. +type ProviderBuilder struct { + Configuration `yaml:",inline" export:"true"` + + // Deprecated: Use Namespaces option instead + Namespace string `description:"Sets the Nomad namespace used to discover services." json:"namespace,omitempty" toml:"namespace,omitempty" yaml:"namespace,omitempty"` + Namespaces []string `description:"Sets the Nomad namespaces used to discover services." json:"namespaces,omitempty" toml:"namespaces,omitempty" yaml:"namespaces,omitempty"` +} + +// BuildProviders builds Nomad provider instances for the given namespaces configuration. +func (p *ProviderBuilder) BuildProviders() []*Provider { + if p.Namespace != "" { + log.WithoutContext().Warnf("Namespace option is deprecated, please use the Namespaces option instead.") + } + + if len(p.Namespaces) == 0 { + return []*Provider{{ + Configuration: p.Configuration, + name: providerName, + // p.Namespace could be empty + namespace: p.Namespace, + }} + } + + var providers []*Provider + for _, namespace := range p.Namespaces { + providers = append(providers, &Provider{ + Configuration: p.Configuration, + name: providerName + "-" + namespace, + namespace: namespace, + }) + } + + return providers +} + +// Configuration represents the Nomad provider configuration. +type Configuration struct { DefaultRule string `description:"Default rule." json:"defaultRule,omitempty" toml:"defaultRule,omitempty" yaml:"defaultRule,omitempty"` Constraints string `description:"Constraints is an expression that Traefik matches against the Nomad service's tags to determine whether to create route(s) for that service." json:"constraints,omitempty" toml:"constraints,omitempty" yaml:"constraints,omitempty" export:"true"` Endpoint *EndpointConfig `description:"Nomad endpoint settings" json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty" export:"true"` Prefix string `description:"Prefix for nomad service tags." json:"prefix,omitempty" toml:"prefix,omitempty" yaml:"prefix,omitempty" export:"true"` Stale bool `description:"Use stale consistency for catalog reads." json:"stale,omitempty" toml:"stale,omitempty" yaml:"stale,omitempty" export:"true"` - Namespace string `description:"Sets the Nomad namespace used to discover services." json:"namespace,omitempty" toml:"namespace,omitempty" yaml:"namespace,omitempty" export:"true"` ExposedByDefault bool `description:"Expose Nomad services by default." json:"exposedByDefault,omitempty" toml:"exposedByDefault,omitempty" yaml:"exposedByDefault,omitempty" export:"true"` RefreshInterval ptypes.Duration `description:"Interval for polling Nomad API." json:"refreshInterval,omitempty" toml:"refreshInterval,omitempty" yaml:"refreshInterval,omitempty" export:"true"` +} +// SetDefaults sets the default values for the Nomad Traefik Provider Configuration. +func (c *Configuration) SetDefaults() { + c.Endpoint = &EndpointConfig{} + c.Prefix = defaultPrefix + c.ExposedByDefault = true + c.RefreshInterval = ptypes.Duration(15 * time.Second) + c.DefaultRule = defaultTemplateRule +} + +// Provider holds configuration along with the namespace it will discover services in. +type Provider struct { + Configuration + + name string + namespace string client *api.Client // client for Nomad API defaultRuleTpl *template.Template // default routing rule } @@ -72,22 +124,23 @@ type EndpointConfig struct { EndpointWaitTime ptypes.Duration `description:"WaitTime limits how long a Watch will block. If not provided, the agent default values will be used" json:"endpointWaitTime,omitempty" toml:"endpointWaitTime,omitempty" yaml:"endpointWaitTime,omitempty" export:"true"` } -// SetDefaults sets the default values for the Nomad Traefik Provider. -func (p *Provider) SetDefaults() { - p.Endpoint = &EndpointConfig{} - p.Prefix = defaultPrefix - p.ExposedByDefault = true - p.RefreshInterval = ptypes.Duration(15 * time.Second) - p.DefaultRule = defaultTemplateRule -} - // Init the Nomad Traefik Provider. func (p *Provider) Init() error { + if p.namespace == api.AllNamespacesNamespace { + return errors.New("wildcard namespace not supported") + } + defaultRuleTpl, err := provider.MakeDefaultRuleTemplate(p.DefaultRule, nil) if err != nil { return fmt.Errorf("error while parsing default rule: %w", err) } p.defaultRuleTpl = defaultRuleTpl + + // In case they didn't initialize Provider with BuildProviders + if p.name == "" { + p.name = providerName + } + return nil } @@ -95,13 +148,13 @@ func (p *Provider) Init() error { // using the given configuration channel. func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.Pool) error { var err error - p.client, err = createClient(p.Namespace, p.Endpoint) + p.client, err = createClient(p.namespace, p.Endpoint) if err != nil { return fmt.Errorf("failed to create nomad API client: %w", err) } pool.GoCtx(func(routineCtx context.Context) { - ctxLog := log.With(routineCtx, log.Str(log.ProviderName, providerName)) + ctxLog := log.With(routineCtx, log.Str(log.ProviderName, p.name)) logger := log.FromContext(ctxLog) operation := func() error { @@ -154,7 +207,7 @@ func (p *Provider) loadConfiguration(ctx context.Context, configurationC chan<- return err } configurationC <- dynamic.Message{ - ProviderName: providerName, + ProviderName: p.name, Configuration: p.buildConfig(ctx, items), } diff --git a/pkg/provider/nomad/nomad_test.go b/pkg/provider/nomad/nomad_test.go index 02e1217ef..b9d2bfa39 100644 --- a/pkg/provider/nomad/nomad_test.go +++ b/pkg/provider/nomad/nomad_test.go @@ -64,7 +64,12 @@ func Test_globalConfig(t *testing.T) { for _, test := range cases { t.Run(test.Name, func(t *testing.T) { - p := Provider{ExposedByDefault: test.ExposedByDefault, Prefix: test.Prefix} + p := Provider{ + Configuration: Configuration{ + ExposedByDefault: test.ExposedByDefault, + Prefix: test.Prefix, + }, + } result := p.getExtraConf(test.Tags) require.Equal(t, test.exp, result) }) @@ -91,7 +96,7 @@ func Test_getNomadServiceData(t *testing.T) { require.NoError(t, err) // fudge client, avoid starting up via Provide - p.client, err = createClient(p.Namespace, p.Endpoint) + p.client, err = createClient(p.namespace, p.Endpoint) require.NoError(t, err) // make the query for services diff --git a/webui/src/components/_commons/PanelMiddlewares.vue b/webui/src/components/_commons/PanelMiddlewares.vue index 3437f8bef..ef0a56e0d 100644 --- a/webui/src/components/_commons/PanelMiddlewares.vue +++ b/webui/src/components/_commons/PanelMiddlewares.vue @@ -1151,6 +1151,9 @@ export default { if (name.startsWith('consulcatalog-')) { return `statics/providers/consulcatalog.svg` } + if (name.startsWith('nomad-')) { + return `statics/providers/nomad.svg` + } return `statics/providers/${name}.svg` } diff --git a/webui/src/components/_commons/PanelMirroringServices.vue b/webui/src/components/_commons/PanelMirroringServices.vue index 849d99559..4ddb6a5c7 100644 --- a/webui/src/components/_commons/PanelMirroringServices.vue +++ b/webui/src/components/_commons/PanelMirroringServices.vue @@ -75,6 +75,9 @@ export default { if (name.startsWith('consulcatalog-')) { return `statics/providers/consulcatalog.svg` } + if (name.startsWith('nomad-')) { + return `statics/providers/nomad.svg` + } return `statics/providers/${name}.svg` } diff --git a/webui/src/components/_commons/PanelRouterDetails.vue b/webui/src/components/_commons/PanelRouterDetails.vue index 582d13133..4b5d2029b 100644 --- a/webui/src/components/_commons/PanelRouterDetails.vue +++ b/webui/src/components/_commons/PanelRouterDetails.vue @@ -141,6 +141,9 @@ export default { if (name.startsWith('consulcatalog-')) { return `statics/providers/consulcatalog.svg` } + if (name.startsWith('nomad-')) { + return `statics/providers/nomad.svg` + } return `statics/providers/${name}.svg` } diff --git a/webui/src/components/_commons/PanelServiceDetails.vue b/webui/src/components/_commons/PanelServiceDetails.vue index 6d8de0857..b73436172 100644 --- a/webui/src/components/_commons/PanelServiceDetails.vue +++ b/webui/src/components/_commons/PanelServiceDetails.vue @@ -155,6 +155,9 @@ export default { if (name.startsWith('consulcatalog-')) { return `statics/providers/consulcatalog.svg` } + if (name.startsWith('nomad-')) { + return `statics/providers/nomad.svg` + } return `statics/providers/${name}.svg` } diff --git a/webui/src/components/_commons/PanelWeightedServices.vue b/webui/src/components/_commons/PanelWeightedServices.vue index 2b6f539d6..e5b4c27ff 100644 --- a/webui/src/components/_commons/PanelWeightedServices.vue +++ b/webui/src/components/_commons/PanelWeightedServices.vue @@ -75,6 +75,9 @@ export default { if (name.startsWith('consulcatalog-')) { return `statics/providers/consulcatalog.svg` } + if (name.startsWith('nomad-')) { + return `statics/providers/nomad.svg` + } return `statics/providers/${name}.svg` } diff --git a/webui/src/components/_commons/ProviderIcon.vue b/webui/src/components/_commons/ProviderIcon.vue index 6b3bac409..2d792ada5 100644 --- a/webui/src/components/_commons/ProviderIcon.vue +++ b/webui/src/components/_commons/ProviderIcon.vue @@ -20,6 +20,9 @@ export default { if (name.startsWith('consulcatalog-')) { return `statics/providers/consulcatalog.svg` } + if (name.startsWith('nomad-')) { + return `statics/providers/nomad.svg` + } return `statics/providers/${name}.svg` } diff --git a/webui/src/components/dashboard/PanelProvider.vue b/webui/src/components/dashboard/PanelProvider.vue index a4d451866..59fbd4782 100644 --- a/webui/src/components/dashboard/PanelProvider.vue +++ b/webui/src/components/dashboard/PanelProvider.vue @@ -37,6 +37,9 @@ export default { if (name.startsWith('consulcatalog-')) { return `statics/providers/consulcatalog.svg` } + if (name.startsWith('nomad-')) { + return `statics/providers/nomad.svg` + } return `statics/providers/${name}.svg` }