diff --git a/.golangci.yml b/.golangci.yml index 7128a4177..08685510f 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -280,3 +280,9 @@ issues: text: 'unusedwrite: unused write to field' linters: - govet + - path: pkg/cli/deprecation.go + linters: + - goconst + - path: pkg/cli/loader_file.go + linters: + - goconst diff --git a/cmd/traefik/traefik.go b/cmd/traefik/traefik.go index 0ac89fb46..1274e5104 100644 --- a/cmd/traefik/traefik.go +++ b/cmd/traefik/traefik.go @@ -53,7 +53,7 @@ func main() { // traefik config inits tConfig := cmd.NewTraefikConfiguration() - loaders := []cli.ResourceLoader{&tcli.FileLoader{}, &tcli.FlagLoader{}, &tcli.EnvLoader{}} + loaders := []cli.ResourceLoader{&tcli.DeprecationLoader{}, &tcli.FileLoader{}, &tcli.FlagLoader{}, &tcli.EnvLoader{}} cmdTraefik := &cli.Command{ Name: "traefik", diff --git a/docs/content/migration/v2-to-v3.md b/docs/content/migration/v2-to-v3.md index 70b27f820..4396e0887 100644 --- a/docs/content/migration/v2-to-v3.md +++ b/docs/content/migration/v2-to-v3.md @@ -19,6 +19,8 @@ and how it now looks like in v3. ### Docker & Docker Swarm +#### SwarmMode + In v3, the provider Docker has been split into 2 providers: - Docker provider (without Swarm support) @@ -43,7 +45,7 @@ In v3, the provider Docker has been split into 2 providers: This configuration is now unsupported and would prevent Traefik to start. -#### Remediation +##### Remediation In v3, the `swarmMode` should not be used with the Docker provider, and, to use Swarm, the Swarm provider should be used instead. @@ -64,7 +66,35 @@ In v3, the `swarmMode` should not be used with the Docker provider, and, to use --providers.swarm.endpoint=tcp://127.0.0.1:2377 ``` -### HTTP3 Experimental Configuration +#### TLS.CAOptional + +Docker provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://pkg.go.dev/crypto/tls#ClientAuthType). + +??? example "An example usage of the TLS.CAOptional option" + + ```yaml tab="File (YAML)" + providers: + docker: + tls: + caOptional: true + ``` + + ```toml tab="File (TOML)" + [providers.docker.tls] + caOptional=true + ``` + + ```bash tab="CLI" + --providers.docker.tls.caOptional=true + ``` + +##### Remediation + +The `tls.caOptional` option should be removed from the Docker provider static configuration. + +### Experimental Configuration + +#### HTTP3 In v3, HTTP/3 is no longer an experimental feature. It can be enabled on entry points without the associated `experimental.http3` option, which is now removed. @@ -86,12 +116,14 @@ It is now unsupported and would prevent Traefik to start. --experimental.http3=true ``` -#### Remediation +##### Remediation The `http3` option should be removed from the static configuration experimental section. ### Consul provider +#### namespace + The Consul provider `namespace` option was deprecated in v2 and is now removed in v3. It is now unsupported and would prevent Traefik to start. @@ -111,7 +143,7 @@ It is now unsupported and would prevent Traefik to start. --consul.namespace=foobar ``` -#### Remediation +##### Remediation In v3, the `namespaces` option should be used instead of the `namespace` option. @@ -132,8 +164,36 @@ In v3, the `namespaces` option should be used instead of the `namespace` option. --consul.namespaces=foobar ``` +#### TLS.CAOptional + +Consul provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://pkg.go.dev/crypto/tls#ClientAuthType). + +??? example "An example usage of the TLS.CAOptional option" + + ```yaml tab="File (YAML)" + providers: + consul: + tls: + caOptional: true + ``` + + ```toml tab="File (TOML)" + [providers.consul.tls] + caOptional=true + ``` + + ```bash tab="CLI" + --providers.consul.tls.caOptional=true + ``` + +##### Remediation + +The `tls.caOptional` option should be removed from the Consul provider static configuration. + ### ConsulCatalog provider +#### namespace + The ConsulCatalog provider `namespace` option was deprecated in v2 and is now removed in v3. It is now unsupported and would prevent Traefik to start. @@ -153,7 +213,7 @@ It is now unsupported and would prevent Traefik to start. --consulCatalog.namespace=foobar ``` -#### Remediation +##### Remediation In v3, the `namespaces` option should be used instead of the `namespace` option. @@ -174,8 +234,37 @@ In v3, the `namespaces` option should be used instead of the `namespace` option. --consulCatalog.namespaces=foobar ``` +#### Endpoint.TLS.CAOptional + +ConsulCatalog provider `endpoint.tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://pkg.go.dev/crypto/tls#ClientAuthType). + +??? example "An example usage of the Endpoint.TLS.CAOptional option" + + ```yaml tab="File (YAML)" + providers: + consulCatalog: + endpoint: + tls: + caOptional: true + ``` + + ```toml tab="File (TOML)" + [providers.consulCatalog.endpoint.tls] + caOptional=true + ``` + + ```bash tab="CLI" + --providers.consulCatalog.endpoint.tls.caOptional=true + ``` + +##### Remediation + +The `endpoint.tls.caOptional` option should be removed from the ConsulCatalog provider static configuration. + ### Nomad provider +#### namespace + The Nomad provider `namespace` option was deprecated in v2 and is now removed in v3. It is now unsupported and would prevent Traefik to start. @@ -195,7 +284,7 @@ It is now unsupported and would prevent Traefik to start. --nomad.namespace=foobar ``` -#### Remediation +##### Remediation In v3, the `namespaces` option should be used instead of the `namespace` option. @@ -216,6 +305,33 @@ In v3, the `namespaces` option should be used instead of the `namespace` option. --nomad.namespaces=foobar ``` +#### Endpoint.TLS.CAOptional + +Nomad provider `endpoint.tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://pkg.go.dev/crypto/tls#ClientAuthType). + +??? example "An example usage of the Endpoint.TLS.CAOptional option" + + ```yaml tab="File (YAML)" + providers: + nomad: + endpoint: + tls: + caOptional: true + ``` + + ```toml tab="File (TOML)" + [providers.nomad.endpoint.tls] + caOptional=true + ``` + + ```bash tab="CLI" + --providers.nomad.endpoint.tls.caOptional=true + ``` + +##### Remediation + +The `endpoint.tls.caOptional` option should be removed from the Nomad provider static configuration. + ### Rancher v1 Provider In v3, the Rancher v1 provider has been removed because Rancher v1 is [no longer actively maintaned](https://rancher.com/docs/os/v1.x/en/support/), @@ -271,6 +387,90 @@ This configuration is now unsupported and would prevent Traefik to start. All Marathon provider related configuration should be removed from the static configuration. +### HTTP Provider + +#### TLS.CAOptional + +HTTP provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://pkg.go.dev/crypto/tls#ClientAuthType). + +??? example "An example usage of the TLS.CAOptional option" + + ```yaml tab="File (YAML)" + providers: + http: + tls: + caOptional: true + ``` + + ```toml tab="File (TOML)" + [providers.http.tls] + caOptional=true + ``` + + ```bash tab="CLI" + --providers.http.tls.caOptional=true + ``` + +##### Remediation + +The `tls.caOptional` option should be removed from the HTTP provider static configuration. + +### ETCD Provider + +#### TLS.CAOptional + +ETCD provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://pkg.go.dev/crypto/tls#ClientAuthType). + +??? example "An example usage of the TLS.CAOptional option" + + ```yaml tab="File (YAML)" + providers: + etcd: + tls: + caOptional: true + ``` + + ```toml tab="File (TOML)" + [providers.etcd.tls] + caOptional=true + ``` + + ```bash tab="CLI" + --providers.etcd.tls.caOptional=true + ``` + +##### Remediation + +The `tls.caOptional` option should be removed from the ETCD provider static configuration. + +### Redis Provider + +#### TLS.CAOptional + +Redis provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://pkg.go.dev/crypto/tls#ClientAuthType). + +??? example "An example usage of the TLS.CAOptional option" + + ```yaml tab="File (YAML)" + providers: + redis: + tls: + caOptional: true + ``` + + ```toml tab="File (TOML)" + [providers.redis.tls] + caOptional=true + ``` + + ```bash tab="CLI" + --providers.redis.tls.caOptional=true + ``` + +##### Remediation + +The `tls.caOptional` option should be removed from the Redis provider static configuration. + ### InfluxDB v1 InfluxDB v1.x maintenance [ended in 2021](https://www.influxdata.com/blog/influxdb-oss-and-enterprise-roadmap-update-from-influxdays-emea/). @@ -415,3 +615,5 @@ Here are two possible transition strategies: For legacy stacks that cannot immediately upgrade to the latest vendor agents supporting OTLP ingestion, using OpenTelemetry (OTel) collectors with appropriate exporters configuration is a viable solution. This allows continued compatibility with the existing infrastructure. + +Please check the [OpenTelemetry Tracing provider documention](../observability/tracing/opentelemetry.md) for more information. diff --git a/pkg/cli/deprecation.go b/pkg/cli/deprecation.go new file mode 100644 index 000000000..379d986a7 --- /dev/null +++ b/pkg/cli/deprecation.go @@ -0,0 +1,541 @@ +package cli + +import ( + "errors" + "os" + "reflect" + "strings" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/traefik/paerser/cli" + "github.com/traefik/paerser/flag" + "github.com/traefik/paerser/parser" +) + +type DeprecationLoader struct{} + +func (d DeprecationLoader) Load(args []string, cmd *cli.Command) (bool, error) { + if logDeprecation(cmd.Configuration, args) { + return true, errors.New("incompatible deprecated static option found") + } + + return false, nil +} + +// logDeprecation prints deprecation hints and returns whether incompatible deprecated options need to be removed. +func logDeprecation(traefikConfiguration interface{}, args []string) bool { + // This part doesn't handle properly a flag defined like this: + // --accesslog true + // where `true` could be considered as a new argument. + // This is not really an issue with the deprecation loader since it will filter the unknown nodes later in this + // function. + for i, arg := range args { + if !strings.Contains(arg, "=") { + args[i] = arg + "=true" + } + } + + labels, err := flag.Parse(args, nil) + if err != nil { + log.Error().Err(err).Msg("deprecated static options analysis failed") + return false + } + + node, err := parser.DecodeToNode(labels, "traefik") + if err != nil { + log.Error().Err(err).Msg("deprecated static options analysis failed") + return false + } + + if node != nil && len(node.Children) > 0 { + config := &configuration{} + filterUnknownNodes(reflect.TypeOf(config), node) + + if len(node.Children) > 0 { + // Telling parser to look for the label struct tag to allow empty values. + err = parser.AddMetadata(config, node, parser.MetadataOpts{TagName: "label"}) + if err != nil { + log.Error().Err(err).Msg("deprecated static options analysis failed") + return false + } + + err = parser.Fill(config, node, parser.FillerOpts{}) + if err != nil { + log.Error().Err(err).Msg("deprecated static options analysis failed") + return false + } + + if config.deprecationNotice(log.With().Str("loader", "FLAG").Logger()) { + return true + } + + // No further deprecation parsing and logging, + // as args configuration contains at least one deprecated option. + return false + } + } + + // FILE + ref, err := flag.Parse(args, traefikConfiguration) + if err != nil { + log.Error().Err(err).Msg("deprecated static options analysis failed") + return false + } + + configFileFlag := "traefik.configfile" + if _, ok := ref["traefik.configFile"]; ok { + configFileFlag = "traefik.configFile" + } + + config := &configuration{} + _, err = loadConfigFiles(ref[configFileFlag], config) + + if err == nil { + if config.deprecationNotice(log.With().Str("loader", "FILE").Logger()) { + return true + } + } + + config = &configuration{} + l := EnvLoader{} + _, err = l.Load(os.Args, &cli.Command{ + Configuration: config, + }) + + if err == nil { + if config.deprecationNotice(log.With().Str("loader", "ENV").Logger()) { + return true + } + } + + return false +} + +func filterUnknownNodes(fType reflect.Type, node *parser.Node) bool { + var children []*parser.Node + for _, child := range node.Children { + if hasKnownNodes(fType, child) { + children = append(children, child) + } + } + + node.Children = children + return len(node.Children) > 0 +} + +func hasKnownNodes(rootType reflect.Type, node *parser.Node) bool { + rType := rootType + if rootType.Kind() == reflect.Pointer { + rType = rootType.Elem() + } + + // unstructured type fitting anything, considering the current node as known. + if rType.Kind() == reflect.Map && rType.Elem().Kind() == reflect.Interface { + return true + } + + // unstructured type fitting anything, considering the current node as known. + if rType.Kind() == reflect.Interface { + return true + } + + // find matching field in struct type. + field, b := findTypedField(rType, node) + if !b { + return b + } + + if len(node.Children) > 0 { + return filterUnknownNodes(field.Type, node) + } + + return true +} + +func findTypedField(rType reflect.Type, node *parser.Node) (reflect.StructField, bool) { + // avoid panicking. + if rType.Kind() != reflect.Struct { + return reflect.StructField{}, false + } + + for i := 0; i < rType.NumField(); i++ { + cField := rType.Field(i) + + // ignore unexported fields. + if cField.PkgPath == "" { + if strings.EqualFold(cField.Name, node.Name) { + node.FieldName = cField.Name + return cField, true + } + } + } + + return reflect.StructField{}, false +} + +// configuration holds the static configuration removed/deprecated options. +type configuration struct { + Experimental *experimental `json:"experimental,omitempty" toml:"experimental,omitempty" yaml:"experimental,omitempty" label:"allowEmpty" file:"allowEmpty"` + Pilot map[string]any `json:"pilot,omitempty" toml:"pilot,omitempty" yaml:"pilot,omitempty" label:"allowEmpty" file:"allowEmpty"` + Providers *providers `json:"providers,omitempty" toml:"providers,omitempty" yaml:"providers,omitempty" label:"allowEmpty" file:"allowEmpty"` + Tracing *tracing `json:"tracing,omitempty" toml:"tracing,omitempty" yaml:"tracing,omitempty" label:"allowEmpty" file:"allowEmpty"` +} + +func (c *configuration) deprecationNotice(logger zerolog.Logger) bool { + if c == nil { + return false + } + + var incompatible bool + if c.Pilot != nil { + incompatible = true + logger.Error().Msg("Pilot configuration has been removed in v3, please remove all Pilot-related static configuration for Traefik to start." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#pilot") + } + + incompatibleExperimental := c.Experimental.deprecationNotice(logger) + incompatibleProviders := c.Providers.deprecationNotice(logger) + incompatibleTracing := c.Tracing.deprecationNotice(logger) + return incompatible || incompatibleExperimental || incompatibleProviders || incompatibleTracing +} + +type providers struct { + Docker *docker `json:"docker,omitempty" toml:"docker,omitempty" yaml:"docker,omitempty" label:"allowEmpty" file:"allowEmpty"` + Swarm *swarm `json:"swarm,omitempty" toml:"swarm,omitempty" yaml:"swarm,omitempty" label:"allowEmpty" file:"allowEmpty"` + Consul *consul `json:"consul,omitempty" toml:"consul,omitempty" yaml:"consul,omitempty" label:"allowEmpty" file:"allowEmpty"` + ConsulCatalog *consulCatalog `json:"consulCatalog,omitempty" toml:"consulCatalog,omitempty" yaml:"consulCatalog,omitempty" label:"allowEmpty" file:"allowEmpty"` + Nomad *nomad `json:"nomad,omitempty" toml:"nomad,omitempty" yaml:"nomad,omitempty" label:"allowEmpty" file:"allowEmpty"` + Marathon map[string]any `json:"marathon,omitempty" toml:"marathon,omitempty" yaml:"marathon,omitempty" label:"allowEmpty" file:"allowEmpty"` + Rancher map[string]any `json:"rancher,omitempty" toml:"rancher,omitempty" yaml:"rancher,omitempty" label:"allowEmpty" file:"allowEmpty"` + ETCD *etcd `json:"etcd,omitempty" toml:"etcd,omitempty" yaml:"etcd,omitempty" label:"allowEmpty" file:"allowEmpty"` + Redis *redis `json:"redis,omitempty" toml:"redis,omitempty" yaml:"redis,omitempty" label:"allowEmpty" file:"allowEmpty"` + HTTP *http `json:"http,omitempty" toml:"http,omitempty" yaml:"http,omitempty" label:"allowEmpty" file:"allowEmpty"` +} + +func (p *providers) deprecationNotice(logger zerolog.Logger) bool { + if p == nil { + return false + } + + var incompatible bool + + if p.Marathon != nil { + incompatible = true + logger.Error().Msg("Marathon provider has been removed in v3, please remove all Marathon-related static configuration for Traefik to start." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#marathon-provider") + } + + if p.Rancher != nil { + incompatible = true + logger.Error().Msg("Rancher provider has been removed in v3, please remove all Rancher-related static configuration for Traefik to start." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#rancher-v1-provider") + } + + dockerIncompatible := p.Docker.deprecationNotice(logger) + consulIncompatible := p.Consul.deprecationNotice(logger) + consulCatalogIncompatible := p.ConsulCatalog.deprecationNotice(logger) + nomadIncompatible := p.Nomad.deprecationNotice(logger) + swarmIncompatible := p.Swarm.deprecationNotice(logger) + etcdIncompatible := p.ETCD.deprecationNotice(logger) + redisIncompatible := p.Redis.deprecationNotice(logger) + httpIncompatible := p.HTTP.deprecationNotice(logger) + return incompatible || + dockerIncompatible || + consulIncompatible || + consulCatalogIncompatible || + nomadIncompatible || + swarmIncompatible || + etcdIncompatible || + redisIncompatible || + httpIncompatible +} + +type tls struct { + CAOptional *bool `json:"caOptional,omitempty" toml:"caOptional,omitempty" yaml:"caOptional,omitempty"` +} + +type docker struct { + SwarmMode *bool `json:"swarmMode,omitempty" toml:"swarmMode,omitempty" yaml:"swarmMode,omitempty"` + TLS *tls `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty"` +} + +func (d *docker) deprecationNotice(logger zerolog.Logger) bool { + if d == nil { + return false + } + + var incompatible bool + + if d.SwarmMode != nil { + incompatible = true + logger.Error().Msg("Docker provider `swarmMode` option has been removed in v3, please use the Swarm Provider instead." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#docker-docker-swarm") + } + + if d.TLS != nil && d.TLS.CAOptional != nil { + incompatible = true + logger.Error().Msg("Docker provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://github.com/golang/go/blob/740a490f71d026bb7d2d13cb8fa2d6d6e0572b70/src/crypto/tls/common.go#L634)." + + "Please remove all occurrences from the static configuration for Traefik to start." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#tlscaoptional") + } + + return incompatible +} + +type swarm struct { + TLS *tls `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty"` +} + +func (s *swarm) deprecationNotice(logger zerolog.Logger) bool { + if s == nil { + return false + } + + var incompatible bool + + if s.TLS != nil && s.TLS.CAOptional != nil { + incompatible = true + logger.Error().Msg("Swarm provider `tls.CAOptional` option does not exist, as TLS client authentication is a server side option (see https://github.com/golang/go/blob/740a490f71d026bb7d2d13cb8fa2d6d6e0572b70/src/crypto/tls/common.go#L634)." + + "Please remove all occurrences from the static configuration for Traefik to start.") + } + + return incompatible +} + +type etcd struct { + TLS *tls `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty"` +} + +func (e *etcd) deprecationNotice(logger zerolog.Logger) bool { + if e == nil { + return false + } + + var incompatible bool + + if e.TLS != nil && e.TLS.CAOptional != nil { + incompatible = true + logger.Error().Msg("ETCD provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://github.com/golang/go/blob/740a490f71d026bb7d2d13cb8fa2d6d6e0572b70/src/crypto/tls/common.go#L634)." + + "Please remove all occurrences from the static configuration for Traefik to start." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#tlscaoptional_3") + } + + return incompatible +} + +type redis struct { + TLS *tls `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty"` +} + +func (r *redis) deprecationNotice(logger zerolog.Logger) bool { + if r == nil { + return false + } + + var incompatible bool + + if r.TLS != nil && r.TLS.CAOptional != nil { + incompatible = true + logger.Error().Msg("Redis provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://github.com/golang/go/blob/740a490f71d026bb7d2d13cb8fa2d6d6e0572b70/src/crypto/tls/common.go#L634)." + + "Please remove all occurrences from the static configuration for Traefik to start." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#tlscaoptional_4") + } + + return incompatible +} + +type consul struct { + Namespace *string `json:"namespace,omitempty" toml:"namespace,omitempty" yaml:"namespace,omitempty"` + TLS *tls `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty"` +} + +func (c *consul) deprecationNotice(logger zerolog.Logger) bool { + if c == nil { + return false + } + + var incompatible bool + + if c.Namespace != nil { + incompatible = true + logger.Error().Msg("Consul provider `namespace` option has been removed, please use the `namespaces` option instead." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#consul-provider") + } + + if c.TLS != nil && c.TLS.CAOptional != nil { + incompatible = true + logger.Error().Msg("Consul provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://github.com/golang/go/blob/740a490f71d026bb7d2d13cb8fa2d6d6e0572b70/src/crypto/tls/common.go#L634)." + + "Please remove all occurrences from the static configuration for Traefik to start." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#tlscaoptional_1") + } + + return incompatible +} + +type consulCatalog struct { + Namespace *string `json:"namespace,omitempty" toml:"namespace,omitempty" yaml:"namespace,omitempty"` + Endpoint *endpointConfig `json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty" label:"allowEmpty" file:"allowEmpty"` +} + +type endpointConfig struct { + TLS *tls `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty"` +} + +func (c *consulCatalog) deprecationNotice(logger zerolog.Logger) bool { + if c == nil { + return false + } + + var incompatible bool + + if c.Namespace != nil { + incompatible = true + logger.Error().Msg("ConsulCatalog provider `namespace` option has been removed, please use the `namespaces` option instead." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#consulcatalog-provider") + } + + if c.Endpoint != nil && c.Endpoint.TLS != nil && c.Endpoint.TLS.CAOptional != nil { + incompatible = true + logger.Error().Msg("ConsulCatalog provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://github.com/golang/go/blob/740a490f71d026bb7d2d13cb8fa2d6d6e0572b70/src/crypto/tls/common.go#L634)." + + "Please remove all occurrences from the static configuration for Traefik to start." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#endpointtlscaoptional") + } + + return incompatible +} + +type nomad struct { + Namespace *string `json:"namespace,omitempty" toml:"namespace,omitempty" yaml:"namespace,omitempty"` + Endpoint *endpointConfig `json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty" label:"allowEmpty" file:"allowEmpty"` +} + +func (n *nomad) deprecationNotice(logger zerolog.Logger) bool { + if n == nil { + return false + } + + var incompatible bool + + if n.Namespace != nil { + incompatible = true + logger.Error().Msg("Nomad provider `namespace` option has been removed, please use the `namespaces` option instead." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#nomad-provider") + } + + if n.Endpoint != nil && n.Endpoint.TLS != nil && n.Endpoint.TLS.CAOptional != nil { + incompatible = true + logger.Error().Msg("Nomad provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://github.com/golang/go/blob/740a490f71d026bb7d2d13cb8fa2d6d6e0572b70/src/crypto/tls/common.go#L634)." + + "Please remove all occurrences from the static configuration for Traefik to start." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#endpointtlscaoptional_1") + } + + return incompatible +} + +type http struct { + TLS *tls `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty"` +} + +func (h *http) deprecationNotice(logger zerolog.Logger) bool { + if h == nil { + return false + } + + var incompatible bool + + if h.TLS != nil && h.TLS.CAOptional != nil { + incompatible = true + logger.Error().Msg("HTTP provider `tls.CAOptional` option has been removed in v3, as TLS client authentication is a server side option (see https://github.com/golang/go/blob/740a490f71d026bb7d2d13cb8fa2d6d6e0572b70/src/crypto/tls/common.go#L634)." + + "Please remove all occurrences from the static configuration for Traefik to start." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#tlscaoptional_2") + } + + return incompatible +} + +type experimental struct { + HTTP3 *bool `json:"http3,omitempty" toml:"http3,omitempty" yaml:"http3,omitempty"` +} + +func (e *experimental) deprecationNotice(logger zerolog.Logger) bool { + if e == nil { + return false + } + + if e.HTTP3 != nil { + logger.Error().Msg("HTTP3 is not an experimental feature in v3 and the associated enablement has been removed." + + "Please remove its usage from the static configuration for Traefik to start." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#http3-experimental-configuration") + + return true + } + + return false +} + +type tracing struct { + SpanNameLimit *int `json:"spanNameLimit,omitempty" toml:"spanNameLimit,omitempty" yaml:"spanNameLimit,omitempty"` + Jaeger map[string]any `json:"jaeger,omitempty" toml:"jaeger,omitempty" yaml:"jaeger,omitempty" label:"allowEmpty" file:"allowEmpty"` + Zipkin map[string]any `json:"zipkin,omitempty" toml:"zipkin,omitempty" yaml:"zipkin,omitempty" label:"allowEmpty" file:"allowEmpty"` + Datadog map[string]any `json:"datadog,omitempty" toml:"datadog,omitempty" yaml:"datadog,omitempty" label:"allowEmpty" file:"allowEmpty"` + Instana map[string]any `json:"instana,omitempty" toml:"instana,omitempty" yaml:"instana,omitempty" label:"allowEmpty" file:"allowEmpty"` + Haystack map[string]any `json:"haystack,omitempty" toml:"haystack,omitempty" yaml:"haystack,omitempty" label:"allowEmpty" file:"allowEmpty"` + Elastic map[string]any `json:"elastic,omitempty" toml:"elastic,omitempty" yaml:"elastic,omitempty" label:"allowEmpty" file:"allowEmpty"` +} + +func (t *tracing) deprecationNotice(logger zerolog.Logger) bool { + if t == nil { + return false + } + var incompatible bool + if t.SpanNameLimit != nil { + incompatible = true + logger.Error().Msg("SpanNameLimit option for Tracing has been removed in v3, as Span names are now of a fixed length." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#tracing") + } + + if t.Jaeger != nil { + incompatible = true + logger.Error().Msg("Jaeger Tracing backend has been removed in v3, please remove all Jaeger-related Tracing static configuration for Traefik to start." + + "In v3, Open Telemetry replaces specific tracing backend implementations, and an collector/exporter can be used to export metrics in a vendor specific format." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#tracing") + } + + if t.Zipkin != nil { + incompatible = true + logger.Error().Msg("Zipkin Tracing backend has been removed in v3, please remove all Zipkin-related Tracing static configuration for Traefik to start." + + "In v3, Open Telemetry replaces specific tracing backend implementations, and an collector/exporter can be used to export metrics in a vendor specific format." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#tracing") + } + + if t.Datadog != nil { + incompatible = true + logger.Error().Msg("Datadog Tracing backend has been removed in v3, please remove all Datadog-related Tracing static configuration for Traefik to start." + + "In v3, Open Telemetry replaces specific tracing backend implementations, and an collector/exporter can be used to export metrics in a vendor specific format." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#tracing") + } + + if t.Instana != nil { + incompatible = true + logger.Error().Msg("Instana Tracing backend has been removed in v3, please remove all Instana-related Tracing static configuration for Traefik to start." + + "In v3, Open Telemetry replaces specific tracing backend implementations, and an collector/exporter can be used to export metrics in a vendor specific format." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#tracing") + } + + if t.Haystack != nil { + incompatible = true + logger.Error().Msg("Haystack Tracing backend has been removed in v3, please remove all Haystack-related Tracing static configuration for Traefik to start." + + "In v3, Open Telemetry replaces specific tracing backend implementations, and an collector/exporter can be used to export metrics in a vendor specific format." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#tracing") + } + + if t.Elastic != nil { + incompatible = true + logger.Error().Msg("Elastic Tracing backend has been removed in v3, please remove all Elastic-related Tracing static configuration for Traefik to start." + + "In v3, Open Telemetry replaces specific tracing backend implementations, and an collector/exporter can be used to export metrics in a vendor specific format." + + "For more information please read the migration guide: https://doc.traefik.io/traefik/v3.0/migration/v2-to-v3/#tracing") + } + + return incompatible +} diff --git a/pkg/cli/deprecation_test.go b/pkg/cli/deprecation_test.go new file mode 100644 index 000000000..c225c0e7b --- /dev/null +++ b/pkg/cli/deprecation_test.go @@ -0,0 +1,404 @@ +package cli + +import ( + "testing" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/traefik/paerser/cli" + "github.com/traefik/traefik/v3/cmd" +) + +func ptr[T any](t T) *T { + return &t +} + +func TestDeprecationNotice(t *testing.T) { + tests := []struct { + desc string + config configuration + }{ + { + desc: "Docker provider swarmMode option is incompatible", + config: configuration{ + Providers: &providers{ + Docker: &docker{ + SwarmMode: ptr(true), + }, + }, + }, + }, + { + desc: "Docker provider tls.CAOptional option is incompatible", + config: configuration{ + Providers: &providers{ + Docker: &docker{ + TLS: &tls{ + CAOptional: ptr(true), + }, + }, + }, + }, + }, + { + desc: "Swarm provider tls.CAOptional option is incompatible", + config: configuration{ + Providers: &providers{ + Swarm: &swarm{ + TLS: &tls{ + CAOptional: ptr(true), + }, + }, + }, + }, + }, + { + desc: "Consul provider namespace option is incompatible", + config: configuration{ + Providers: &providers{ + Consul: &consul{ + Namespace: ptr("foobar"), + }, + }, + }, + }, + { + desc: "Consul provider tls.CAOptional option is incompatible", + config: configuration{ + Providers: &providers{ + Consul: &consul{ + TLS: &tls{ + CAOptional: ptr(true), + }, + }, + }, + }, + }, + { + desc: "ConsulCatalog provider namespace option is incompatible", + config: configuration{ + Providers: &providers{ + ConsulCatalog: &consulCatalog{ + Namespace: ptr("foobar"), + }, + }, + }, + }, + { + desc: "ConsulCatalog provider tls.CAOptional option is incompatible", + config: configuration{ + Providers: &providers{ + ConsulCatalog: &consulCatalog{ + Endpoint: &endpointConfig{ + TLS: &tls{ + CAOptional: ptr(true), + }, + }, + }, + }, + }, + }, + { + desc: "Nomad provider namespace option is incompatible", + config: configuration{ + Providers: &providers{ + Nomad: &nomad{ + Namespace: ptr("foobar"), + }, + }, + }, + }, + { + desc: "Nomad provider tls.CAOptional option is incompatible", + config: configuration{ + Providers: &providers{ + Nomad: &nomad{ + Endpoint: &endpointConfig{ + TLS: &tls{ + CAOptional: ptr(true), + }, + }, + }, + }, + }, + }, + { + desc: "Marathon configuration is incompatible", + config: configuration{ + Providers: &providers{ + Marathon: map[string]any{ + "foo": "bar", + }, + }, + }, + }, + { + desc: "Rancher configuration is incompatible", + config: configuration{ + Providers: &providers{ + Rancher: map[string]any{ + "foo": "bar", + }, + }, + }, + }, + { + desc: "ETCD provider tls.CAOptional option is incompatible", + config: configuration{ + Providers: &providers{ + ETCD: &etcd{ + TLS: &tls{ + CAOptional: ptr(true), + }, + }, + }, + }, + }, + { + desc: "Redis provider tls.CAOptional option is incompatible", + config: configuration{ + Providers: &providers{ + Redis: &redis{ + TLS: &tls{ + CAOptional: ptr(true), + }, + }, + }, + }, + }, + { + desc: "HTTP provider tls.CAOptional option is incompatible", + config: configuration{ + Providers: &providers{ + HTTP: &http{ + TLS: &tls{ + CAOptional: ptr(true), + }, + }, + }, + }, + }, + { + desc: "Pilot configuration is incompatible", + config: configuration{ + Pilot: map[string]any{ + "foo": "bar", + }, + }, + }, + { + desc: "Experimental HTTP3 enablement configuration is incompatible", + config: configuration{ + Experimental: &experimental{ + HTTP3: ptr(true), + }, + }, + }, + { + desc: "Tracing SpanNameLimit option is incompatible", + config: configuration{ + Tracing: &tracing{ + SpanNameLimit: ptr(42), + }, + }, + }, + { + desc: "Tracing Jaeger configuration is incompatible", + config: configuration{ + Tracing: &tracing{ + Jaeger: map[string]any{ + "foo": "bar", + }, + }, + }, + }, + { + desc: "Tracing Zipkin configuration is incompatible", + config: configuration{ + Tracing: &tracing{ + Zipkin: map[string]any{ + "foo": "bar", + }, + }, + }, + }, + { + desc: "Tracing Datadog configuration is incompatible", + config: configuration{ + Tracing: &tracing{ + Datadog: map[string]any{ + "foo": "bar", + }, + }, + }, + }, + { + desc: "Tracing Instana configuration is incompatible", + config: configuration{ + Tracing: &tracing{ + Instana: map[string]any{ + "foo": "bar", + }, + }, + }, + }, + { + desc: "Tracing Haystack configuration is incompatible", + config: configuration{ + Tracing: &tracing{ + Haystack: map[string]any{ + "foo": "bar", + }, + }, + }, + }, + { + desc: "Tracing Elastic configuration is incompatible", + config: configuration{ + Tracing: &tracing{ + Elastic: map[string]any{ + "foo": "bar", + }, + }, + }, + }, + } + + for _, test := range tests { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + var gotLog bool + var gotLevel zerolog.Level + testHook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, message string) { + gotLog = true + gotLevel = level + }) + + logger := log.With().Logger().Hook(testHook) + assert.True(t, test.config.deprecationNotice(logger)) + assert.True(t, gotLog) + assert.Equal(t, zerolog.ErrorLevel, gotLevel) + }) + } +} + +func TestLoad(t *testing.T) { + testCases := []struct { + desc string + args []string + env map[string]string + wantDeprecated bool + }{ + { + desc: "Empty", + args: []string{}, + wantDeprecated: false, + }, + { + desc: "[FLAG] providers.marathon is deprecated", + args: []string{ + "--access-log", + "--log.level=DEBUG", + "--entrypoints.test.http.tls", + "--providers.nomad.endpoint.tls.insecureskipverify=true", + "--providers.marathon", + }, + wantDeprecated: true, + }, + { + desc: "[FLAG] multiple deprecated", + args: []string{ + "--access-log", + "--log.level=DEBUG", + "--entrypoints.test.http.tls", + "--providers.marathon", + "--pilot.token=XXX", + }, + wantDeprecated: true, + }, + { + desc: "[FLAG] no deprecated", + args: []string{ + "--access-log", + "--log.level=DEBUG", + "--entrypoints.test.http.tls", + "--providers.docker", + }, + wantDeprecated: false, + }, + { + desc: "[ENV] providers.marathon is deprecated", + env: map[string]string{ + "TRAEFIK_ACCESS_LOG": "", + "TRAEFIK_LOG_LEVEL": "DEBUG", + "TRAEFIK_ENTRYPOINT_TEST_HTTP_TLS": "true", + "TRAEFIK_PROVIDERS_MARATHON": "true", + }, + wantDeprecated: true, + }, + { + desc: "[ENV] multiple deprecated", + env: map[string]string{ + "TRAEFIK_ACCESS_LOG": "true", + "TRAEFIK_LOG_LEVEL": "DEBUG", + "TRAEFIK_ENTRYPOINT_TEST_HTTP_TLS": "true", + "TRAEFIK_PROVIDERS_MARATHON": "true", + "TRAEFIK_PILOT_TOKEN": "xxx", + }, + wantDeprecated: true, + }, + { + desc: "[ENV] no deprecated", + env: map[string]string{ + "TRAEFIK_ACCESS_LOG": "true", + "TRAEFIK_LOG_LEVEL": "DEBUG", + "TRAEFIK_ENTRYPOINT_TEST_HTTP_TLS": "true", + }, + + wantDeprecated: false, + }, + { + desc: "[FILE] providers.marathon is deprecated", + args: []string{ + "--configfile=./fixtures/traefik_deprecated.toml", + }, + wantDeprecated: true, + }, + { + desc: "[FILE] multiple deprecated", + args: []string{ + "--configfile=./fixtures/traefik_multiple_deprecated.toml", + }, + wantDeprecated: true, + }, + { + desc: "[FILE] no deprecated", + args: []string{ + "--configfile=./fixtures/traefik_no_deprecated.toml", + }, + wantDeprecated: false, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + tconfig := cmd.NewTraefikConfiguration() + c := &cli.Command{Configuration: tconfig} + l := DeprecationLoader{} + + for name, val := range test.env { + t.Setenv(name, val) + } + deprecated, err := l.Load(test.args, c) + assert.Equal(t, test.wantDeprecated, deprecated) + if !test.wantDeprecated { + require.NoError(t, err) + } + }) + } +} diff --git a/pkg/cli/fixtures/traefik_deprecated.toml b/pkg/cli/fixtures/traefik_deprecated.toml new file mode 100644 index 000000000..21fa1d1c1 --- /dev/null +++ b/pkg/cli/fixtures/traefik_deprecated.toml @@ -0,0 +1,5 @@ +[accesslog] + +[entrypoints.test.http.tls] + +[providers.marathon] diff --git a/pkg/cli/fixtures/traefik_multiple_deprecated.toml b/pkg/cli/fixtures/traefik_multiple_deprecated.toml new file mode 100644 index 000000000..0847e9da3 --- /dev/null +++ b/pkg/cli/fixtures/traefik_multiple_deprecated.toml @@ -0,0 +1,8 @@ +[accesslog] + +[entrypoints.test.http.tls] + +[providers.marathon] + +[pilot] + token="xxx" diff --git a/pkg/cli/fixtures/traefik_no_deprecated.toml b/pkg/cli/fixtures/traefik_no_deprecated.toml new file mode 100644 index 000000000..282d2f873 --- /dev/null +++ b/pkg/cli/fixtures/traefik_no_deprecated.toml @@ -0,0 +1,3 @@ +[accesslog] + +[entrypoints.test.http.tls]