Skip to content

Commit

Permalink
Publish externalIPs of Nodes
Browse files Browse the repository at this point in the history
  • Loading branch information
coufalja committed Jan 24, 2020
1 parent 4be51a9 commit 51d536b
Show file tree
Hide file tree
Showing 5 changed files with 283 additions and 3 deletions.
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ func main() {
Compatibility: cfg.Compatibility,
PublishInternal: cfg.PublishInternal,
PublishHostIP: cfg.PublishHostIP,
PublishHostExternalIP: cfg.PublishHostExternalIP,
ConnectorServer: cfg.ConnectorSourceServer,
CRDSourceAPIVersion: cfg.CRDSourceAPIVersion,
CRDSourceKind: cfg.CRDSourceKind,
Expand Down
2 changes: 2 additions & 0 deletions pkg/apis/externaldns/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ type Config struct {
Compatibility string
PublishInternal bool
PublishHostIP bool
PublishHostExternalIP bool
ConnectorSourceServer string
Provider string
GoogleProject string
Expand Down Expand Up @@ -283,6 +284,7 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("compatibility", "Process annotation semantics from legacy implementations (optional, options: mate, molecule)").Default(defaultConfig.Compatibility).EnumVar(&cfg.Compatibility, "", "mate", "molecule")
app.Flag("publish-internal-services", "Allow external-dns to publish DNS records for ClusterIP services (optional)").BoolVar(&cfg.PublishInternal)
app.Flag("publish-host-ip", "Allow external-dns to publish host-ip for headless services (optional)").BoolVar(&cfg.PublishHostIP)
app.Flag("publish-host-external-ip", "Allow external-dns to publish external host-ip for headless services, valid only when publish-host-ip is false (optional)").BoolVar(&cfg.PublishHostExternalIP)
app.Flag("connector-source-server", "The server to connect for connector source, valid only when using connector source").Default(defaultConfig.ConnectorSourceServer).StringVar(&cfg.ConnectorSourceServer)
app.Flag("crd-source-apiversion", "API version of the CRD for crd source, e.g. `externaldns.k8s.io/v1alpha1`, valid only when using crd source").Default(defaultConfig.CRDSourceAPIVersion).StringVar(&cfg.CRDSourceAPIVersion)
app.Flag("crd-source-kind", "Kind of the CRD for the crd source in API group and version specified by crd-source-apiversion").Default(defaultConfig.CRDSourceKind).StringVar(&cfg.CRDSourceKind)
Expand Down
22 changes: 21 additions & 1 deletion source/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,15 @@ type serviceSource struct {
ignoreHostnameAnnotation bool
publishInternal bool
publishHostIP bool
publishHostExternalIP bool
serviceInformer coreinformers.ServiceInformer
podInformer coreinformers.PodInformer
nodeInformer coreinformers.NodeInformer
serviceTypeFilter map[string]struct{}
}

// NewServiceSource creates a new serviceSource with the given config.
func NewServiceSource(kubeClient kubernetes.Interface, namespace, annotationFilter string, fqdnTemplate string, combineFqdnAnnotation bool, compatibility string, publishInternal bool, publishHostIP bool, serviceTypeFilter []string, ignoreHostnameAnnotation bool) (Source, error) {
func NewServiceSource(kubeClient kubernetes.Interface, namespace, annotationFilter string, fqdnTemplate string, combineFqdnAnnotation bool, compatibility string, publishInternal bool, publishHostIP bool, publishHostExternalIP bool, serviceTypeFilter []string, ignoreHostnameAnnotation bool) (Source, error) {
var (
tmpl *template.Template
err error
Expand Down Expand Up @@ -136,6 +137,7 @@ func NewServiceSource(kubeClient kubernetes.Interface, namespace, annotationFilt
ignoreHostnameAnnotation: ignoreHostnameAnnotation,
publishInternal: publishInternal,
publishHostIP: publishHostIP,
publishHostExternalIP: publishHostExternalIP,
serviceInformer: serviceInformer,
podInformer: podInformer,
nodeInformer: nodeInformer,
Expand Down Expand Up @@ -242,6 +244,24 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri
} else {
log.Debugf("Pod %s is not in running phase", v.Spec.Hostname)
}
} else if sc.publishHostExternalIP == true {
node, err := sc.nodeInformer.Lister().Get(v.Spec.NodeName)
if err != nil {
return nil
}
var externalIPs endpoint.Targets
for _, address := range node.Status.Addresses {
if address.Type == v1.NodeExternalIP {
externalIPs = append(externalIPs, address.Address)
}
}

log.Debugf("Generating matching endpoint %s with Host ExternalIPs %v", headlessDomain, externalIPs)
if v.Status.Phase == v1.PodRunning {
targetsByHeadlessDomain[headlessDomain] = append(targetsByHeadlessDomain[headlessDomain], externalIPs...)
} else {
log.Debugf("Pod %s is not in running phase", v.Spec.Hostname)
}
} else {
log.Debugf("Generating matching endpoint %s with PodIP %s", headlessDomain, v.Status.PodIP)
// To reduce traffice on the DNS API only add record for running Pods. Good Idea?
Expand Down
258 changes: 257 additions & 1 deletion source/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ func (suite *ServiceSuite) SetupTest() {
"",
false,
false,
false,
[]string{},
false,
)
Expand Down Expand Up @@ -143,6 +144,7 @@ func testServiceSourceNewServiceSource(t *testing.T) {
"",
false,
false,
false,
ti.serviceTypesFilter,
false,
)
Expand Down Expand Up @@ -1083,6 +1085,7 @@ func testServiceSourceEndpoints(t *testing.T) {
tc.compatibility,
false,
false,
false,
tc.serviceTypesFilter,
tc.ignoreHostnameAnnotation,
)
Expand Down Expand Up @@ -1253,6 +1256,7 @@ func TestClusterIpServices(t *testing.T) {
tc.compatibility,
true,
false,
false,
[]string{},
tc.ignoreHostnameAnnotation,
)
Expand Down Expand Up @@ -1584,6 +1588,7 @@ func TestNodePortServices(t *testing.T) {
tc.compatibility,
true,
false,
false,
[]string{},
tc.ignoreHostnameAnnotation,
)
Expand Down Expand Up @@ -1818,6 +1823,7 @@ func TestHeadlessServices(t *testing.T) {
tc.compatibility,
true,
false,
false,
[]string{},
tc.ignoreHostnameAnnotation,
)
Expand Down Expand Up @@ -2052,6 +2058,255 @@ func TestHeadlessServicesHostIP(t *testing.T) {
tc.compatibility,
true,
true,
false,
[]string{},
tc.ignoreHostnameAnnotation,
)
require.NoError(t, err)

endpoints, err := client.Endpoints()
if tc.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
}

// Validate returned endpoints against desired endpoints.
validateEndpoints(t, endpoints, tc.expected)
})
}
}

