Repository: containous/traefik
Pull request #6875 (2020-06-02 15:33:25)

feat: use parser to load dynamic config from file.
### What does this PR do?

Uses parser to load dynamic config from file.

### Motivation

Be able to use `SetDefault` method.

### More

- [x] Added/updated tests
- ~[ ] Added/updated documentation~



Patch:
diff --git a/docs/content/migration/v2.md b/docs/content/migration/v2.md
index bc500eef6..5239c4a87 100644
--- a/docs/content/migration/v2.md
+++ b/docs/content/migration/v2.md
@@ -302,3 +302,9 @@ providers:
 --entryPoints.websecure.address=:443
 --providers.kubernetesIngress=true
 ```
+
+## v2.2 to v2.3
+
+### File Provider
+
+The file parser has been changed, since v2.3 the unknown options/fields in a dynamic configuration file are treated as errors.
diff --git a/docs/content/providers/file.md b/docs/content/providers/file.md
index dc6a0b0c6..cf13f53b1 100644
--- a/docs/content/providers/file.md
+++ b/docs/content/providers/file.md
@@ -238,7 +238,7 @@ Thus, it's possible to define easily lot of routers, services and TLS certificat
     [[tls.certificates]]
       certFile = "/etc/traefik/cert-{{ $e }}.pem"
       keyFile = "/etc/traefik/cert-{{ $e }}.key"
-      store = ["my-store-foo-{{ $e }}", "my-store-bar-{{ $e }}"]
+      stores = ["my-store-foo-{{ $e }}", "my-store-bar-{{ $e }}"]
     {{ end }}
     
     [tls.config]
diff --git a/integration/fixtures/acme/certificates.toml b/integration/fixtures/acme/certificates.toml
index ba6414025..11c5a83ef 100644
--- a/integration/fixtures/acme/certificates.toml
+++ b/integration/fixtures/acme/certificates.toml
@@ -11,6 +11,6 @@
     [http.routers.test.tls]
 
 [[tls.certificates]]
-  store = ["default"]
+  stores = ["default"]
   certFile = "fixtures/acme/ssl/wildcard.crt"
   keyFile = "fixtures/acme/ssl/wildcard.key"
diff --git a/integration/fixtures/tcp/mixed.toml b/integration/fixtures/tcp/mixed.toml
index ef25965ec..38d852e7e 100644
--- a/integration/fixtures/tcp/mixed.toml
+++ b/integration/fixtures/tcp/mixed.toml
@@ -22,7 +22,7 @@
     [http.routers.my-router]
      rule = "Path(`/test`)"
      service = "whoami"
-     entrypoint=["tcp"]
+     entrypoints = ["tcp"]
      
     [http.routers.my-https-router]
       entryPoints=["tcp"]
@@ -41,14 +41,14 @@
       service = "whoami-a"
       entryPoints = [ "tcp" ]
       [tcp.routers.to-whoami-a.tls]
-        passthrough=true
+        passthrough = true
 
     [tcp.routers.to-whoami-b]
       rule = "HostSNI(`whoami-b.test`)"
       service = "whoami-b"
       entryPoints = [ "tcp" ]
       [tcp.routers.to-whoami-b.tls]
-        passthrough=true
+        passthrough = true
 
     [tcp.routers.to-whoami-no-cert]
       rule = "HostSNI(`whoami-c.test`)"
diff --git a/integration/fixtures/timeout/keepalive.toml b/integration/fixtures/timeout/keepalive.toml
index f3d7c3df7..a2b9637ef 100644
--- a/integration/fixtures/timeout/keepalive.toml
+++ b/integration/fixtures/timeout/keepalive.toml
@@ -31,4 +31,3 @@
       passHostHeader = true
       [[http.services.keepalive.loadBalancer.servers]]
         url = "{{ .KeepAliveServer }}"
-        weight = 1
diff --git a/integration/fixtures/tracing/simple-jaeger-collector.toml b/integration/fixtures/tracing/simple-jaeger-collector.toml
index d10d5b914..781a828e1 100644
--- a/integration/fixtures/tracing/simple-jaeger-collector.toml
+++ b/integration/fixtures/tracing/simple-jaeger-collector.toml
@@ -58,13 +58,13 @@
         url = "http://{{.WhoAmiIP}}:{{.WhoAmiPort}}"
 
   [http.services.service2]
-    passHostHeader = true
     [http.services.service2.loadBalancer]
+      passHostHeader = true
       [[http.services.service2.loadBalancer.servers]]
         url = "http://{{.WhoAmiIP}}:{{.WhoAmiPort}}"
 
   [http.services.service3]
-    passHostHeader = true
     [http.services.service3.loadBalancer]
+      passHostHeader = true
       [[http.services.service3.loadBalancer.servers]]
         url = "http://{{.WhoAmiIP}}:{{.WhoAmiPort}}"
diff --git a/integration/fixtures/tracing/simple-jaeger.toml b/integration/fixtures/tracing/simple-jaeger.toml
index 056d15b4b..99269c009 100644
--- a/integration/fixtures/tracing/simple-jaeger.toml
+++ b/integration/fixtures/tracing/simple-jaeger.toml
@@ -57,13 +57,13 @@
         url = "http://{{.WhoAmiIP}}:{{.WhoAmiPort}}"
 
   [http.services.service2]
-    passHostHeader = true
     [http.services.service2.loadBalancer]
+      passHostHeader = true
       [[http.services.service2.loadBalancer.servers]]
         url = "http://{{.WhoAmiIP}}:{{.WhoAmiPort}}"
 
   [http.services.service3]
-    passHostHeader = true
     [http.services.service3.loadBalancer]
+      passHostHeader = true
       [[http.services.service3.loadBalancer.servers]]
         url = "http://{{.WhoAmiIP}}:{{.WhoAmiPort}}"
diff --git a/integration/fixtures/tracing/simple-zipkin.toml b/integration/fixtures/tracing/simple-zipkin.toml
index 43842b70e..b5cfaa355 100644
--- a/integration/fixtures/tracing/simple-zipkin.toml
+++ b/integration/fixtures/tracing/simple-zipkin.toml
@@ -53,13 +53,13 @@
         url = "http://{{.WhoAmiIP}}:{{.WhoAmiPort}}"
 
   [http.services.service2]
-    passHostHeader = true
     [http.services.service2.loadBalancer]
+      passHostHeader = true
       [[http.services.service2.loadBalancer.servers]]
         url = "http://{{.WhoAmiIP}}:{{.WhoAmiPort}}"
 
   [http.services.service3]
-    passHostHeader = true
     [http.services.service3.loadBalancer]
+      passHostHeader = true
       [[http.services.service3.loadBalancer.servers]]
         url = "http://{{.WhoAmiIP}}:{{.WhoAmiPort}}"
diff --git a/pkg/config/dynamic/http_config.go b/pkg/config/dynamic/http_config.go
index 27c5e44f4..ad99ba8e8 100644
--- a/pkg/config/dynamic/http_config.go
+++ b/pkg/config/dynamic/http_config.go
@@ -21,7 +21,7 @@ type HTTPConfiguration struct {
 // Model is a set of default router's values.
 type Model struct {
 	Middlewares []string         `json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty"`
