1
0
Fork 0

Move code to pkg

This commit is contained in:
Ludovic Fernandez 2019-03-15 09:42:03 +01:00 committed by Traefiker Bot
parent bd4c822670
commit f1b085fa36
465 changed files with 656 additions and 680 deletions

307
pkg/provider/file/file.go Normal file
View file

@ -0,0 +1,307 @@
package file
import (
"context"
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
"strings"
"text/template"
"github.com/containous/traefik/pkg/config"
"github.com/containous/traefik/pkg/log"
"github.com/containous/traefik/pkg/provider"
"github.com/containous/traefik/pkg/safe"
"github.com/containous/traefik/pkg/tls"
"github.com/pkg/errors"
"gopkg.in/fsnotify.v1"
)
const providerName = "file"
var _ provider.Provider = (*Provider)(nil)
// Provider holds configurations of the provider.
type Provider struct {
provider.BaseProvider `mapstructure:",squash" export:"true"`
Directory string `description:"Load configuration from one or more .toml files in a directory" export:"true"`
TraefikFile string
}
// Init the provider
func (p *Provider) Init() error {
return p.BaseProvider.Init()
}
// Provide allows the file provider to provide configurations to traefik
// using the given configuration channel.
func (p *Provider) Provide(configurationChan chan<- config.Message, pool *safe.Pool) error {
configuration, err := p.BuildConfiguration()
if err != nil {
return err
}
if p.Watch {
var watchItem string
switch {
case len(p.Directory) > 0:
watchItem = p.Directory
case len(p.Filename) > 0:
watchItem = filepath.Dir(p.Filename)
default:
watchItem = filepath.Dir(p.TraefikFile)
}
if err := p.addWatcher(pool, watchItem, configurationChan, p.watcherCallback); err != nil {
return err
}
}
sendConfigToChannel(configurationChan, configuration)
return nil
}
// BuildConfiguration loads configuration either from file or a directory specified by 'Filename'/'Directory'
// and returns a 'Configuration' object
func (p *Provider) BuildConfiguration() (*config.Configuration, error) {
ctx := log.With(context.Background(), log.Str(log.ProviderName, providerName))
if len(p.Directory) > 0 {
return p.loadFileConfigFromDirectory(ctx, p.Directory, nil)
}
if len(p.Filename) > 0 {
return p.loadFileConfig(p.Filename, true)
}
if len(p.TraefikFile) > 0 {
return p.loadFileConfig(p.TraefikFile, false)
}
return nil, errors.New("error using file configuration backend, no filename defined")
}
func (p *Provider) addWatcher(pool *safe.Pool, directory string, configurationChan chan<- config.Message, callback func(chan<- config.Message, fsnotify.Event)) error {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return fmt.Errorf("error creating file watcher: %s", err)
}
err = watcher.Add(directory)
if err != nil {
return fmt.Errorf("error adding file watcher: %s", err)
}
// Process events
pool.Go(func(stop chan bool) {
defer watcher.Close()
for {
select {
case <-stop:
return
case evt := <-watcher.Events:
if p.Directory == "" {
var filename string
if len(p.Filename) > 0 {
filename = p.Filename
} else {
filename = p.TraefikFile
}
_, evtFileName := filepath.Split(evt.Name)
_, confFileName := filepath.Split(filename)
if evtFileName == confFileName {
callback(configurationChan, evt)
}
} else {
callback(configurationChan, evt)
}
case err := <-watcher.Errors:
log.WithoutContext().WithField(log.ProviderName, providerName).Errorf("Watcher event error: %s", err)
}
}
})
return nil
}
func (p *Provider) watcherCallback(configurationChan chan<- config.Message, event fsnotify.Event) {
watchItem := p.TraefikFile
if len(p.Directory) > 0 {
watchItem = p.Directory
} else if len(p.Filename) > 0 {
watchItem = p.Filename
}
logger := log.WithoutContext().WithField(log.ProviderName, providerName)
if _, err := os.Stat(watchItem); err != nil {
logger.Errorf("Unable to watch %s : %v", watchItem, err)
return
}
configuration, err := p.BuildConfiguration()
if err != nil {
logger.Errorf("Error occurred during watcher callback: %s", err)
return
}
sendConfigToChannel(configurationChan, configuration)
}
func sendConfigToChannel(configurationChan chan<- config.Message, configuration *config.Configuration) {
configurationChan <- config.Message{
ProviderName: "file",
Configuration: configuration,
}
}
func readFile(filename string) (string, error) {
if len(filename) > 0 {
buf, err := ioutil.ReadFile(filename)
if err != nil {
return "", err
}
return string(buf), nil
}
return "", fmt.Errorf("invalid filename: %s", filename)
}
func (p *Provider) loadFileConfig(filename string, parseTemplate bool) (*config.Configuration, error) {
fileContent, err := readFile(filename)
if err != nil {
return nil, fmt.Errorf("error reading configuration file: %s - %s", filename, err)
}
var configuration *config.Configuration
if parseTemplate {
configuration, err = p.CreateConfiguration(fileContent, template.FuncMap{}, false)
} else {
configuration, err = p.DecodeConfiguration(fileContent)
}
if err != nil {
return nil, err
}
var tlsConfigs []*tls.Configuration
for _, conf := range configuration.TLS {
bytes, err := conf.Certificate.CertFile.Read()
if err != nil {
log.Error(err)
continue
}
conf.Certificate.CertFile = tls.FileOrContent(string(bytes))
bytes, err = conf.Certificate.KeyFile.Read()
if err != nil {
log.Error(err)
continue
}
conf.Certificate.KeyFile = tls.FileOrContent(string(bytes))
tlsConfigs = append(tlsConfigs, conf)
}
configuration.TLS = tlsConfigs
return configuration, nil
}
func (p *Provider) loadFileConfigFromDirectory(ctx context.Context, directory string, configuration *config.Configuration) (*config.Configuration, error) {
logger := log.FromContext(ctx)
fileList, err := ioutil.ReadDir(directory)
if err != nil {
return configuration, fmt.Errorf("unable to read directory %s: %v", directory, err)
}
if configuration == nil {
configuration = &config.Configuration{
HTTP: &config.HTTPConfiguration{
Routers: make(map[string]*config.Router),
Middlewares: make(map[string]*config.Middleware),
Services: make(map[string]*config.Service),
},
TCP: &config.TCPConfiguration{
Routers: make(map[string]*config.TCPRouter),
Services: make(map[string]*config.TCPService),
},
}
}
configTLSMaps := make(map[*tls.Configuration]struct{})
for _, item := range fileList {
if item.IsDir() {
configuration, err = p.loadFileConfigFromDirectory(ctx, filepath.Join(directory, item.Name()), configuration)
if err != nil {
return configuration, fmt.Errorf("unable to load content configuration from subdirectory %s: %v", item, err)
}
continue
} else if !strings.HasSuffix(item.Name(), ".toml") && !strings.HasSuffix(item.Name(), ".tmpl") {
continue
}
var c *config.Configuration
c, err = p.loadFileConfig(path.Join(directory, item.Name()), true)
if err != nil {
return configuration, err
}
for name, conf := range c.HTTP.Routers {
if _, exists := configuration.HTTP.Routers[name]; exists {
logger.WithField(log.RouterName, name).Warn("HTTP router already configured, skipping")
} else {
configuration.HTTP.Routers[name] = conf
}
}
for name, conf := range c.HTTP.Middlewares {
if _, exists := configuration.HTTP.Middlewares[name]; exists {
logger.WithField(log.MiddlewareName, name).Warn("HTTP middleware already configured, skipping")
} else {
configuration.HTTP.Middlewares[name] = conf
}
}
for name, conf := range c.HTTP.Services {
if _, exists := configuration.HTTP.Services[name]; exists {
logger.WithField(log.ServiceName, name).Warn("HTTP service already configured, skipping")
} else {
configuration.HTTP.Services[name] = conf
}
}
for name, conf := range c.TCP.Routers {
if _, exists := configuration.TCP.Routers[name]; exists {
logger.WithField(log.RouterName, name).Warn("TCP router already configured, skipping")
} else {
configuration.TCP.Routers[name] = conf
}
}
for name, conf := range c.TCP.Services {
if _, exists := configuration.TCP.Services[name]; exists {
logger.WithField(log.ServiceName, name).Warn("TCP service already configured, skipping")
} else {
configuration.TCP.Services[name] = conf
}
}
for _, conf := range c.TLS {
if _, exists := configTLSMaps[conf]; exists {
logger.Warnf("TLS configuration %v already configured, skipping", conf)
} else {
configTLSMaps[conf] = struct{}{}
}
}
}
for conf := range configTLSMaps {
configuration.TLS = append(configuration.TLS, conf)
}
return configuration, nil
}

