Dynamic Configuration Refactoring
This commit is contained in:
parent
d3ae88f108
commit
a09dfa3ce1
452 changed files with 21023 additions and 9419 deletions
217
old/provider/dynamodb/dynamodb.go
Normal file
217
old/provider/dynamodb/dynamodb.go
Normal file
|
@ -0,0 +1,217 @@
|
|||
package dynamodb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/defaults"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/dynamodb"
|
||||
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
|
||||
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface"
|
||||
"github.com/cenk/backoff"
|
||||
"github.com/containous/traefik/job"
|
||||
"github.com/containous/traefik/old/log"
|
||||
"github.com/containous/traefik/old/provider"
|
||||
"github.com/containous/traefik/old/types"
|
||||
"github.com/containous/traefik/safe"
|
||||
)
|
||||
|
||||
var _ provider.Provider = (*Provider)(nil)
|
||||
|
||||
// Provider holds configuration for provider.
|
||||
type Provider struct {
|
||||
provider.BaseProvider `mapstructure:",squash" export:"true"`
|
||||
AccessKeyID string `description:"The AWS credentials access key to use for making requests"`
|
||||
RefreshSeconds int `description:"Polling interval (in seconds)" export:"true"`
|
||||
Region string `description:"The AWS region to use for requests" export:"true"`
|
||||
SecretAccessKey string `description:"The AWS credentials secret key to use for making requests"`
|
||||
TableName string `description:"The AWS dynamodb table that stores configuration for traefik" export:"true"`
|
||||
Endpoint string `description:"The endpoint of a dynamodb. Used for testing with a local dynamodb"`
|
||||
}
|
||||
|
||||
type dynamoClient struct {
|
||||
db dynamodbiface.DynamoDBAPI
|
||||
}
|
||||
|
||||
// Init the provider
|
||||
func (p *Provider) Init(constraints types.Constraints) error {
|
||||
return p.BaseProvider.Init(constraints)
|
||||
}
|
||||
|
||||
// createClient configures aws credentials and creates a dynamoClient
|
||||
func (p *Provider) createClient() (*dynamoClient, error) {
|
||||
log.Info("Creating Provider client...")
|
||||
sess, err := session.NewSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if p.Region == "" {
|
||||
return nil, errors.New("no Region provided for Provider")
|
||||
}
|
||||
cfg := &aws.Config{
|
||||
Region: &p.Region,
|
||||
Credentials: credentials.NewChainCredentials(
|
||||
[]credentials.Provider{
|
||||
&credentials.StaticProvider{
|
||||
Value: credentials.Value{
|
||||
AccessKeyID: p.AccessKeyID,
|
||||
SecretAccessKey: p.SecretAccessKey,
|
||||
},
|
||||
},
|
||||
&credentials.EnvProvider{},
|
||||
&credentials.SharedCredentialsProvider{},
|
||||
defaults.RemoteCredProvider(*(defaults.Config()), defaults.Handlers()),
|
||||
}),
|
||||
}
|
||||
|
||||
if p.Trace {
|
||||
cfg.WithLogger(aws.LoggerFunc(func(args ...interface{}) {
|
||||
log.Debug(args...)
|
||||
}))
|
||||
}
|
||||
|
||||
if p.Endpoint != "" {
|
||||
cfg.Endpoint = aws.String(p.Endpoint)
|
||||
}
|
||||
|
||||
return &dynamoClient{
|
||||
db: dynamodb.New(sess, cfg),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// scanTable scans the given table and returns slice of all items in the table
|
||||
func (p *Provider) scanTable(client *dynamoClient) ([]map[string]*dynamodb.AttributeValue, error) {
|
||||
log.Debugf("Scanning Provider table: %s ...", p.TableName)
|
||||
params := &dynamodb.ScanInput{
|
||||
TableName: aws.String(p.TableName),
|
||||
}
|
||||
items := make([]map[string]*dynamodb.AttributeValue, 0)
|
||||
err := client.db.ScanPages(params,
|
||||
func(page *dynamodb.ScanOutput, lastPage bool) bool {
|
||||
items = append(items, page.Items...)
|
||||
return !lastPage
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("Failed to scan Provider table %s", p.TableName)
|
||||
return nil, err
|
||||
}
|
||||
log.Debugf("Successfully scanned Provider table %s", p.TableName)
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// buildConfiguration retrieves items from dynamodb and converts them into Backends and Frontends in a Configuration
|
||||
func (p *Provider) buildConfiguration(client *dynamoClient) (*types.Configuration, error) {
|
||||
items, err := p.scanTable(client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debugf("Number of Items retrieved from Provider: %d", len(items))
|
||||
backends := make(map[string]*types.Backend)
|
||||
frontends := make(map[string]*types.Frontend)
|
||||
// unmarshal dynamoAttributes into Backends and Frontends
|
||||
for i, item := range items {
|
||||
log.Debugf("Provider Item: %d\n%v", i, item)
|
||||
// verify the type of each item by checking to see if it has
|
||||
// the corresponding type, backend or frontend map
|
||||
if backend, exists := item["backend"]; exists {
|
||||
log.Debug("Unmarshaling backend from Provider...")
|
||||
tmpBackend := &types.Backend{}
|
||||
err = dynamodbattribute.Unmarshal(backend, tmpBackend)
|
||||
if err != nil {
|
||||
log.Errorf(err.Error())
|
||||
} else {
|
||||
backends[*item["name"].S] = tmpBackend
|
||||
log.Debug("Backend from Provider unmarshalled successfully")
|
||||
}
|
||||
} else if frontend, exists := item["frontend"]; exists {
|
||||
log.Debugf("Unmarshaling frontend from Provider...")
|
||||
tmpFrontend := &types.Frontend{}
|
||||
err = dynamodbattribute.Unmarshal(frontend, tmpFrontend)
|
||||
if err != nil {
|
||||
log.Errorf(err.Error())
|
||||
} else {
|
||||
frontends[*item["name"].S] = tmpFrontend
|
||||
log.Debug("Frontend from Provider unmarshalled successfully")
|
||||
}
|
||||
} else {
|
||||
log.Warnf("Error in format of Provider Item: %v", item)
|
||||
}
|
||||
}
|
||||
|
||||
return &types.Configuration{
|
||||
Backends: backends,
|
||||
Frontends: frontends,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Provide provides the configuration to traefik via the configuration channel
|
||||
// if watch is enabled it polls dynamodb
|
||||
func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool) error {
|
||||
handleCanceled := func(ctx context.Context, err error) error {
|
||||
if ctx.Err() == context.Canceled || err == context.Canceled {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
pool.Go(func(stop chan bool) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
safe.Go(func() {
|
||||
<-stop
|
||||
cancel()
|
||||
})
|
||||
|
||||
operation := func() error {
|
||||
awsClient, err := p.createClient()
|
||||
if err != nil {
|
||||
return handleCanceled(ctx, err)
|
||||
}
|
||||
|
||||
configuration, err := p.buildConfiguration(awsClient)
|
||||
if err != nil {
|
||||
return handleCanceled(ctx, err)
|
||||
}
|
||||
|
||||
configurationChan <- types.ConfigMessage{
|
||||
ProviderName: "dynamodb",
|
||||
Configuration: configuration,
|
||||
}
|
||||
|
||||
if p.Watch {
|
||||
reload := time.NewTicker(time.Second * time.Duration(p.RefreshSeconds))
|
||||
defer reload.Stop()
|
||||
for {
|
||||
log.Debug("Watching Provider...")
|
||||
select {
|
||||
case <-reload.C:
|
||||
configuration, err := p.buildConfiguration(awsClient)
|
||||
if err != nil {
|
||||
return handleCanceled(ctx, err)
|
||||
}
|
||||
|
||||
configurationChan <- types.ConfigMessage{
|
||||
ProviderName: "dynamodb",
|
||||
Configuration: configuration,
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return handleCanceled(ctx, ctx.Err())
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
notify := func(err error, time time.Duration) {
|
||||
log.Errorf("Provider error: %s time: %v", err.Error(), time)
|
||||
}
|
||||
|
||||
err := backoff.RetryNotify(safe.OperationWithRecover(operation), job.NewBackOff(backoff.NewExponentialBackOff()), notify)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to connect to Provider. %s", err.Error())
|
||||
}
|
||||
})
|
||||
return nil
|
||||
}
|
140
old/provider/dynamodb/dynamodb_test.go
Normal file
140
old/provider/dynamodb/dynamodb_test.go
Normal file
|
@ -0,0 +1,140 @@
|
|||
package dynamodb
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/dynamodb"
|
||||
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
|
||||
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface"
|
||||
"github.com/containous/traefik/old/types"
|
||||
)
|
||||
|
||||
type mockDynamoDBClient struct {
|
||||
dynamodbiface.DynamoDBAPI
|
||||
testWithError bool
|
||||
}
|
||||
|
||||
var backend = &types.Backend{
|
||||
HealthCheck: &types.HealthCheck{
|
||||
Path: "/build",
|
||||
},
|
||||
Servers: map[string]types.Server{
|
||||
"server1": {
|
||||
URL: "http://test.traefik.io",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var frontend = &types.Frontend{
|
||||
EntryPoints: []string{"http"},
|
||||
Backend: "test.traefik.io",
|
||||
Routes: map[string]types.Route{
|
||||
"route1": {
|
||||
Rule: "Host:test.traefik.io",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// ScanPages simulates a call to ScanPages (see https://docs.aws.amazon.com/sdk-for-go/api/service/dynamodb/#DynamoDB.ScanPages)
|
||||
// by running the fn function twice and returning an item each time.
|
||||
func (m *mockDynamoDBClient) ScanPages(input *dynamodb.ScanInput, fn func(*dynamodb.ScanOutput, bool) bool) error {
|
||||
if m.testWithError {
|
||||
return errors.New("fake error")
|
||||
}
|
||||
attributeBackend, err := dynamodbattribute.Marshal(backend)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attributeFrontend, err := dynamodbattribute.Marshal(frontend)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fn(&dynamodb.ScanOutput{
|
||||
Items: []map[string]*dynamodb.AttributeValue{
|
||||
{
|
||||
"id": &dynamodb.AttributeValue{
|
||||
S: aws.String("test.traefik.io_backend"),
|
||||
},
|
||||
"name": &dynamodb.AttributeValue{
|
||||
S: aws.String("test.traefik.io"),
|
||||
},
|
||||
"backend": attributeBackend,
|
||||
},
|
||||
},
|
||||
}, false)
|
||||
|
||||
fn(&dynamodb.ScanOutput{
|
||||
Items: []map[string]*dynamodb.AttributeValue{
|
||||
{
|
||||
"id": &dynamodb.AttributeValue{
|
||||
S: aws.String("test.traefik.io_frontend"),
|
||||
},
|
||||
"name": &dynamodb.AttributeValue{
|
||||
S: aws.String("test.traefik.io"),
|
||||
},
|
||||
"frontend": attributeFrontend,
|
||||
},
|
||||
},
|
||||
}, true)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestBuildConfigurationSuccessful(t *testing.T) {
|
||||
dbiface := &dynamoClient{
|
||||
db: &mockDynamoDBClient{
|
||||
testWithError: false,
|
||||
},
|
||||
}
|
||||
provider := Provider{}
|
||||
loadedConfig, err := provider.buildConfiguration(dbiface)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expectedConfig := &types.Configuration{
|
||||
Backends: map[string]*types.Backend{
|
||||
"test.traefik.io": backend,
|
||||
},
|
||||
Frontends: map[string]*types.Frontend{
|
||||
"test.traefik.io": frontend,
|
||||
},
|
||||
}
|
||||
if !reflect.DeepEqual(loadedConfig, expectedConfig) {
|
||||
t.Fatalf("Configurations did not match: %v %v", loadedConfig, expectedConfig)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildConfigurationFailure(t *testing.T) {
|
||||
dbiface := &dynamoClient{
|
||||
db: &mockDynamoDBClient{
|
||||
testWithError: true,
|
||||
},
|
||||
}
|
||||
provider := Provider{}
|
||||
_, err := provider.buildConfiguration(dbiface)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateClientSuccessful(t *testing.T) {
|
||||
provider := Provider{
|
||||
Region: "us-east-1",
|
||||
}
|
||||
_, err := provider.createClient()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateClientFailure(t *testing.T) {
|
||||
provider := Provider{}
|
||||
_, err := provider.createClient()
|
||||
if err == nil {
|
||||
t.Fatal("Expected error")
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue