diff --git a/main.go b/main.go index a19d1ae58..144586de8 100644 --- a/main.go +++ b/main.go @@ -66,22 +66,39 @@ func main() { go serveMetrics(cfg.MetricsAddress) go handleSigterm(stopChan) - client, err := newClient(cfg) - if err != nil { - log.Fatal(err) - } + var client *kubernetes.Clientset - serviceSource, err := source.NewServiceSource(client, cfg.Namespace, cfg.FqdnTemplate, cfg.Compatibility) - if err != nil { - log.Fatal(err) - } - source.Register("service", serviceSource) + // create only those services we explicitly ask for in cfg.Sources + for _, sourceType := range cfg.Sources { + // we only need a k8s client if we're creating a non-fake source, and + // have not already instantiated a k8s client + if sourceType != "fake" && client == nil { + var err error + client, err = newClient(cfg) + if err != nil { + log.Fatal(err) + } + } - ingressSource, err := source.NewIngressSource(client, cfg.Namespace, cfg.FqdnTemplate) - if err != nil { - log.Fatal(err) + var src source.Source + var err error + switch sourceType { + case "fake": + src, err = source.NewFakeSource(cfg.FqdnTemplate) + case "service": + src, err = source.NewServiceSource(client, cfg.Namespace, cfg.FqdnTemplate, cfg.Compatibility) + case "ingress": + src, err = source.NewIngressSource(client, cfg.Namespace, cfg.FqdnTemplate) + default: + log.Fatalf("Don't know how to handle sourceType '%s'", sourceType) + } + + if err != nil { + log.Fatal(err) + } + + source.Register(sourceType, src) } - source.Register("ingress", ingressSource) sources, err := source.LookupMultiple(cfg.Sources) if err != nil { @@ -96,6 +113,8 @@ func main() { p, err = provider.NewGoogleProvider(cfg.GoogleProject, cfg.DomainFilter, cfg.DryRun) case "aws": p, err = provider.NewAWSProvider(cfg.DomainFilter, cfg.DryRun) + case "inmemory": + p, err = provider.NewInMemoryProviderWithDomainAndLogging("example.com"), nil default: log.Fatalf("unknown dns provider: %s", cfg.Provider) } diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index e3a9fcd09..236b08a2c 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -87,13 +87,13 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("kubeconfig", "Retrieve target cluster configuration from a Kubernetes configuration file (default: auto-detect)").Default(defaultConfig.KubeConfig).StringVar(&cfg.KubeConfig) // Flags related to processing sources - app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress") + app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, fake)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "fake") app.Flag("namespace", "Limit sources of endpoints to a specific namespace (default: all namespaces)").Default(defaultConfig.Namespace).StringVar(&cfg.Namespace) - app.Flag("fqdn-template", "A templated string that's used to generate DNS names from sources that don't define a hostname themselves (optional)").Default(defaultConfig.FqdnTemplate).StringVar(&cfg.FqdnTemplate) + app.Flag("fqdn-template", "A templated string that's used to generate DNS names from sources that don't define a hostname themselves, or to add a hostname suffix when paired with the fake source (optional)").Default(defaultConfig.FqdnTemplate).StringVar(&cfg.FqdnTemplate) app.Flag("compatibility", "Process annotation semantics from legacy implementations (optional, options: mate, molecule)").Default(defaultConfig.Compatibility).EnumVar(&cfg.Compatibility, "", "mate", "molecule") // Flags related to providers - app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, google)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "google") + app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, google, inmemory)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "google", "inmemory") app.Flag("google-project", "When using the Google provider, specify the Google project (required when --provider=google)").Default(defaultConfig.GoogleProject).StringVar(&cfg.GoogleProject) app.Flag("domain-filter", "Limit possible target zones by a domain suffix (optional)").Default(defaultConfig.DomainFilter).StringVar(&cfg.DomainFilter) diff --git a/provider/inmemory.go b/provider/inmemory.go index b6acae430..9ed4b471b 100644 --- a/provider/inmemory.go +++ b/provider/inmemory.go @@ -20,6 +20,8 @@ import ( "errors" "strings" + log "github.com/Sirupsen/logrus" + "github.com/kubernetes-incubator/external-dns/endpoint" "github.com/kubernetes-incubator/external-dns/plan" ) @@ -58,6 +60,35 @@ func NewInMemoryProvider() *InMemoryProvider { } } +// NewInMemoryProviderWithDomainAndLogging returns InMemoryProvider DNS provider interface +// implementation with a specified domain +func NewInMemoryProviderWithDomainAndLogging(domain string) *InMemoryProvider { + im := &InMemoryProvider{ + filter: &filter{}, + OnApplyChanges: func(changes *plan.Changes) { + for _, v := range changes.Create { + log.Infof("CREATE: %v", v) + } + for _, v := range changes.UpdateOld { + log.Infof("UPDATE (old): %v", v) + } + for _, v := range changes.UpdateNew { + log.Infof("UPDATE (new): %v", v) + } + for _, v := range changes.Delete { + log.Infof("DELETE: %v", v) + } + }, + OnRecords: func() {}, + domain: domain, + client: newInMemoryClient(), + } + + im.CreateZone(domain) + + return im +} + // CreateZone adds new zone if not present func (im *InMemoryProvider) CreateZone(newZone string) error { return im.client.CreateZone(newZone) diff --git a/source/fake.go b/source/fake.go new file mode 100644 index 000000000..7abccc123 --- /dev/null +++ b/source/fake.go @@ -0,0 +1,102 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +Note: currently only supports IP targets (A records), not hostname targets +*/ + +package source + +import ( + "fmt" + "math/rand" + "net" + "time" + + "github.com/kubernetes-incubator/external-dns/endpoint" +) + +// fakeSource is an implementation of Source for that provides dummy endpoints +// for testing/dry-running of dns providers without needing an attached +// kubernetes cluster. + +type fakeSource struct { + dnsName string +} + +const ( + defaultDNSName = "example.com" +) + +func init() { + rand.Seed(time.Now().UnixNano()) +} + +// NewFakeSource creates a new fakeSource with the given client and namespace scope. +func NewFakeSource(dnsName string) (Source, error) { + if dnsName == "" { + dnsName = defaultDNSName + } + + return &fakeSource{ + dnsName: dnsName, + }, nil +} + +// Endpoints returns endpoint objects +func (sc *fakeSource) Endpoints() ([]*endpoint.Endpoint, error) { + endpoints := make([]*endpoint.Endpoint, 10) + + for i := 0; i < 10; i++ { + endpoints[i], _ = sc.generateEndpoint() + } + + return endpoints, nil +} + +func (sc *fakeSource) generateEndpoint() (*endpoint.Endpoint, error) { + endpoint := endpoint.NewEndpoint( + generateDNSName(4, sc.dnsName), + generateIPAddress(), + "A", + ) + + return endpoint, nil +} + +func generateIPAddress() string { + // 192.0.2.[1-255] is reserved by RFC 5737 for documentation and examples + return net.IPv4( + byte(192), + byte(0), + byte(2), + byte(rand.Intn(253)+1), + ).String() +} + +var letterRunes = []rune("abcdefghijklmnopqrstuvwxyz") + +func generateDNSName(prefixLength int, dnsName string) string { + prefixBytes := make([]rune, prefixLength) + + for i := range prefixBytes { + prefixBytes[i] = letterRunes[rand.Intn(len(letterRunes))] + } + + prefixStr := string(prefixBytes) + + return fmt.Sprintf("%s.%s", prefixStr, dnsName) +} diff --git a/source/fake_test.go b/source/fake_test.go new file mode 100644 index 000000000..1ebf6c7be --- /dev/null +++ b/source/fake_test.go @@ -0,0 +1,72 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package source + +import ( + "net" + "regexp" + "testing" + + "github.com/kubernetes-incubator/external-dns/endpoint" +) + +func generateTestEndpoints() []*endpoint.Endpoint { + sc, _ := NewFakeSource("") + + endpoints, _ := sc.Endpoints() + + return endpoints +} + +func TestFakeSourceReturnsTenEndpoints(t *testing.T) { + endpoints := generateTestEndpoints() + + count := len(endpoints) + + if count != 10 { + t.Error(count) + } +} + +func TestFakeEndpointsBelongToDomain(t *testing.T) { + validRecord := regexp.MustCompile(`^[a-z]{4}\.example\.com$`) + + endpoints := generateTestEndpoints() + + for _, e := range endpoints { + valid := validRecord.MatchString(e.DNSName) + + if !valid { + t.Error(e.DNSName) + } + } +} + +func TestFakeEndpointsResolveToIPAddresses(t *testing.T) { + endpoints := generateTestEndpoints() + + for _, e := range endpoints { + ip := net.ParseIP(e.Target) + + if ip == nil { + t.Error(e) + } + } +} + +// Validate that FakeSource is a source +var _ Source = &fakeSource{}