-	TLS         *RouterTLSConfig `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty"`
+	TLS         *RouterTLSConfig `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty"`
 }
 
 // +k8s:deepcopy-gen=true
@@ -42,7 +42,7 @@ type Router struct {
 	Service     string           `json:"service,omitempty" toml:"service,omitempty" yaml:"service,omitempty"`
 	Rule        string           `json:"rule,omitempty" toml:"rule,omitempty" yaml:"rule,omitempty"`
 	Priority    int              `json:"priority,omitempty" toml:"priority,omitempty,omitzero" yaml:"priority,omitempty"`
-	TLS         *RouterTLSConfig `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty"`
+	TLS         *RouterTLSConfig `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty"`
 }
 
 // +k8s:deepcopy-gen=true
@@ -103,7 +103,7 @@ func (w *WRRService) SetDefaults() {
 
 // Sticky holds the sticky configuration.
 type Sticky struct {
-	Cookie *Cookie `json:"cookie,omitempty" toml:"cookie,omitempty" yaml:"cookie,omitempty" label:"allowEmpty"`
+	Cookie *Cookie `json:"cookie,omitempty" toml:"cookie,omitempty" yaml:"cookie,omitempty" label:"allowEmpty" file:"allowEmpty"`
 }
 
 // +k8s:deepcopy-gen=true
@@ -120,7 +120,7 @@ type Cookie struct {
 
 // ServersLoadBalancer holds the ServersLoadBalancer configuration.
 type ServersLoadBalancer struct {
-	Sticky             *Sticky             `json:"sticky,omitempty" toml:"sticky,omitempty" yaml:"sticky,omitempty" label:"allowEmpty"`
+	Sticky             *Sticky             `json:"sticky,omitempty" toml:"sticky,omitempty" yaml:"sticky,omitempty" label:"allowEmpty" file:"allowEmpty"`
 	Servers            []Server            `json:"servers,omitempty" toml:"servers,omitempty" yaml:"servers,omitempty" label-slice-as-struct:"server"`
 	HealthCheck        *HealthCheck        `json:"healthCheck,omitempty" toml:"healthCheck,omitempty" yaml:"healthCheck,omitempty"`
 	PassHostHeader     *bool               `json:"passHostHeader" toml:"passHostHeader" yaml:"passHostHeader"`
@@ -162,8 +162,8 @@ type ResponseForwarding struct {
 // Server holds the server configuration.
 type Server struct {
 	URL    string `json:"url,omitempty" toml:"url,omitempty" yaml:"url,omitempty" label:"-"`
-	Scheme string `toml:"-" json:"-" yaml:"-"`
-	Port   string `toml:"-" json:"-" yaml:"-"`
+	Scheme string `toml:"-" json:"-" yaml:"-" file:"-"`
+	Port   string `toml:"-" json:"-" yaml:"-" file:"-"`
 }
 
 // SetDefaults Default values for a Server.
diff --git a/pkg/config/dynamic/middlewares.go b/pkg/config/dynamic/middlewares.go
index 8727b818b..acdb8f3a9 100644
--- a/pkg/config/dynamic/middlewares.go
+++ b/pkg/config/dynamic/middlewares.go
@@ -34,7 +34,7 @@ type Middleware struct {
 	InFlightReq       *InFlightReq       `json:"inFlightReq,omitempty" toml:"inFlightReq,omitempty" yaml:"inFlightReq,omitempty"`
 	Buffering         *Buffering         `json:"buffering,omitempty" toml:"buffering,omitempty" yaml:"buffering,omitempty"`
 	CircuitBreaker    *CircuitBreaker    `json:"circuitBreaker,omitempty" toml:"circuitBreaker,omitempty" yaml:"circuitBreaker,omitempty"`
-	Compress          *Compress          `json:"compress,omitempty" toml:"compress,omitempty" yaml:"compress,omitempty" label:"allowEmpty"`
+	Compress          *Compress          `json:"compress,omitempty" toml:"compress,omitempty" yaml:"compress,omitempty" label:"allowEmpty" file:"allowEmpty"`
 	PassTLSClientCert *PassTLSClientCert `json:"passTLSClientCert,omitempty" toml:"passTLSClientCert,omitempty" yaml:"passTLSClientCert,omitempty"`
 	Retry             *Retry             `json:"retry,omitempty" toml:"retry,omitempty" yaml:"retry,omitempty"`
 	ContentType       *ContentType       `json:"contentType,omitempty" toml:"contentType,omitempty" yaml:"contentType,omitempty"`
@@ -275,7 +275,7 @@ func (s *IPStrategy) Get() (ip.Strategy, error) {
 // IPWhiteList holds the ip white list configuration.
 type IPWhiteList struct {
 	SourceRange []string    `json:"sourceRange,omitempty" toml:"sourceRange,omitempty" yaml:"sourceRange,omitempty"`
-	IPStrategy  *IPStrategy `json:"ipStrategy,omitempty" toml:"ipStrategy,omitempty" yaml:"ipStrategy,omitempty"  label:"allowEmpty"`
+	IPStrategy  *IPStrategy `json:"ipStrategy,omitempty" toml:"ipStrategy,omitempty" yaml:"ipStrategy,omitempty"  label:"allowEmpty" file:"allowEmpty"`
 }
 
 // +k8s:deepcopy-gen=true
diff --git a/pkg/config/dynamic/tcp_config.go b/pkg/config/dynamic/tcp_config.go
index 158f3e113..88592f5fb 100644
--- a/pkg/config/dynamic/tcp_config.go
+++ b/pkg/config/dynamic/tcp_config.go
@@ -50,7 +50,7 @@ type TCPRouter struct {
 	EntryPoints []string            `json:"entryPoints,omitempty" toml:"entryPoints,omitempty" yaml:"entryPoints,omitempty"`
 	Service     string              `json:"service,omitempty" toml:"service,omitempty" yaml:"service,omitempty"`
 	Rule        string              `json:"rule,omitempty" toml:"rule,omitempty" yaml:"rule,omitempty"`
-	TLS         *RouterTCPTLSConfig `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty"`
+	TLS         *RouterTCPTLSConfig `json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty"`
 }
 
 // +k8s:deepcopy-gen=true
diff --git a/pkg/config/dynamic/udp_config.go b/pkg/config/dynamic/udp_config.go
index c890445a5..f94aa1095 100644
--- a/pkg/config/dynamic/udp_config.go
+++ b/pkg/config/dynamic/udp_config.go
@@ -78,5 +78,5 @@ func (l *UDPServersLoadBalancer) Mergeable(loadBalancer *UDPServersLoadBalancer)
 // UDPServer defines a UDP server configuration.
 type UDPServer struct {
 	Address string `json:"address,omitempty" toml:"address,omitempty" yaml:"address,omitempty" label:"-"`
-	Port    string `toml:"-" json:"-" yaml:"-"`
+	Port    string `toml:"-" json:"-" yaml:"-" file:"-"`
 }
diff --git a/pkg/config/file/file.go b/pkg/config/file/file.go
index 1bd48da8f..f55ec2fe3 100644
--- a/pkg/config/file/file.go
+++ b/pkg/config/file/file.go
@@ -2,7 +2,11 @@
 package file
 
 import (
+	"fmt"
+
+	"github.com/BurntSushi/toml"
 	"github.com/containous/traefik/v2/pkg/config/parser"
+	"gopkg.in/yaml.v2"
 )
 
 // Decode decodes the given configuration file into the given element.
@@ -22,11 +26,57 @@ func Decode(filePath string, element interface{}) error {
 		return err
 	}
 
-	metaOpts := parser.MetadataOpts{TagName: parser.TagLabel, AllowSliceAsStruct: true}
+	metaOpts := parser.MetadataOpts{TagName: parser.TagFile, AllowSliceAsStruct: false}
 	err = parser.AddMetadata(element, root, metaOpts)
 	if err != nil {
 		return err
 	}
 
-	return parser.Fill(element, root, parser.FillerOpts{AllowSliceAsStruct: true})
+	return parser.Fill(element, root, parser.FillerOpts{AllowSliceAsStruct: false})
+}
+
+// DecodeContent decodes the given configuration file content into the given element.
+// The operation goes through three stages roughly summarized as:
+// file contents -> tree of untyped nodes
+// untyped nodes -> nodes augmented with metadata such as kind (inferred from element)
+// "typed" nodes -> typed element.
+func DecodeContent(content string, extension string, element interface{}) error {
+	data := make(map[string]interface{})
+
+	switch extension {
+	case ".toml":
+		_, err := toml.Decode(content, &data)
+		if err != nil {
+			return err
+		}
+
+	case ".yml", ".yaml":
+		var err error
+		err = yaml.Unmarshal([]byte(content), &data)
+		if err != nil {
+			return err
+		}
+
+	default:
+		return fmt.Errorf("unsupported file extension: %s", extension)
+	}
+
+	filters := getRootFieldNames(element)
+
+	node, err := decodeRawToNode(data, parser.DefaultRootName, filters...)
+	if err != nil {
+		return err
+	}
+
+	if len(node.Children) == 0 {
+		return nil
+	}
+
+	metaOpts := parser.MetadataOpts{TagName: parser.TagFile, AllowSliceAsStruct: false}
+	err = parser.AddMetadata(element, node, metaOpts)
+	if err != nil {
+		return err
+	}
+
+	return parser.Fill(element, node, parser.FillerOpts{AllowSliceAsStruct: false})
 }
diff --git a/pkg/config/parser/tags.go b/pkg/config/parser/tags.go
index a4c479aea..3304c45c4 100644
--- a/pkg/config/parser/tags.go
+++ b/pkg/config/parser/tags.go
@@ -6,6 +6,11 @@ const (
 	// - "-": ignore the field.
 	TagLabel = "label"
 
+	// TagFile allows to apply a custom behavior.
+	// - "allowEmpty": allows to create an empty struct.
+	// - "-": ignore the field.
+	TagFile = "file"
+
 	// TagLabelSliceAsStruct allows to use a slice of struct by creating one entry into the slice.
 	// The value is the substitution name used in the label to access the slice.
 	TagLabelSliceAsStruct = "label-slice-as-struct"
diff --git a/pkg/config/static/entrypoints.go b/pkg/config/static/entrypoints.go
index 1c0e030ca..88c4ba8df 100644
--- a/pkg/config/static/entrypoints.go
+++ b/pkg/config/static/entrypoints.go
@@ -12,7 +12,7 @@ import (
 type EntryPoint struct {
 	Address          string                `description:"Entry point address." json:"address,omitempty" toml:"address,omitempty" yaml:"address,omitempty"`
 	Transport        *EntryPointsTransport `description:"Configures communication between clients and Traefik." json:"transport,omitempty" toml:"transport,omitempty" yaml:"transport,omitempty"`
-	ProxyProtocol    *ProxyProtocol        `description:"Proxy-Protocol configuration." json:"proxyProtocol,omitempty" toml:"proxyProtocol,omitempty" yaml:"proxyProtocol,omitempty" label:"allowEmpty"`
+	ProxyProtocol    *ProxyProtocol        `description:"Proxy-Protocol configuration." json:"proxyProtocol,omitempty" toml:"proxyProtocol,omitempty" yaml:"proxyProtocol,omitempty" label:"allowEmpty" file:"allowEmpty"`
 	ForwardedHeaders *ForwardedHeaders     `description:"Trust client forwarding headers." json:"forwardedHeaders,omitempty" toml:"forwardedHeaders,omitempty" yaml:"forwardedHeaders,omitempty"`
 	HTTP             HTTPConfig            `description:"HTTP configuration." json:"http,omitempty" toml:"http,omitempty" yaml:"http,omitempty"`
 }
@@ -51,7 +51,7 @@ func (ep *EntryPoint) SetDefaults() {
 type HTTPConfig struct {
 	Redirections *Redirections `description:"Set of redirection" json:"redirections,omitempty" toml:"redirections,omitempty" yaml:"redirections,omitempty"`
 	Middlewares  []string      `description:"Default middlewares for the routers linked to the entry point." json:"middlewares,omitempty" toml:"middlewares,omitempty" yaml:"middlewares,omitempty"`
-	TLS          *TLSConfig    `description:"Default TLS configuration for the routers linked to the entry point." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty"`
+	TLS          *TLSConfig    `description:"Default TLS configuration for the routers linked to the entry point." json:"tls,omitempty" toml:"tls,omitempty" yaml:"tls,omitempty" label:"allowEmpty" file:"allowEmpty"`
 }
 
 // Redirections is a set of redirection for an entry point.
diff --git a/pkg/config/static/static_config.go b/pkg/config/static/static_config.go
index 1fac9e019..91f5e68c6 100644
--- a/pkg/config/static/static_config.go
+++ b/pkg/config/static/static_config.go
@@ -57,15 +57,15 @@ type Configuration struct {
 	EntryPoints      EntryPoints       `description:"Entry points definition." json:"entryPoints,omitempty" toml:"entryPoints,omitempty" yaml:"entryPoints,omitempty" export:"true"`
 	Providers        *Providers        `description:"Providers configuration." json:"providers,omitempty" toml:"providers,omitempty" yaml:"providers,omitempty" export:"true"`
 
-	API     *API           `description:"Enable api/dashboard." json:"api,omitempty" toml:"api,omitempty" yaml:"api,omitempty" label:"allowEmpty" export:"true"`
+	API     *API           `description:"Enable api/dashboard." json:"api,omitempty" toml:"api,omitempty" yaml:"api,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
 	Metrics *types.Metrics `description:"Enable a metrics exporter." json:"metrics,omitempty" toml:"metrics,omitempty" yaml:"metrics,omitempty" export:"true"`
-	Ping    *ping.Handler  `description:"Enable ping." json:"ping,omitempty" toml:"ping,omitempty" yaml:"ping,omitempty" label:"allowEmpty" export:"true"`
+	Ping    *ping.Handler  `description:"Enable ping." json:"ping,omitempty" toml:"ping,omitempty" yaml:"ping,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
 
-	Log       *types.TraefikLog `description:"Traefik log settings." json:"log,omitempty" toml:"log,omitempty" yaml:"log,omitempty" label:"allowEmpty" export:"true"`
-	AccessLog *types.AccessLog  `description:"Access log settings." json:"accessLog,omitempty" toml:"accessLog,omitempty" yaml:"accessLog,omitempty" label:"allowEmpty" export:"true"`
-	Tracing   *Tracing          `description:"OpenTracing configuration." json:"tracing,omitempty" toml:"tracing,omitempty" yaml:"tracing,omitempty" label:"allowEmpty" export:"true"`
+	Log       *types.TraefikLog `description:"Traefik log settings." json:"log,omitempty" toml:"log,omitempty" yaml:"log,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
+	AccessLog *types.AccessLog  `description:"Access log settings." json:"accessLog,omitempty" toml:"accessLog,omitempty" yaml:"accessLog,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
+	Tracing   *Tracing          `description:"OpenTracing configuration." json:"tracing,omitempty" toml:"tracing,omitempty" yaml:"tracing,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
 
-	HostResolver *types.HostResolverConfig `description:"Enable CNAME Flattening." json:"hostResolver,omitempty" toml:"hostResolver,omitempty" yaml:"hostResolver,omitempty" label:"allowEmpty" export:"true"`
+	HostResolver *types.HostResolverConfig `description:"Enable CNAME Flattening." json:"hostResolver,omitempty" toml:"hostResolver,omitempty" yaml:"hostResolver,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
 
 	CertificatesResolvers map[string]CertificateResolver `description:"Certificates resolvers configuration." json:"certificatesResolvers,omitempty" toml:"certificatesResolvers,omitempty" yaml:"certificatesResolvers,omitempty" export:"true"`
 }
@@ -77,8 +77,8 @@ type CertificateResolver struct {
 
 // Global holds the global configuration.
 type Global struct {
-	CheckNewVersion    bool `description:"Periodically check if a new version has been released." json:"checkNewVersion,omitempty" toml:"checkNewVersion,omitempty" yaml:"checkNewVersion,omitempty" label:"allowEmpty" export:"true"`
-	SendAnonymousUsage bool `description:"Periodically send anonymous usage statistics. If the option is not specified, it will be enabled by default." json:"sendAnonymousUsage,omitempty" toml:"sendAnonymousUsage,omitempty" yaml:"sendAnonymousUsage,omitempty" label:"allowEmpty" export:"true"`
+	CheckNewVersion    bool `description:"Periodically check if a new version has been released." json:"checkNewVersion,omitempty" toml:"checkNewVersion,omitempty" yaml:"checkNewVersion,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
+	SendAnonymousUsage bool `description:"Periodically send anonymous usage statistics. If the option is not specified, it will be enabled by default." json:"sendAnonymousUsage,omitempty" toml:"sendAnonymousUsage,omitempty" yaml:"sendAnonymousUsage,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
 }
 
 // ServersTransport options to configure communication between Traefik and the servers.
@@ -95,8 +95,8 @@ type API struct {
 	Dashboard bool `description:"Activate dashboard." json:"dashboard,omitempty" toml:"dashboard,omitempty" yaml:"dashboard,omitempty" export:"true"`
 	Debug     bool `description:"Enable additional endpoints for debugging and profiling." json:"debug,omitempty" toml:"debug,omitempty" yaml:"debug,omitempty" export:"true"`
 	// TODO: Re-enable statistics
-	// Statistics      *types.Statistics `description:"Enable more detailed statistics." json:"statistics,omitempty" toml:"statistics,omitempty" yaml:"statistics,omitempty" export:"true" label:"allowEmpty"`
-	DashboardAssets *assetfs.AssetFS `json:"-" toml:"-" yaml:"-" label:"-"`
+	// Statistics      *types.Statistics `description:"Enable more detailed statistics." json:"statistics,omitempty" toml:"statistics,omitempty" yaml:"statistics,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"`
+	DashboardAssets *assetfs.AssetFS `json:"-" toml:"-" yaml:"-" label:"-" file:"-"`
 }
 
 // SetDefaults sets the default values.
@@ -144,12 +144,12 @@ func (a *LifeCycle) SetDefaults() {
 type Tracing struct {
 	ServiceName   string           `description:"Set the name for this service." json:"serviceName,omitempty" toml:"serviceName,omitempty" yaml:"serviceName,omitempty" export:"true"`
 	SpanNameLimit int              `description:"Set the maximum character limit for Span names (default 0 = no limit)." json:"spanNameLimit,omitempty" toml:"spanNameLimit,omitempty" yaml:"spanNameLimit,omitempty" export:"true"`
-	Jaeger        *jaeger.Config   `description:"Settings for Jaeger." json:"jaeger,omitempty" toml:"jaeger,omitempty" yaml:"jaeger,omitempty" export:"true" label:"allowEmpty"`
-	Zipkin        *zipkin.Config   `description:"Settings for Zipkin." json:"zipkin,omitempty" toml:"zipkin,omitempty" yaml:"zipkin,omitempty" export:"true" label:"allowEmpty"`
-	Datadog       *datadog.Config  `description:"Settings for Datadog." json:"datadog,omitempty" toml:"datadog,omitempty" yaml:"datadog,omitempty" export:"true" label:"allowEmpty"`
-	Instana       *instana.Config  `description:"Settings for Instana." json:"instana,omitempty" toml:"instana,omitempty" yaml:"instana,omitempty" export:"true" label:"allowEmpty"`
-	Haystack      *haystack.Config `description:"Settings for Haystack." json:"haystack,omitempty" toml:"haystack,omitempty" yaml:"haystack,omitempty" export:"true" label:"allowEmpty"`
-	Elastic       *elastic.Config  `description:"Settings for Elastic." json:"elastic,omitempty" toml:"elastic,omitempty" yaml:"elastic,omitempty" export:"true" label:"allowEmpty"`
+	Jaeger        *jaeger.Config   `description:"Settings for Jaeger." json:"jaeger,omitempty" toml:"jaeger,omitempty" yaml:"jaeger,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"`
+	Zipkin        *zipkin.Config   `description:"Settings for Zipkin." json:"zipkin,omitempty" toml:"zipkin,omitempty" yaml:"zipkin,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"`
+	Datadog       *datadog.Config  `description:"Settings for Datadog." json:"datadog,omitempty" toml:"datadog,omitempty" yaml:"datadog,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"`
+	Instana       *instana.Config  `description:"Settings for Instana." json:"instana,omitempty" toml:"instana,omitempty" yaml:"instana,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"`
+	Haystack      *haystack.Config `description:"Settings for Haystack." json:"haystack,omitempty" toml:"haystack,omitempty" yaml:"haystack,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"`
+	Elastic       *elastic.Config  `description:"Settings for Elastic." json:"elastic,omitempty" toml:"elastic,omitempty" yaml:"elastic,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"`
 }
 
 // SetDefaults sets the default values.
@@ -162,19 +162,19 @@ func (t *Tracing) SetDefaults() {
 type Providers struct {
 	ProvidersThrottleDuration types.Duration `description:"Backends throttle duration: minimum duration between 2 events from providers before applying a new configuration. It avoids unnecessary reloads if multiples events are sent in a short amount of time." json:"providersThrottleDuration,omitempty" toml:"providersThrottleDuration,omitempty" yaml:"providersThrottleDuration,omitempty" export:"true"`
 
-	Docker            *docker.Provider        `description:"Enable Docker backend with default settings." json:"docker,omitempty" toml:"docker,omitempty" yaml:"docker,omitempty" export:"true" label:"allowEmpty"`
+	Docker            *docker.Provider        `description:"Enable Docker backend with default settings." json:"docker,omitempty" toml:"docker,omitempty" yaml:"docker,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"`
 	File              *file.Provider          `description:"Enable File backend with default settings." json:"file,omitempty" toml:"file,omitempty" yaml:"file,omitempty" export:"true"`
-	Marathon          *marathon.Provider      `description:"Enable Marathon backend with default settings." json:"marathon,omitempty" toml:"marathon,omitempty" yaml:"marathon,omitempty" export:"true" label:"allowEmpty"`
-	KubernetesIngress *ingress.Provider       `description:"Enable Kubernetes backend with default settings." json:"kubernetesIngress,omitempty" toml:"kubernetesIngress,omitempty" yaml:"kubernetesIngress,omitempty" export:"true" label:"allowEmpty"`
-	KubernetesCRD     *crd.Provider           `description:"Enable Kubernetes backend with default settings." json:"kubernetesCRD,omitempty" toml:"kubernetesCRD,omitempty" yaml:"kubernetesCRD,omitempty" export:"true" label:"allowEmpty"`
-	Rest              *rest.Provider          `description:"Enable Rest backend with default settings." json:"rest,omitempty" toml:"rest,omitempty" yaml:"rest,omitempty" export:"true" label:"allowEmpty"`
-	Rancher           *rancher.Provider       `description:"Enable Rancher backend with default settings." json:"rancher,omitempty" toml:"rancher,omitempty" yaml:"rancher,omitempty" export:"true" label:"allowEmpty"`
+	Marathon          *marathon.Provider      `description:"Enable Marathon backend with default settings." json:"marathon,omitempty" toml:"marathon,omitempty" yaml:"marathon,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"`
+	KubernetesIngress *ingress.Provider       `description:"Enable Kubernetes backend with default settings." json:"kubernetesIngress,omitempty" toml:"kubernetesIngress,omitempty" yaml:"kubernetesIngress,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"`
+	KubernetesCRD     *crd.Provider           `description:"Enable Kubernetes backend with default settings." json:"kubernetesCRD,omitempty" toml:"kubernetesCRD,omitempty" yaml:"kubernetesCRD,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"`
+	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.Provider `description:"Enable ConsulCatalog backend with default settings." json:"consulCatalog,omitempty" toml:"consulCatalog,omitempty" yaml:"consulCatalog,omitempty"`
 
-	Consul    *consul.Provider `description:"Enable Consul backend with default settings." json:"consul,omitempty" toml:"consul,omitempty" yaml:"consul,omitempty" export:"true" label:"allowEmpty"`
-	Etcd      *etcd.Provider   `description:"Enable Etcd backend with default settings." json:"etcd,omitempty" toml:"etcd,omitempty" yaml:"etcd,omitempty" export:"true" label:"allowEmpty"`
-	ZooKeeper *zk.Provider     `description:"Enable ZooKeeper backend with default settings." json:"zooKeeper,omitempty" toml:"zooKeeper,omitempty" yaml:"zooKeeper,omitempty" export:"true" label:"allowEmpty"`
-	Redis     *redis.Provider  `description:"Enable Redis backend with default settings." json:"redis,omitempty" toml:"redis,omitempty" yaml:"redis,omitempty" export:"true" label:"allowEmpty"`
+	Consul    *consul.Provider `description:"Enable Consul backend with default settings." json:"consul,omitempty" toml:"consul,omitempty" yaml:"consul,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"`
+	Etcd      *etcd.Provider   `description:"Enable Etcd backend with default settings." json:"etcd,omitempty" toml:"etcd,omitempty" yaml:"etcd,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"`
+	ZooKeeper *zk.Provider     `description:"Enable ZooKeeper backend with default settings." json:"zooKeeper,omitempty" toml:"zooKeeper,omitempty" yaml:"zooKeeper,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"`
+	Redis     *redis.Provider  `description:"Enable Redis backend with default settings." json:"redis,omitempty" toml:"redis,omitempty" yaml:"redis,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"`
 }
 
 // SetEffectiveConfiguration adds missing configuration parameters derived from existing ones.
diff --git a/pkg/provider/acme/provider.go b/pkg/provider/acme/provider.go
index 0f9ef03de..14309375f 100644
--- a/pkg/provider/acme/provider.go
+++ b/pkg/provider/acme/provider.go
@@ -38,9 +38,9 @@ type Configuration struct {
 	CAServer      string         `description:"CA server to use." json:"caServer,omitempty" toml:"caServer,omitempty" yaml:"caServer,omitempty"`
 	Storage       string         `description:"Storage to use." json:"storage,omitempty" toml:"storage,omitempty" yaml:"storage,omitempty"`
 	KeyType       string         `description:"KeyType used for generating certificate private key. Allow value 'EC256', 'EC384', 'RSA2048', 'RSA4096', 'RSA8192'." json:"keyType,omitempty" toml:"keyType,omitempty" yaml:"keyType,omitempty"`
-	DNSChallenge  *DNSChallenge  `description:"Activate DNS-01 Challenge." json:"dnsChallenge,omitempty" toml:"dnsChallenge,omitempty" yaml:"dnsChallenge,omitempty" label:"allowEmpty"`
-	HTTPChallenge *HTTPChallenge `description:"Activate HTTP-01 Challenge." json:"httpChallenge,omitempty" toml:"httpChallenge,omitempty" yaml:"httpChallenge,omitempty" label:"allowEmpty"`
-	TLSChallenge  *TLSChallenge  `description:"Activate TLS-ALPN-01 Challenge." json:"tlsChallenge,omitempty" toml:"tlsChallenge,omitempty" yaml:"tlsChallenge,omitempty" label:"allowEmpty"`
+	DNSChallenge  *DNSChallenge  `description:"Activate DNS-01 Challenge." json:"dnsChallenge,omitempty" toml:"dnsChallenge,omitempty" yaml:"dnsChallenge,omitempty" label:"allowEmpty" file:"allowEmpty"`
+	HTTPChallenge *HTTPChallenge `description:"Activate HTTP-01 Challenge." json:"httpChallenge,omitempty" toml:"httpChallenge,omitempty" yaml:"httpChallenge,omitempty" label:"allowEmpty" file:"allowEmpty"`
+	TLSChallenge  *TLSChallenge  `description:"Activate TLS-ALPN-01 Challenge." json:"tlsChallenge,omitempty" toml:"tlsChallenge,omitempty" yaml:"tlsChallenge,omitempty" label:"allowEmpty" file:"allowEmpty"`
 }
 
 // SetDefaults sets the default values.
diff --git a/pkg/provider/file/file.go b/pkg/provider/file/file.go
index 879023a00..e7e33bd75 100644
--- a/pkg/provider/file/file.go
+++ b/pkg/provider/file/file.go
@@ -11,15 +11,14 @@ import (
 	"strings"
 	"text/template"
 
-	"github.com/BurntSushi/toml"
 	"github.com/Masterminds/sprig"
 	"github.com/containous/traefik/v2/pkg/config/dynamic"
+	"github.com/containous/traefik/v2/pkg/config/file"
 	"github.com/containous/traefik/v2/pkg/log"
 	"github.com/containous/traefik/v2/pkg/provider"
 	"github.com/containous/traefik/v2/pkg/safe"
 	"github.com/containous/traefik/v2/pkg/tls"
 	"gopkg.in/fsnotify.v1"
-	"gopkg.in/yaml.v2"
 )
 
 const providerName = "file"
@@ -418,22 +417,9 @@ func (p *Provider) decodeConfiguration(filePath string, content string) (*dynami
 		},
 	}
 
-	switch strings.ToLower(filepath.Ext(filePath)) {
-	case ".toml":
-		_, err := toml.Decode(content, configuration)
-		if err != nil {
-			return nil, err
-		}
-
-	case ".yml", ".yaml":
-		var err error
-		err = yaml.Unmarshal([]byte(content), configuration)
-		if err != nil {
-			return nil, err
-		}
-
-	default:
-		return nil, fmt.Errorf("unsupported file extension: %s", filePath)
+	err := file.DecodeContent(content, strings.ToLower(filepath.Ext(filePath)), configuration)
+	if err != nil {
+		return nil, err
 	}
 
 	return configuration, nil
diff --git a/pkg/provider/file/fixtures/toml/simple_file_01.toml b/pkg/provider/file/fixtures/toml/simple_file_01.toml
index bb3df5d87..8fe23ab40 100644
--- a/pkg/provider/file/fixtures/toml/simple_file_01.toml
+++ b/pkg/provider/file/fixtures/toml/simple_file_01.toml
@@ -1,5 +1,5 @@
 [http.routers]
- 
+
   [http.routers."router1"]
     service = "application-1"
 
diff --git a/pkg/provider/file/fixtures/toml/simple_file_02.toml b/pkg/provider/file/fixtures/toml/simple_file_02.toml
index b7ef74001..c8da9db80 100644
--- a/pkg/provider/file/fixtures/toml/simple_file_02.toml
+++ b/pkg/provider/file/fixtures/toml/simple_file_02.toml
@@ -66,7 +66,7 @@
 
   [tcp.services.applicationtcp-1.loadBalancer]
     [[tcp.services.applicationtcp-1.loadBalancer.servers]]
-      url = "http://172.17.0.9:80"
+      address = "http://172.17.0.9:80"
 
 [udp.routers]
 
@@ -77,4 +77,4 @@
 
   [udp.services.applicationudp-1.loadBalancer]
     [[udp.services.applicationudp-1.loadBalancer.servers]]
-      url = "http://172.17.0.10:80"
+      address = "http://172.17.0.10:80"
diff --git a/pkg/provider/file/fixtures/toml/template_in_directory_file02.toml b/pkg/provider/file/fixtures/toml/template_in_directory_file02.toml
index 35c2d25f3..07dadaf9c 100644
--- a/pkg/provider/file/fixtures/toml/template_in_directory_file02.toml
+++ b/pkg/provider/file/fixtures/toml/template_in_directory_file02.toml
@@ -1,6 +1,6 @@
 [http.services]
 {{ range $i, $e := until 20 }}
-  [http.services.application-{{ $e }}]
-    [[http.services.application-{{ $e }}.servers]]
-    url="http://127.0.0.1"
+  [http.services.application-{{ $e }}.loadBalancer]
+    [[http.services.application-{{ $e }}.loadBalancer.servers]]
+    url = "http://127.0.0.1"
 {{ end }}
\ No newline at end of file
diff --git a/pkg/provider/file/fixtures/yaml/template_in_directory_file01.yml b/pkg/provider/file/fixtures/yaml/template_in_directory_file01.yml
index 908eed00c..185740d81 100644
--- a/pkg/provider/file/fixtures/yaml/template_in_directory_file01.yml
+++ b/pkg/provider/file/fixtures/yaml/template_in_directory_file01.yml
@@ -1,6 +1,6 @@
 http:
-{{ range $i, $e := until 20 }}
   routers:
+{{ range $i, $e := until 20 }}
     router{{ $e }}:
       service: application-1
 {{ end }}
diff --git a/pkg/types/metrics.go b/pkg/types/metrics.go
index de0c08283..41d835249 100644
--- a/pkg/types/metrics.go
+++ b/pkg/types/metrics.go
@@ -6,10 +6,10 @@ import (
 
 // Metrics provides options to expose and send Traefik metrics to different third party monitoring systems.
 type Metrics struct {
-	Prometheus *Prometheus `description:"Prometheus metrics exporter type." json:"prometheus,omitempty" toml:"prometheus,omitempty" yaml:"prometheus,omitempty" export:"true" label:"allowEmpty"`
-	Datadog    *Datadog    `description:"Datadog metrics exporter type." json:"datadog,omitempty" toml:"datadog,omitempty" yaml:"datadog,omitempty" export:"true" label:"allowEmpty"`
-	StatsD     *Statsd     `description:"StatsD metrics exporter type." json:"statsD,omitempty" toml:"statsD,omitempty" yaml:"statsD,omitempty" export:"true" label:"allowEmpty"`
-	InfluxDB   *InfluxDB   `description:"InfluxDB metrics exporter type." json:"influxDB,omitempty" toml:"influxDB,omitempty" yaml:"influxDB,omitempty" label:"allowEmpty"`
+	Prometheus *Prometheus `description:"Prometheus metrics exporter type." json:"prometheus,omitempty" toml:"prometheus,omitempty" yaml:"prometheus,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"`
+	Datadog    *Datadog    `description:"Datadog metrics exporter type." json:"datadog,omitempty" toml:"datadog,omitempty" yaml:"datadog,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"`
+	StatsD     *Statsd     `description:"StatsD metrics exporter type." json:"statsD,omitempty" toml:"statsD,omitempty" yaml:"statsD,omitempty" export:"true" label:"allowEmpty" file:"allowEmpty"`
+	InfluxDB   *InfluxDB   `description:"InfluxDB metrics exporter type." json:"influxDB,omitempty" toml:"influxDB,omitempty" yaml:"influxDB,omitempty" label:"allowEmpty" file:"allowEmpty"`
 }
 
 // Prometheus can contain specific configuration used by the Prometheus Metrics exporter.


Test patch:
diff --git a/pkg/cli/fixtures_test.go b/pkg/cli/fixtures_test.go
index 3cdca3927..95718636f 100644
--- a/pkg/cli/fixtures_test.go
+++ b/pkg/cli/fixtures_test.go
@@ -4,7 +4,7 @@ type Yo struct {
 	Foo string `description:"Foo description"`
 	Fii string `description:"Fii description"`
 	Fuu string `description:"Fuu description"`
-	Yi  *Yi    `label:"allowEmpty"`
+	Yi  *Yi    `label:"allowEmpty" file:"allowEmpty"`
 	Yu  *Yi
 }
 
diff --git a/pkg/config/file/file_test.go b/pkg/config/file/file_test.go
index c769514e3..138ecebce 100644
--- a/pkg/config/file/file_test.go
+++ b/pkg/config/file/file_test.go
@@ -42,6 +42,32 @@ fii = "bir"
 	assert.Equal(t, expected, element)
 }
 
+func TestDecodeContent_TOML(t *testing.T) {
+	content := `
+foo = "bar"
+fii = "bir"
+[yi]
+`
+
+	element := &Yo{
+		Fuu: "test",
+	}
+
+	err := DecodeContent(content, ".toml", element)
+	require.NoError(t, err)
+
+	expected := &Yo{
+		Foo: "bar",
+		Fii: "bir",
+		Fuu: "test",
+		Yi: &Yi{
+			Foo: "foo",
+			Fii: "fii",
+		},
+	}
+	assert.Equal(t, expected, element)
+}
+
 func TestDecode_YAML(t *testing.T) {
 	f, err := ioutil.TempFile("", "traefik-config-*.yaml")
 	require.NoError(t, err)
@@ -74,3 +100,29 @@ yi: {}
 	}
 	assert.Equal(t, expected, element)
 }
+
+func TestDecodeContent_YAML(t *testing.T) {
+	content := `
+foo: bar
+fii: bir
+yi: {}
+`
+
+	element := &Yo{
+		Fuu: "test",
+	}
+
+	err := DecodeContent(content, ".yaml", element)
+	require.NoError(t, err)
+
+	expected := &Yo{
+		Foo: "bar",
+		Fii: "bir",
+		Fuu: "test",
+		Yi: &Yi{
+			Foo: "foo",
+			Fii: "fii",
+		},
+	}
+	assert.Equal(t, expected, element)
+}
diff --git a/pkg/config/file/fixtures_test.go b/pkg/config/file/fixtures_test.go
index 25521e8a3..9904f82d1 100644
--- a/pkg/config/file/fixtures_test.go
+++ b/pkg/config/file/fixtures_test.go
@@ -6,7 +6,7 @@ type Yo struct {
 	Foo string
 	Fii string
 	Fuu string
-	Yi  *Yi `label:"allowEmpty"`
+	Yi  *Yi `file:"allowEmpty"`
 }
 
 func (y *Yo) SetDefaults() {
diff --git a/pkg/provider/file/file_test.go b/pkg/provider/file/file_test.go
index fa7fd39ed..7581564f4 100644
--- a/pkg/provider/file/file_test.go
+++ b/pkg/provider/file/file_test.go
@@ -90,11 +90,12 @@ func TestProvideWithoutWatch(t *testing.T) {
 			timeout := time.After(time.Second)
 			select {
 			case conf := <-configChan:
+
 				require.NotNil(t, conf.Configuration.HTTP)
 				numServices := len(conf.Configuration.HTTP.Services) + len(conf.Configuration.TCP.Services) + len(conf.Configuration.UDP.Services)
 				numRouters := len(conf.Configuration.HTTP.Routers) + len(conf.Configuration.TCP.Routers) + len(conf.Configuration.UDP.Routers)
-				assert.Equal(t, numServices, test.expectedNumService)
-				assert.Equal(t, numRouters, test.expectedNumRouter)
+				assert.Equal(t, test.expectedNumService, numServices)
+				assert.Equal(t, test.expectedNumRouter, numRouters)
 				require.NotNil(t, conf.Configuration.TLS)
 				assert.Len(t, conf.Configuration.TLS.Certificates, test.expectedNumTLSConf)
 				assert.Len(t, conf.Configuration.TLS.Options, test.expectedNumTLSOptions)