// TestHeadlessServicesExternalHostIP tests that headless services generate the correct endpoints.
func TestHeadlessServicesExternalHostIP(t *testing.T) {
for _, tc := range []struct {
title string
targetNamespace string
svcNamespace string
svcName string
svcType v1.ServiceType
compatibility string
fqdnTemplate string
ignoreHostnameAnnotation bool
labels map[string]string
annotations map[string]string
clusterIP string
hostIPs []string
selector map[string]string
lbs []string
podnames []string
hostnames []string
phases []v1.PodPhase
expected []*endpoint.Endpoint
expectError bool
}{
{
"annotated Headless services return endpoints for each selected Pod",
"",
"testing",
"foo",
v1.ServiceTypeClusterIP,
"",
"",
false,
map[string]string{"component": "foo"},
map[string]string{
hostnameAnnotationKey: "service.example.org",
},
v1.ClusterIPNone,
[]string{"1.1.1.1", "1.1.1.2"},
map[string]string{
"component": "foo",
},
[]string{},
[]string{"foo-0", "foo-1"},
[]string{"foo-0", "foo-1"},
[]v1.PodPhase{v1.PodRunning, v1.PodRunning},
[]*endpoint.Endpoint{
{DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}},
{DNSName: "foo-1.service.example.org", Targets: endpoint.Targets{"1.1.1.2"}},
{DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}},
},
false,
},
{
"hostname annotated Headless services are ignored",
"",
"testing",
"foo",
v1.ServiceTypeClusterIP,
"",
"",
true,
map[string]string{"component": "foo"},
map[string]string{
hostnameAnnotationKey: "service.example.org",
},
v1.ClusterIPNone,
[]string{"1.1.1.1", "1.1.1.2"},
map[string]string{
"component": "foo",
},
[]string{},
[]string{"foo-0", "foo-1"},
[]string{"foo-0", "foo-1"},
[]v1.PodPhase{v1.PodRunning, v1.PodRunning},
[]*endpoint.Endpoint{},
false,
},
{
"annotated Headless services return endpoints with TTL for each selected Pod",
"",
"testing",
"foo",
v1.ServiceTypeClusterIP,
"",
"",
false,
map[string]string{"component": "foo"},
map[string]string{
hostnameAnnotationKey: "service.example.org",
ttlAnnotationKey: "1",
},
v1.ClusterIPNone,
[]string{"1.1.1.1", "1.1.1.2"},
map[string]string{
"component": "foo",
},
[]string{},
[]string{"foo-0", "foo-1"},
[]string{"foo-0", "foo-1"},
[]v1.PodPhase{v1.PodRunning, v1.PodRunning},
[]*endpoint.Endpoint{
{DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}, RecordTTL: endpoint.TTL(1)},
{DNSName: "foo-1.service.example.org", Targets: endpoint.Targets{"1.1.1.2"}, RecordTTL: endpoint.TTL(1)},
{DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}, RecordTTL: endpoint.TTL(1)},
},
false,
},
{
"annotated Headless services return endpoints for each selected Pod, which are in running state",
"",
"testing",
"foo",
v1.ServiceTypeClusterIP,
"",
"",
false,
map[string]string{"component": "foo"},
map[string]string{
hostnameAnnotationKey: "service.example.org",
},
v1.ClusterIPNone,
[]string{"1.1.1.1", "1.1.1.2"},
map[string]string{
"component": "foo",
},
[]string{},
[]string{"foo-0", "foo-1"},
[]string{"foo-0", "foo-1"},
[]v1.PodPhase{v1.PodRunning, v1.PodFailed},
[]*endpoint.Endpoint{
{DNSName: "foo-0.service.example.org", Targets: endpoint.Targets{"1.1.1.1"}},
{DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1"}},
},
false,
},
{
"annotated Headless services return endpoints for pods missing hostname",
"",
"testing",
"foo",
v1.ServiceTypeClusterIP,
"",
"",
false,
map[string]string{"component": "foo"},
map[string]string{
hostnameAnnotationKey: "service.example.org",
},
v1.ClusterIPNone,
[]string{"1.1.1.1", "1.1.1.2"},
map[string]string{
"component": "foo",
},
[]string{},
[]string{"foo-0", "foo-1"},
[]string{"", ""},
[]v1.PodPhase{v1.PodRunning, v1.PodRunning},
[]*endpoint.Endpoint{
{DNSName: "service.example.org", Targets: endpoint.Targets{"1.1.1.1", "1.1.1.2"}},
},
false,
},
} {
t.Run(tc.title, func(t *testing.T) {
// Create a Kubernetes testing client
kubernetes := fake.NewSimpleClientset()

service := &v1.Service{
Spec: v1.ServiceSpec{
Type: tc.svcType,
ClusterIP: tc.clusterIP,
Selector: tc.selector,
},
ObjectMeta: metav1.ObjectMeta{
Namespace: tc.svcNamespace,
Name: tc.svcName,
Labels: tc.labels,
Annotations: tc.annotations,
},
Status: v1.ServiceStatus{},
}
_, err := kubernetes.CoreV1().Services(service.Namespace).Create(service)
require.NoError(t, err)

for i, podname := range tc.podnames {
node := &v1.Node{
ObjectMeta: metav1.ObjectMeta{
Name: "node-" + podname,
},
Spec: v1.NodeSpec{},
Status: v1.NodeStatus{
Addresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: tc.hostIPs[i]}},
},
}
_, err = kubernetes.CoreV1().Nodes().Create(node)
require.NoError(t, err)

pod := &v1.Pod{
Spec: v1.PodSpec{
Containers: []v1.Container{},
Hostname: tc.hostnames[i],
NodeName: node.Name,
},
ObjectMeta: metav1.ObjectMeta{
Namespace: tc.svcNamespace,
Name: podname,
Labels: tc.labels,
Annotations: tc.annotations,
},
Status: v1.PodStatus{
HostIP: tc.hostIPs[i],
Phase: tc.phases[i],
},
}

_, err = kubernetes.CoreV1().Pods(tc.svcNamespace).Create(pod)
require.NoError(t, err)
}

// Create our object under test and get the endpoints.
client, _ := NewServiceSource(
kubernetes,
tc.targetNamespace,
"",
tc.fqdnTemplate,
false,
tc.compatibility,
true,
false,
true,
[]string{},
tc.ignoreHostnameAnnotation,
)
Expand Down Expand Up @@ -2156,6 +2411,7 @@ func TestExternalServices(t *testing.T) {
tc.compatibility,
true,
false,
false,
[]string{},
tc.ignoreHostnameAnnotation,
)
Expand Down Expand Up @@ -2198,7 +2454,7 @@ func BenchmarkServiceEndpoints(b *testing.B) {
_, err := kubernetes.CoreV1().Services(service.Namespace).Create(service)
require.NoError(b, err)

client, err := NewServiceSource(kubernetes, v1.NamespaceAll, "", "", false, "", false, false, []string{}, false)
client, err := NewServiceSource(kubernetes, v1.NamespaceAll, "", "", false, "", false, false, false, []string{}, false)
require.NoError(b, err)

for i := 0; i < b.N; i++ {
Expand Down
Loading

0 comments on commit 51d536b

Please sign in to comment.