View file

@ -0,0 +1,360 @@
package file
import (
"context"
"fmt"
"io/ioutil"
"os"
"path"
"testing"
"time"
"github.com/containous/traefik/pkg/config"
"github.com/containous/traefik/pkg/safe"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type ProvideTestCase struct {
desc string
directoryContent []string
fileContent string
traefikFileContent string
expectedNumRouter int
expectedNumService int
expectedNumTLSConf int
}
func TestProvideWithoutWatch(t *testing.T) {
for _, test := range getTestCases() {
t.Run(test.desc+" without watch", func(t *testing.T) {
provider, clean := createProvider(t, test, false)
defer clean()
configChan := make(chan config.Message)
provider.DebugLogGeneratedTemplate = true
go func() {
err := provider.Provide(configChan, safe.NewPool(context.Background()))
assert.NoError(t, err)
}()
timeout := time.After(time.Second)
select {
case conf := <-configChan:
assert.Len(t, conf.Configuration.HTTP.Services, test.expectedNumService)
assert.Len(t, conf.Configuration.HTTP.Routers, test.expectedNumRouter)
assert.Len(t, conf.Configuration.TLS, test.expectedNumTLSConf)
case <-timeout:
t.Errorf("timeout while waiting for config")
}
})
}
}
func TestProvideWithWatch(t *testing.T) {
for _, test := range getTestCases() {
t.Run(test.desc+" with watch", func(t *testing.T) {
provider, clean := createProvider(t, test, true)
defer clean()
configChan := make(chan config.Message)
go func() {
err := provider.Provide(configChan, safe.NewPool(context.Background()))
assert.NoError(t, err)
}()
timeout := time.After(time.Second)
select {
case conf := <-configChan:
assert.Len(t, conf.Configuration.HTTP.Services, 0)
assert.Len(t, conf.Configuration.HTTP.Routers, 0)
assert.Len(t, conf.Configuration.TLS, 0)
case <-timeout:
t.Errorf("timeout while waiting for config")
}
if len(test.fileContent) > 0 {
if err := ioutil.WriteFile(provider.Filename, []byte(test.fileContent), 0755); err != nil {
t.Error(err)
}
}
if len(test.traefikFileContent) > 0 {
if err := ioutil.WriteFile(provider.TraefikFile, []byte(test.traefikFileContent), 0755); err != nil {
t.Error(err)
}
}
if len(test.directoryContent) > 0 {
for _, fileContent := range test.directoryContent {
createRandomFile(t, provider.Directory, fileContent)
}
}
timeout = time.After(time.Second * 1)
var numUpdates, numServices, numRouters, numTLSConfs int
for {
select {
case conf := <-configChan:
numUpdates++
numServices = len(conf.Configuration.HTTP.Services)
numRouters = len(conf.Configuration.HTTP.Routers)
numTLSConfs = len(conf.Configuration.TLS)
t.Logf("received update #%d: services %d/%d, routers %d/%d, TLS configs %d/%d", numUpdates, numServices, test.expectedNumService, numRouters, test.expectedNumRouter, numTLSConfs, test.expectedNumTLSConf)
if numServices == test.expectedNumService && numRouters == test.expectedNumRouter && numTLSConfs == test.expectedNumTLSConf {
return
}
case <-timeout:
t.Fatal("timeout while waiting for config")
}
}
})
}
}
func TestErrorWhenEmptyConfig(t *testing.T) {
provider := &Provider{}
configChan := make(chan config.Message)
errorChan := make(chan struct{})
go func() {
err := provider.Provide(configChan, safe.NewPool(context.Background()))
assert.Error(t, err)
close(errorChan)
}()
timeout := time.After(time.Second)
select {
case <-configChan:
t.Fatal("We should not receive config message")
case <-timeout:
t.Fatal("timeout while waiting for config")
case <-errorChan:
}
}
func getTestCases() []ProvideTestCase {
return []ProvideTestCase{
{
desc: "simple file",
fileContent: createRoutersConfiguration(3) + createServicesConfiguration(6) + createTLS(5),
expectedNumRouter: 3,
expectedNumService: 6,
expectedNumTLSConf: 5,
},
{
desc: "simple file and a traefik file",
fileContent: createRoutersConfiguration(4) + createServicesConfiguration(8) + createTLS(4),
traefikFileContent: `
debug=true
`,
expectedNumRouter: 4,
expectedNumService: 8,
expectedNumTLSConf: 4,
},
{
desc: "template file",
fileContent: `
[http.routers]
{{ range $i, $e := until 20 }}
[http.routers.router{{ $e }}]
service = "application"
{{ end }}
`,
expectedNumRouter: 20,
},
{
desc: "simple directory",
directoryContent: []string{
createRoutersConfiguration(2),
createServicesConfiguration(3),
createTLS(4),
},
expectedNumRouter: 2,
expectedNumService: 3,
expectedNumTLSConf: 4,
},
{
desc: "template in directory",
directoryContent: []string{
`
[http.routers]
{{ range $i, $e := until 20 }}
[http.routers.router{{ $e }}]
service = "application"
{{ end }}
`,
`
[http.services]
{{ range $i, $e := until 20 }}
[http.services.application-{{ $e }}]
[[http.services.application-{{ $e }}.servers]]
url="http://127.0.0.1"
weight = 1
{{ end }}
`,
},
expectedNumRouter: 20,
expectedNumService: 20,
},
{
desc: "simple traefik file",
traefikFileContent: `
debug=true
[providers.file]
` + createRoutersConfiguration(2) + createServicesConfiguration(3) + createTLS(4),
expectedNumRouter: 2,
expectedNumService: 3,
expectedNumTLSConf: 4,
},
{
desc: "simple traefik file with templating",
traefikFileContent: `
temp="{{ getTag \"test\" }}"
[providers.file]
` + createRoutersConfiguration(2) + createServicesConfiguration(3) + createTLS(4),
expectedNumRouter: 2,
expectedNumService: 3,
expectedNumTLSConf: 4,
},
}
}
func createProvider(t *testing.T, test ProvideTestCase, watch bool) (*Provider, func()) {
tempDir := createTempDir(t, "testdir")
provider := &Provider{}
provider.Watch = watch
if len(test.directoryContent) > 0 {
if !watch {
for _, fileContent := range test.directoryContent {
createRandomFile(t, tempDir, fileContent)
}
}
provider.Directory = tempDir
}
if len(test.fileContent) > 0 {
if watch {
test.fileContent = ""
}
filename := createRandomFile(t, tempDir, test.fileContent)
provider.Filename = filename.Name()
}
if len(test.traefikFileContent) > 0 {
if watch {
test.traefikFileContent = ""
}
filename := createRandomFile(t, tempDir, test.traefikFileContent)
provider.TraefikFile = filename.Name()
}
return provider, func() {
os.Remove(tempDir)
}
}
// createRandomFile Helper
func createRandomFile(t *testing.T, tempDir string, contents ...string) *os.File {
return createFile(t, tempDir, fmt.Sprintf("temp%d.toml", time.Now().UnixNano()), contents...)
}
// createFile Helper
func createFile(t *testing.T, tempDir string, name string, contents ...string) *os.File {
t.Helper()
fileName := path.Join(tempDir, name)
tempFile, err := os.Create(fileName)
if err != nil {
t.Fatal(err)
}
for _, content := range contents {
_, err = tempFile.WriteString(content)
if err != nil {
t.Fatal(err)
}
}
err = tempFile.Close()
if err != nil {
t.Fatal(err)
}
return tempFile
}
// createTempDir Helper
func createTempDir(t *testing.T, dir string) string {
t.Helper()
d, err := ioutil.TempDir("", dir)
if err != nil {
t.Fatal(err)
}
return d
}
// createRoutersConfiguration Helper
func createRoutersConfiguration(n int) string {
conf := "[http.routers]\n"
for i := 1; i <= n; i++ {
conf += fmt.Sprintf(`
[http.routers."router%[1]d"]
service = "application-%[1]d"
`, i)
}
return conf
}
// createServicesConfiguration Helper
func createServicesConfiguration(n int) string {
conf := "[http.services]\n"
for i := 1; i <= n; i++ {
conf += fmt.Sprintf(`
[http.services.application-%[1]d.loadbalancer]
[[http.services.application-%[1]d.loadbalancer.servers]]
url = "http://172.17.0.%[1]d:80"
weight = 1
`, i)
}
return conf
}
// createTLS Helper
func createTLS(n int) string {
var conf string
for i := 1; i <= n; i++ {
conf += fmt.Sprintf(`[[TLS]]
EntryPoints = ["https"]
[TLS.Certificate]
CertFile = "integration/fixtures/https/snitest%[1]d.com.cert"
KeyFile = "integration/fixtures/https/snitest%[1]d.com.key"
`, i)
}
return conf
}
func TestTLSContent(t *testing.T) {
tempDir := createTempDir(t, "testdir")
defer os.Remove(tempDir)
fileTLS := createRandomFile(t, tempDir, "CONTENT")
fileConfig := createRandomFile(t, tempDir, `
[[tls]]
entryPoints = ["https"]
[tls.certificate]
certFile = "`+fileTLS.Name()+`"
keyFile = "`+fileTLS.Name()+`"
`)
provider := &Provider{}
configuration, err := provider.loadFileConfig(fileConfig.Name(), true)
require.NoError(t, err)
require.Equal(t, "CONTENT", configuration.TLS[0].Certificate.CertFile.String())
require.Equal(t, "CONTENT", configuration.TLS[0].Certificate.KeyFile.String())
}