diff --git a/CHANGELOG.md b/CHANGELOG.md index 70a39026e..c399a57e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Change Log +## [v1.4.0-rc4](https://github.com/containous/traefik/tree/v1.4.0-rc4) (2017-10-02) +[All Commits](https://github.com/containous/traefik/compare/v1.4.0-rc3...v1.4.0-rc4) + +**Bug fixes:** +- **[cluster,kv]** Be certain to clear our marshalled representation before reloading it ([#2165](https://github.com/containous/traefik/pull/2165) by [gozer](https://github.com/gozer)) +- **[consulcatalog]** Consul catalog failed to remove service ([#2157](https://github.com/containous/traefik/pull/2157) by [Juliens](https://github.com/Juliens)) +- **[consulcatalog]** Flaky tests and refresh problem in consul catalog ([#2148](https://github.com/containous/traefik/pull/2148) by [Juliens](https://github.com/Juliens)) +- **[ecs]** Handle empty ECS Clusters properly ([#2170](https://github.com/containous/traefik/pull/2170) by [jeffreykoetsier](https://github.com/jeffreykoetsier)) +- **[middleware]** Fix SSE subscriptions when retries are enabled ([#2145](https://github.com/containous/traefik/pull/2145) by [marco-jantke](https://github.com/marco-jantke)) +- **[websocket]** Forward upgrade error from backend ([#2187](https://github.com/containous/traefik/pull/2187) by [Juliens](https://github.com/Juliens)) +- `bug` command. ([#2178](https://github.com/containous/traefik/pull/2178) by [ldez](https://github.com/ldez)) +- Fix deprecated IdleTimeout config ([#2143](https://github.com/containous/traefik/pull/2143) by [marco-jantke](https://github.com/marco-jantke)) + +**Documentation:** +- **[docker]** Updating Docker output and curl for sticky sessions ([#2150](https://github.com/containous/traefik/pull/2150) by [jtyr](https://github.com/jtyr)) +- **[middleware]** Improve compression documentation ([#2184](https://github.com/containous/traefik/pull/2184) by [errm](https://github.com/errm)) +- Fix grammar mistake in the kv-config docs ([#2197](https://github.com/containous/traefik/pull/2197) by [chr4](https://github.com/chr4)) +- Update gRPC example ([#2191](https://github.com/containous/traefik/pull/2191) by [jsenon](https://github.com/jsenon)) + +**Misc:** +- **[websocket]** Add tests for urlencoded part in url ([#2199](https://github.com/containous/traefik/pull/2199) by [Juliens](https://github.com/Juliens)) + ## [v1.4.0-rc3](https://github.com/containous/traefik/tree/v1.4.0-rc3) (2017-09-18) [All Commits](https://github.com/containous/traefik/compare/v1.4.0-rc2...v1.4.0-rc3) diff --git a/Makefile b/Makefile index 5799a0822..0c04bc283 100644 --- a/Makefile +++ b/Makefile @@ -114,7 +114,7 @@ fmt: gofmt -s -l -w $(SRCS) pull-images: - cat ./integration/resources/compose/*.yml | grep -E '^\s+image:' | awk '{print $$2}' | sort | uniq | xargs -n 1 docker pull + grep --no-filename -E '^\s+image:' ./integration/resources/compose/*.yml | awk '{print $$2}' | sort | uniq | xargs -P 6 -n 1 docker pull help: ## this help @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {sub("\\\\n",sprintf("\n%22c"," "), $$2);printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) diff --git a/cluster/datastore.go b/cluster/datastore.go index ee8f56d9d..47bfd89ba 100644 --- a/cluster/datastore.go +++ b/cluster/datastore.go @@ -199,6 +199,10 @@ func (d *Datastore) get() *Metadata { func (d *Datastore) Load() (Object, error) { d.localLock.Lock() defer d.localLock.Unlock() + + // clear Object first, as mapstructure's decoder doesn't have ZeroFields set to true for merging purposes + d.meta.Object = d.meta.Object[:0] + err := d.kv.LoadConfig(d.meta) if err != nil { return nil, err diff --git a/cmd/traefik/anonymize/anonymize.go b/cmd/traefik/anonymize/anonymize.go new file mode 100644 index 000000000..7be585ee5 --- /dev/null +++ b/cmd/traefik/anonymize/anonymize.go @@ -0,0 +1,136 @@ +package anonymize + +import ( + "encoding/json" + "fmt" + "reflect" + "regexp" + + "github.com/mitchellh/copystructure" + "github.com/mvdan/xurls" +) + +const ( + maskShort = "xxxx" + maskLarge = maskShort + maskShort + maskShort + maskShort + maskShort + maskShort + maskShort + maskShort +) + +// Do configuration. +func Do(baseConfig interface{}, indent bool) (string, error) { + anomConfig, err := copystructure.Copy(baseConfig) + if err != nil { + return "", err + } + + val := reflect.ValueOf(anomConfig) + + err = doOnStruct(val) + if err != nil { + return "", err + } + + configJSON, err := marshal(anomConfig, indent) + if err != nil { + return "", err + } + + return doOnJSON(string(configJSON)), nil +} + +func doOnJSON(input string) string { + mailExp := regexp.MustCompile(`\w[-._\w]*\w@\w[-._\w]*\w\.\w{2,3}"`) + return xurls.Relaxed.ReplaceAllString(mailExp.ReplaceAllString(input, maskLarge+"\""), maskLarge) +} + +func doOnStruct(field reflect.Value) error { + switch field.Kind() { + case reflect.Ptr: + if !field.IsNil() { + if err := doOnStruct(field.Elem()); err != nil { + return err + } + } + case reflect.Struct: + for i := 0; i < field.NumField(); i++ { + fld := field.Field(i) + stField := field.Type().Field(i) + if !isExported(stField) { + continue + } + if stField.Tag.Get("export") == "true" { + if err := doOnStruct(fld); err != nil { + return err + } + } else { + if err := reset(fld, stField.Name); err != nil { + return err + } + } + } + case reflect.Map: + for _, key := range field.MapKeys() { + if err := doOnStruct(field.MapIndex(key)); err != nil { + return err + } + } + case reflect.Slice: + for j := 0; j < field.Len(); j++ { + if err := doOnStruct(field.Index(j)); err != nil { + return err + } + } + } + return nil +} + +func reset(field reflect.Value, name string) error { + if !field.CanSet() { + return fmt.Errorf("cannot reset field %s", name) + } + + switch field.Kind() { + case reflect.Ptr: + if !field.IsNil() { + field.Set(reflect.Zero(field.Type())) + } + case reflect.Struct: + if field.IsValid() { + field.Set(reflect.Zero(field.Type())) + } + case reflect.String: + if field.String() != "" { + field.Set(reflect.ValueOf(maskShort)) + } + case reflect.Map: + if field.Len() > 0 { + field.Set(reflect.MakeMap(field.Type())) + } + case reflect.Slice: + if field.Len() > 0 { + field.Set(reflect.MakeSlice(field.Type(), 0, 0)) + } + case reflect.Interface: + if !field.IsNil() { + return reset(field.Elem(), "") + } + default: + // Primitive type + field.Set(reflect.Zero(field.Type())) + } + return nil +} + +// isExported return true is a struct field is exported, else false +func isExported(f reflect.StructField) bool { + if f.PkgPath != "" && !f.Anonymous { + return false + } + return true +} + +func marshal(anomConfig interface{}, indent bool) ([]byte, error) { + if indent { + return json.MarshalIndent(anomConfig, "", " ") + } + return json.Marshal(anomConfig) +} diff --git a/cmd/traefik/anonymize/anonymize_config_test.go b/cmd/traefik/anonymize/anonymize_config_test.go new file mode 100644 index 000000000..43b0844d2 --- /dev/null +++ b/cmd/traefik/anonymize/anonymize_config_test.go @@ -0,0 +1,666 @@ +package anonymize + +import ( + "crypto/tls" + "testing" + "time" + + "github.com/containous/flaeg" + "github.com/containous/traefik/acme" + "github.com/containous/traefik/configuration" + "github.com/containous/traefik/middlewares" + "github.com/containous/traefik/provider" + "github.com/containous/traefik/provider/boltdb" + "github.com/containous/traefik/provider/consul" + "github.com/containous/traefik/provider/docker" + "github.com/containous/traefik/provider/dynamodb" + "github.com/containous/traefik/provider/ecs" + "github.com/containous/traefik/provider/etcd" + "github.com/containous/traefik/provider/eureka" + "github.com/containous/traefik/provider/file" + "github.com/containous/traefik/provider/kubernetes" + "github.com/containous/traefik/provider/kv" + "github.com/containous/traefik/provider/marathon" + "github.com/containous/traefik/provider/mesos" + "github.com/containous/traefik/provider/rancher" + "github.com/containous/traefik/provider/web" + "github.com/containous/traefik/provider/zk" + "github.com/containous/traefik/safe" + "github.com/containous/traefik/types" + thoas_stats "github.com/thoas/stats" +) + +func TestDo_globalConfiguration(t *testing.T) { + + config := &configuration.GlobalConfiguration{} + + config.GraceTimeOut = flaeg.Duration(666 * time.Second) + config.Debug = true + config.CheckNewVersion = true + config.AccessLogsFile = "AccessLogsFile" + config.AccessLog = &types.AccessLog{ + FilePath: "AccessLog FilePath", + Format: "AccessLog Format", + } + config.TraefikLogsFile = "TraefikLogsFile" + config.LogLevel = "LogLevel" + config.EntryPoints = configuration.EntryPoints{ + "foo": { + Network: "foo Network", + Address: "foo Address", + TLS: &configuration.TLS{ + MinVersion: "foo MinVersion", + CipherSuites: []string{"foo CipherSuites 1", "foo CipherSuites 2", "foo CipherSuites 3"}, + Certificates: configuration.Certificates{ + {CertFile: "CertFile 1", KeyFile: "KeyFile 1"}, + {CertFile: "CertFile 2", KeyFile: "KeyFile 2"}, + }, + ClientCAFiles: []string{"foo ClientCAFiles 1", "foo ClientCAFiles 2", "foo ClientCAFiles 3"}, + }, + Redirect: &configuration.Redirect{ + Replacement: "foo Replacement", + Regex: "foo Regex", + EntryPoint: "foo EntryPoint", + }, + Auth: &types.Auth{ + Basic: &types.Basic{ + UsersFile: "foo Basic UsersFile", + Users: types.Users{"foo Basic Users 1", "foo Basic Users 2", "foo Basic Users 3"}, + }, + Digest: &types.Digest{ + UsersFile: "foo Digest UsersFile", + Users: types.Users{"foo Digest Users 1", "foo Digest Users 2", "foo Digest Users 3"}, + }, + Forward: &types.Forward{ + Address: "foo Address", + TLS: &types.ClientTLS{ + CA: "foo CA", + Cert: "foo Cert", + Key: "foo Key", + InsecureSkipVerify: true, + }, + TrustForwardHeader: true, + }, + }, + WhitelistSourceRange: []string{"foo WhitelistSourceRange 1", "foo WhitelistSourceRange 2", "foo WhitelistSourceRange 3"}, + Compress: true, + ProxyProtocol: true, + }, + "fii": { + Network: "fii Network", + Address: "fii Address", + TLS: &configuration.TLS{ + MinVersion: "fii MinVersion", + CipherSuites: []string{"fii CipherSuites 1", "fii CipherSuites 2", "fii CipherSuites 3"}, + Certificates: configuration.Certificates{ + {CertFile: "CertFile 1", KeyFile: "KeyFile 1"}, + {CertFile: "CertFile 2", KeyFile: "KeyFile 2"}, + }, + ClientCAFiles: []string{"fii ClientCAFiles 1", "fii ClientCAFiles 2", "fii ClientCAFiles 3"}, + }, + Redirect: &configuration.Redirect{ + Replacement: "fii Replacement", + Regex: "fii Regex", + EntryPoint: "fii EntryPoint", + }, + Auth: &types.Auth{ + Basic: &types.Basic{ + UsersFile: "fii Basic UsersFile", + Users: types.Users{"fii Basic Users 1", "fii Basic Users 2", "fii Basic Users 3"}, + }, + Digest: &types.Digest{ + UsersFile: "fii Digest UsersFile", + Users: types.Users{"fii Digest Users 1", "fii Digest Users 2", "fii Digest Users 3"}, + }, + Forward: &types.Forward{ + Address: "fii Address", + TLS: &types.ClientTLS{ + CA: "fii CA", + Cert: "fii Cert", + Key: "fii Key", + InsecureSkipVerify: true, + }, + TrustForwardHeader: true, + }, + }, + WhitelistSourceRange: []string{"fii WhitelistSourceRange 1", "fii WhitelistSourceRange 2", "fii WhitelistSourceRange 3"}, + Compress: true, + ProxyProtocol: true, + }, + } + config.Cluster = &types.Cluster{ + Node: "Cluster Node", + Store: &types.Store{ + Prefix: "Cluster Store Prefix", + // ... + }, + } + config.Constraints = types.Constraints{ + { + Key: "Constraints Key 1", + Regex: "Constraints Regex 2", + MustMatch: true, + }, + { + Key: "Constraints Key 1", + Regex: "Constraints Regex 2", + MustMatch: true, + }, + } + config.ACME = &acme.ACME{ + Email: "acme Email", + Domains: []acme.Domain{ + { + Main: "Domains Main", + SANs: []string{"Domains acme SANs 1", "Domains acme SANs 2", "Domains acme SANs 3"}, + }, + }, + Storage: "Storage", + StorageFile: "StorageFile", + OnDemand: true, + OnHostRule: true, + CAServer: "CAServer", + EntryPoint: "EntryPoint", + DNSProvider: "DNSProvider", + DelayDontCheckDNS: 666, + ACMELogging: true, + TLSConfig: &tls.Config{ + InsecureSkipVerify: true, + // ... + }, + } + config.DefaultEntryPoints = configuration.DefaultEntryPoints{"DefaultEntryPoints 1", "DefaultEntryPoints 2", "DefaultEntryPoints 3"} + config.ProvidersThrottleDuration = flaeg.Duration(666 * time.Second) + config.MaxIdleConnsPerHost = 666 + config.IdleTimeout = flaeg.Duration(666 * time.Second) + config.InsecureSkipVerify = true + config.RootCAs = configuration.RootCAs{"RootCAs 1", "RootCAs 2", "RootCAs 3"} + config.Retry = &configuration.Retry{ + Attempts: 666, + } + config.HealthCheck = &configuration.HealthCheckConfig{ + Interval: flaeg.Duration(666 * time.Second), + } + config.RespondingTimeouts = &configuration.RespondingTimeouts{ + ReadTimeout: flaeg.Duration(666 * time.Second), + WriteTimeout: flaeg.Duration(666 * time.Second), + IdleTimeout: flaeg.Duration(666 * time.Second), + } + config.ForwardingTimeouts = &configuration.ForwardingTimeouts{ + DialTimeout: flaeg.Duration(666 * time.Second), + ResponseHeaderTimeout: flaeg.Duration(666 * time.Second), + } + config.Docker = &docker.Provider{ + BaseProvider: provider.BaseProvider{ + Watch: true, + Filename: "docker Filename", + Constraints: types.Constraints{ + { + Key: "docker Constraints Key 1", + Regex: "docker Constraints Regex 2", + MustMatch: true, + }, + { + Key: "docker Constraints Key 1", + Regex: "docker Constraints Regex 2", + MustMatch: true, + }, + }, + Trace: true, + DebugLogGeneratedTemplate: true, + }, + Endpoint: "docker Endpoint", + Domain: "docker Domain", + TLS: &types.ClientTLS{ + CA: "docker CA", + Cert: "docker Cert", + Key: "docker Key", + InsecureSkipVerify: true, + }, + ExposedByDefault: true, + UseBindPortIP: true, + SwarmMode: true, + } + config.File = &file.Provider{ + BaseProvider: provider.BaseProvider{ + Watch: true, + Filename: "file Filename", + Constraints: types.Constraints{ + { + Key: "file Constraints Key 1", + Regex: "file Constraints Regex 2", + MustMatch: true, + }, + { + Key: "file Constraints Key 1", + Regex: "file Constraints Regex 2", + MustMatch: true, + }, + }, + Trace: true, + DebugLogGeneratedTemplate: true, + }, + Directory: "file Directory", + } + config.Web = &web.Provider{ + Address: "web Address", + CertFile: "web CertFile", + KeyFile: "web KeyFile", + ReadOnly: true, + Statistics: &types.Statistics{ + RecentErrors: 666, + }, + Metrics: &types.Metrics{ + Prometheus: &types.Prometheus{ + Buckets: types.Buckets{6.5, 6.6, 6.7}, + }, + Datadog: &types.Datadog{ + Address: "Datadog Address", + PushInterval: "Datadog PushInterval", + }, + StatsD: &types.Statsd{ + Address: "StatsD Address", + PushInterval: "StatsD PushInterval", + }, + }, + Path: "web Path", + Auth: &types.Auth{ + Basic: &types.Basic{ + UsersFile: "web Basic UsersFile", + Users: types.Users{"web Basic Users 1", "web Basic Users 2", "web Basic Users 3"}, + }, + Digest: &types.Digest{ + UsersFile: "web Digest UsersFile", + Users: types.Users{"web Digest Users 1", "web Digest Users 2", "web Digest Users 3"}, + }, + Forward: &types.Forward{ + Address: "web Address", + TLS: &types.ClientTLS{ + CA: "web CA", + Cert: "web Cert", + Key: "web Key", + InsecureSkipVerify: true, + }, + TrustForwardHeader: true, + }, + }, + Debug: true, + CurrentConfigurations: &safe.Safe{}, + Stats: &thoas_stats.Stats{ + Uptime: time.Now(), + Pid: 666, + ResponseCounts: map[string]int{"foo": 1, "fii": 2, "fuu": 3}, + TotalResponseCounts: map[string]int{"foo": 1, "fii": 2, "fuu": 3}, + TotalResponseTime: time.Now(), + }, + StatsRecorder: &middlewares.StatsRecorder{}, + } + config.Marathon = &marathon.Provider{ + BaseProvider: provider.BaseProvider{ + Watch: true, + Filename: "marathon Filename", + Constraints: types.Constraints{ + { + Key: "marathon Constraints Key 1", + Regex: "marathon Constraints Regex 2", + MustMatch: true, + }, + { + Key: "marathon Constraints Key 1", + Regex: "marathon Constraints Regex 2", + MustMatch: true, + }, + }, + Trace: true, + DebugLogGeneratedTemplate: true, + }, + Endpoint: "", + Domain: "", + ExposedByDefault: true, + GroupsAsSubDomains: true, + DCOSToken: "", + MarathonLBCompatibility: true, + TLS: &types.ClientTLS{ + CA: "marathon CA", + Cert: "marathon Cert", + Key: "marathon Key", + InsecureSkipVerify: true, + }, + DialerTimeout: flaeg.Duration(666 * time.Second), + KeepAlive: flaeg.Duration(666 * time.Second), + ForceTaskHostname: true, + Basic: &marathon.Basic{ + HTTPBasicAuthUser: "marathon HTTPBasicAuthUser", + HTTPBasicPassword: "marathon HTTPBasicPassword", + }, + RespectReadinessChecks: true, + } + config.ConsulCatalog = &consul.CatalogProvider{ + BaseProvider: provider.BaseProvider{ + Watch: true, + Filename: "ConsulCatalog Filename", + Constraints: types.Constraints{ + { + Key: "ConsulCatalog Constraints Key 1", + Regex: "ConsulCatalog Constraints Regex 2", + MustMatch: true, + }, + { + Key: "ConsulCatalog Constraints Key 1", + Regex: "ConsulCatalog Constraints Regex 2", + MustMatch: true, + }, + }, + Trace: true, + DebugLogGeneratedTemplate: true, + }, + Endpoint: "ConsulCatalog Endpoint", + Domain: "ConsulCatalog Domain", + ExposedByDefault: true, + Prefix: "ConsulCatalog Prefix", + FrontEndRule: "ConsulCatalog FrontEndRule", + } + config.Kubernetes = &kubernetes.Provider{ + BaseProvider: provider.BaseProvider{ + Watch: true, + Filename: "k8s Filename", + Constraints: types.Constraints{ + { + Key: "k8s Constraints Key 1", + Regex: "k8s Constraints Regex 2", + MustMatch: true, + }, + { + Key: "k8s Constraints Key 1", + Regex: "k8s Constraints Regex 2", + MustMatch: true, + }, + }, + Trace: true, + DebugLogGeneratedTemplate: true, + }, + Endpoint: "k8s Endpoint", + Token: "k8s Token", + CertAuthFilePath: "k8s CertAuthFilePath", + DisablePassHostHeaders: true, + Namespaces: kubernetes.Namespaces{"k8s Namespaces 1", "k8s Namespaces 2", "k8s Namespaces 3"}, + LabelSelector: "k8s LabelSelector", + } + config.Mesos = &mesos.Provider{ + BaseProvider: provider.BaseProvider{ + Watch: true, + Filename: "mesos Filename", + Constraints: types.Constraints{ + { + Key: "mesos Constraints Key 1", + Regex: "mesos Constraints Regex 2", + MustMatch: true, + }, + { + Key: "mesos Constraints Key 1", + Regex: "mesos Constraints Regex 2", + MustMatch: true, + }, + }, + Trace: true, + DebugLogGeneratedTemplate: true, + }, + Endpoint: "mesos Endpoint", + Domain: "mesos Domain", + ExposedByDefault: true, + GroupsAsSubDomains: true, + ZkDetectionTimeout: 666, + RefreshSeconds: 666, + IPSources: "mesos IPSources", + StateTimeoutSecond: 666, + Masters: []string{"mesos Masters 1", "mesos Masters 2", "mesos Masters 3"}, + } + config.Eureka = &eureka.Provider{ + BaseProvider: provider.BaseProvider{ + Watch: true, + Filename: "eureka Filename", + Constraints: types.Constraints{ + { + Key: "eureka Constraints Key 1", + Regex: "eureka Constraints Regex 2", + MustMatch: true, + }, + { + Key: "eureka Constraints Key 1", + Regex: "eureka Constraints Regex 2", + MustMatch: true, + }, + }, + Trace: true, + DebugLogGeneratedTemplate: true, + }, + Endpoint: "eureka Endpoint", + Delay: "eureka Delay", + } + config.ECS = &ecs.Provider{ + BaseProvider: provider.BaseProvider{ + Watch: true, + Filename: "ecs Filename", + Constraints: types.Constraints{ + { + Key: "ecs Constraints Key 1", + Regex: "ecs Constraints Regex 2", + MustMatch: true, + }, + { + Key: "ecs Constraints Key 1", + Regex: "ecs Constraints Regex 2", + MustMatch: true, + }, + }, + Trace: true, + DebugLogGeneratedTemplate: true, + }, + Domain: "ecs Domain", + ExposedByDefault: true, + RefreshSeconds: 666, + Clusters: ecs.Clusters{"ecs Clusters 1", "ecs Clusters 2", "ecs Clusters 3"}, + Cluster: "ecs Cluster", + AutoDiscoverClusters: true, + Region: "ecs Region", + AccessKeyID: "ecs AccessKeyID", + SecretAccessKey: "ecs SecretAccessKey", + } + config.Rancher = &rancher.Provider{ + BaseProvider: provider.BaseProvider{ + Watch: true, + Filename: "rancher Filename", + Constraints: types.Constraints{ + { + Key: "rancher Constraints Key 1", + Regex: "rancher Constraints Regex 2", + MustMatch: true, + }, + { + Key: "rancher Constraints Key 1", + Regex: "rancher Constraints Regex 2", + MustMatch: true, + }, + }, + Trace: true, + DebugLogGeneratedTemplate: true, + }, + APIConfiguration: rancher.APIConfiguration{ + Endpoint: "rancher Endpoint", + AccessKey: "rancher AccessKey", + SecretKey: "rancher SecretKey", + }, + API: &rancher.APIConfiguration{ + Endpoint: "rancher Endpoint", + AccessKey: "rancher AccessKey", + SecretKey: "rancher SecretKey", + }, + Metadata: &rancher.MetadataConfiguration{ + IntervalPoll: true, + Prefix: "rancher Metadata Prefix", + }, + Domain: "rancher Domain", + RefreshSeconds: 666, + ExposedByDefault: true, + EnableServiceHealthFilter: true, + } + config.DynamoDB = &dynamodb.Provider{ + BaseProvider: provider.BaseProvider{ + Watch: true, + Filename: "dynamodb Filename", + Constraints: types.Constraints{ + { + Key: "dynamodb Constraints Key 1", + Regex: "dynamodb Constraints Regex 2", + MustMatch: true, + }, + { + Key: "dynamodb Constraints Key 1", + Regex: "dynamodb Constraints Regex 2", + MustMatch: true, + }, + }, + Trace: true, + DebugLogGeneratedTemplate: true, + }, + AccessKeyID: "dynamodb AccessKeyID", + RefreshSeconds: 666, + Region: "dynamodb Region", + SecretAccessKey: "dynamodb SecretAccessKey", + TableName: "dynamodb TableName", + Endpoint: "dynamodb Endpoint", + } + config.Etcd = &etcd.Provider{ + Provider: kv.Provider{ + BaseProvider: provider.BaseProvider{ + Watch: true, + Filename: "etcd Filename", + Constraints: types.Constraints{ + { + Key: "etcd Constraints Key 1", + Regex: "etcd Constraints Regex 2", + MustMatch: true, + }, + { + Key: "etcd Constraints Key 1", + Regex: "etcd Constraints Regex 2", + MustMatch: true, + }, + }, + Trace: true, + DebugLogGeneratedTemplate: true, + }, + Endpoint: "etcd Endpoint", + Prefix: "etcd Prefix", + TLS: &types.ClientTLS{ + CA: "etcd CA", + Cert: "etcd Cert", + Key: "etcd Key", + InsecureSkipVerify: true, + }, + Username: "etcd Username", + Password: "etcd Password", + }, + } + config.Zookeeper = &zk.Provider{ + Provider: kv.Provider{ + BaseProvider: provider.BaseProvider{ + Watch: true, + Filename: "zk Filename", + Constraints: types.Constraints{ + { + Key: "zk Constraints Key 1", + Regex: "zk Constraints Regex 2", + MustMatch: true, + }, + { + Key: "zk Constraints Key 1", + Regex: "zk Constraints Regex 2", + MustMatch: true, + }, + }, + Trace: true, + DebugLogGeneratedTemplate: true, + }, + Endpoint: "zk Endpoint", + Prefix: "zk Prefix", + TLS: &types.ClientTLS{ + CA: "zk CA", + Cert: "zk Cert", + Key: "zk Key", + InsecureSkipVerify: true, + }, + Username: "zk Username", + Password: "zk Password", + }, + } + config.Boltdb = &boltdb.Provider{ + Provider: kv.Provider{ + BaseProvider: provider.BaseProvider{ + Watch: true, + Filename: "boltdb Filename", + Constraints: types.Constraints{ + { + Key: "boltdb Constraints Key 1", + Regex: "boltdb Constraints Regex 2", + MustMatch: true, + }, + { + Key: "boltdb Constraints Key 1", + Regex: "boltdb Constraints Regex 2", + MustMatch: true, + }, + }, + Trace: true, + DebugLogGeneratedTemplate: true, + }, + Endpoint: "boltdb Endpoint", + Prefix: "boltdb Prefix", + TLS: &types.ClientTLS{ + CA: "boltdb CA", + Cert: "boltdb Cert", + Key: "boltdb Key", + InsecureSkipVerify: true, + }, + Username: "boltdb Username", + Password: "boltdb Password", + }, + } + config.Consul = &consul.Provider{ + Provider: kv.Provider{ + BaseProvider: provider.BaseProvider{ + Watch: true, + Filename: "consul Filename", + Constraints: types.Constraints{ + { + Key: "consul Constraints Key 1", + Regex: "consul Constraints Regex 2", + MustMatch: true, + }, + { + Key: "consul Constraints Key 1", + Regex: "consul Constraints Regex 2", + MustMatch: true, + }, + }, + Trace: true, + DebugLogGeneratedTemplate: true, + }, + Endpoint: "consul Endpoint", + Prefix: "consul Prefix", + TLS: &types.ClientTLS{ + CA: "consul CA", + Cert: "consul Cert", + Key: "consul Key", + InsecureSkipVerify: true, + }, + Username: "consul Username", + Password: "consul Password", + }, + } + + cleanJSON, err := Do(config, true) + if err != nil { + t.Fatal(err, cleanJSON) + } +} diff --git a/cmd/traefik/anonymize/anonymize_doOnJSON_test.go b/cmd/traefik/anonymize/anonymize_doOnJSON_test.go new file mode 100644 index 000000000..87f43d125 --- /dev/null +++ b/cmd/traefik/anonymize/anonymize_doOnJSON_test.go @@ -0,0 +1,238 @@ +package anonymize + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func Test_doOnJSON(t *testing.T) { + baseConfiguration := ` +{ + "GraceTimeOut": 10000000000, + "Debug": false, + "CheckNewVersion": true, + "AccessLogsFile": "", + "TraefikLogsFile": "", + "LogLevel": "ERROR", + "EntryPoints": { + "http": { + "Network": "", + "Address": ":80", + "TLS": null, + "Redirect": { + "EntryPoint": "https", + "Regex": "", + "Replacement": "" + }, + "Auth": null, + "Compress": false + }, + "https": { + "Network": "", + "Address": ":443", + "TLS": { + "MinVersion": "", + "CipherSuites": null, + "Certificates": null, + "ClientCAFiles": null + }, + "Redirect": null, + "Auth": null, + "Compress": false + } + }, + "Cluster": null, + "Constraints": [], + "ACME": { + "Email": "foo@bar.com", + "Domains": [ + { + "Main": "foo@bar.com", + "SANs": null + }, + { + "Main": "foo@bar.com", + "SANs": null + } + ], + "Storage": "", + "StorageFile": "/acme/acme.json", + "OnDemand": true, + "OnHostRule": true, + "CAServer": "", + "EntryPoint": "https", + "DNSProvider": "", + "DelayDontCheckDNS": 0, + "ACMELogging": false, + "TLSConfig": null + }, + "DefaultEntryPoints": [ + "https", + "http" + ], + "ProvidersThrottleDuration": 2000000000, + "MaxIdleConnsPerHost": 200, + "IdleTimeout": 180000000000, + "InsecureSkipVerify": false, + "Retry": null, + "HealthCheck": { + "Interval": 30000000000 + }, + "Docker": null, + "File": null, + "Web": null, + "Marathon": null, + "Consul": null, + "ConsulCatalog": null, + "Etcd": null, + "Zookeeper": null, + "Boltdb": null, + "Kubernetes": null, + "Mesos": null, + "Eureka": null, + "ECS": null, + "Rancher": null, + "DynamoDB": null, + "ConfigFile": "/etc/traefik/traefik.toml" +} +` + expectedConfiguration := ` +{ + "GraceTimeOut": 10000000000, + "Debug": false, + "CheckNewVersion": true, + "AccessLogsFile": "", + "TraefikLogsFile": "", + "LogLevel": "ERROR", + "EntryPoints": { + "http": { + "Network": "", + "Address": ":80", + "TLS": null, + "Redirect": { + "EntryPoint": "https", + "Regex": "", + "Replacement": "" + }, + "Auth": null, + "Compress": false + }, + "https": { + "Network": "", + "Address": ":443", + "TLS": { + "MinVersion": "", + "CipherSuites": null, + "Certificates": null, + "ClientCAFiles": null + }, + "Redirect": null, + "Auth": null, + "Compress": false + } + }, + "Cluster": null, + "Constraints": [], + "ACME": { + "Email": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "Domains": [ + { + "Main": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "SANs": null + }, + { + "Main": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "SANs": null + } + ], + "Storage": "", + "StorageFile": "/acme/acme.json", + "OnDemand": true, + "OnHostRule": true, + "CAServer": "", + "EntryPoint": "https", + "DNSProvider": "", + "DelayDontCheckDNS": 0, + "ACMELogging": false, + "TLSConfig": null + }, + "DefaultEntryPoints": [ + "https", + "http" + ], + "ProvidersThrottleDuration": 2000000000, + "MaxIdleConnsPerHost": 200, + "IdleTimeout": 180000000000, + "InsecureSkipVerify": false, + "Retry": null, + "HealthCheck": { + "Interval": 30000000000 + }, + "Docker": null, + "File": null, + "Web": null, + "Marathon": null, + "Consul": null, + "ConsulCatalog": null, + "Etcd": null, + "Zookeeper": null, + "Boltdb": null, + "Kubernetes": null, + "Mesos": null, + "Eureka": null, + "ECS": null, + "Rancher": null, + "DynamoDB": null, + "ConfigFile": "/etc/traefik/traefik.toml" +} +` + anomConfiguration := doOnJSON(baseConfiguration) + + if anomConfiguration != expectedConfiguration { + t.Errorf("Got %s, want %s.", anomConfiguration, expectedConfiguration) + } +} + +func Test_doOnJSON_simple(t *testing.T) { + testCases := []struct { + name string + input string + expectedOutput string + }{ + { + name: "email", + input: `{ + "email1": "goo@example.com", + "email2": "foo.bargoo@example.com", + "email3": "foo.bargoo@example.com.us" + }`, + expectedOutput: `{ + "email1": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "email2": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "email3": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + }`, + }, + { + name: "url", + input: `{ + "URL": "foo domain.com foo", + "URL": "foo sub.domain.com foo", + "URL": "foo sub.sub.domain.com foo", + "URL": "foo sub.sub.sub.domain.com.us foo" + }`, + expectedOutput: `{ + "URL": "foo xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx foo", + "URL": "foo xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx foo", + "URL": "foo xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx foo", + "URL": "foo xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx foo" + }`, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + output := doOnJSON(test.input) + assert.Equal(t, test.expectedOutput, output) + }) + } +} diff --git a/cmd/traefik/anonymize/anonymize_doOnStruct_test.go b/cmd/traefik/anonymize/anonymize_doOnStruct_test.go new file mode 100644 index 000000000..3d9529682 --- /dev/null +++ b/cmd/traefik/anonymize/anonymize_doOnStruct_test.go @@ -0,0 +1,176 @@ +package anonymize + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/assert" +) + +type Courgette struct { + Ji string + Ho string +} +type Tomate struct { + Ji string + Ho string +} + +type Carotte struct { + Name string + Value int + Courgette Courgette + ECourgette Courgette `export:"true"` + Pourgette *Courgette + EPourgette *Courgette `export:"true"` + Aubergine map[string]string + EAubergine map[string]string `export:"true"` + SAubergine map[string]Tomate + ESAubergine map[string]Tomate `export:"true"` + PSAubergine map[string]*Tomate + EPAubergine map[string]*Tomate `export:"true"` +} + +func Test_doOnStruct(t *testing.T) { + testCase := []struct { + name string + base *Carotte + expected *Carotte + hasError bool + }{ + { + name: "primitive", + base: &Carotte{ + Name: "koko", + Value: 666, + }, + expected: &Carotte{ + Name: "xxxx", + }, + }, + { + name: "struct", + base: &Carotte{ + Name: "koko", + Courgette: Courgette{ + Ji: "huu", + }, + }, + expected: &Carotte{ + Name: "xxxx", + }, + }, + { + name: "pointer", + base: &Carotte{ + Name: "koko", + Pourgette: &Courgette{ + Ji: "hoo", + }, + }, + expected: &Carotte{ + Name: "xxxx", + Pourgette: nil, + }, + }, + { + name: "export struct", + base: &Carotte{ + Name: "koko", + ECourgette: Courgette{ + Ji: "huu", + }, + }, + expected: &Carotte{ + Name: "xxxx", + ECourgette: Courgette{ + Ji: "xxxx", + }, + }, + }, + { + name: "export pointer struct", + base: &Carotte{ + Name: "koko", + ECourgette: Courgette{ + Ji: "huu", + }, + }, + expected: &Carotte{ + Name: "xxxx", + ECourgette: Courgette{ + Ji: "xxxx", + }, + }, + }, + { + name: "export map string/string", + base: &Carotte{ + Name: "koko", + EAubergine: map[string]string{ + "foo": "bar", + }, + }, + expected: &Carotte{ + Name: "xxxx", + EAubergine: map[string]string{ + "foo": "bar", + }, + }, + }, + { + name: "export map string/pointer", + base: &Carotte{ + Name: "koko", + EPAubergine: map[string]*Tomate{ + "foo": { + Ji: "fdskljf", + }, + }, + }, + expected: &Carotte{ + Name: "xxxx", + EPAubergine: map[string]*Tomate{ + "foo": { + Ji: "xxxx", + }, + }, + }, + }, + { + name: "export map string/struct (UNSAFE)", + base: &Carotte{ + Name: "koko", + ESAubergine: map[string]Tomate{ + "foo": { + Ji: "JiJiJi", + }, + }, + }, + expected: &Carotte{ + Name: "xxxx", + ESAubergine: map[string]Tomate{ + "foo": { + Ji: "JiJiJi", + }, + }, + }, + hasError: true, + }, + } + + for _, test := range testCase { + t.Run(test.name, func(t *testing.T) { + val := reflect.ValueOf(test.base).Elem() + err := doOnStruct(val) + if !test.hasError && err != nil { + t.Fatal(err) + } + if test.hasError && err == nil { + t.Fatal("Got no error but want an error.") + } + + assert.EqualValues(t, test.expected, test.base) + }) + } +} diff --git a/cmd/traefik/bug.go b/cmd/traefik/bug.go index 66913c27f..27d99d389 100644 --- a/cmd/traefik/bug.go +++ b/cmd/traefik/bug.go @@ -2,20 +2,18 @@ package main import ( "bytes" - "encoding/json" "fmt" "net/url" "os/exec" - "regexp" "runtime" "text/template" "github.com/containous/flaeg" - "github.com/mvdan/xurls" + "github.com/containous/traefik/cmd/traefik/anonymize" ) -var ( - bugtracker = "https://github.com/containous/traefik/issues/new" +const ( + bugTracker = "https://github.com/containous/traefik/issues/new" bugTemplate = `