mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-06 01:26:59 +02:00
Add pod source
Pod source is a key feature of kOps' DNS Controller. Among other things, i is used for etcd and API discovery.
This commit is contained in:
parent
10d0ee1c81
commit
5a46584221
@ -282,6 +282,9 @@ Here's a rough outline on what is to come (subject to change):
|
||||
### v1.0
|
||||
|
||||
- [ ] Ability to replace Kops' [DNS Controller](https://github.com/kubernetes/kops/tree/HEAD/dns-controller)
|
||||
- [x] Add support for pod source
|
||||
- [ ] Add support for DNS Controller annotations for pod, ingress, and service sources
|
||||
- [ ] Add support for kOps gossip provider
|
||||
- [x] Ability to replace Zalando's [Mate](https://github.com/linki/mate)
|
||||
- [x] Ability to replace Molecule Software's [route53-kubernetes](https://github.com/wearemolecule/route53-kubernetes)
|
||||
|
||||
|
@ -343,7 +343,7 @@ func (cfg *Config) ParseFlags(args []string) error {
|
||||
app.Flag("skipper-routegroup-groupversion", "The resource version for skipper routegroup").Default(source.DefaultRoutegroupVersion).StringVar(&cfg.SkipperRouteGroupVersion)
|
||||
|
||||
// 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, node, fake, connector, istio-gateway, istio-virtualservice, cloudfoundry, contour-ingressroute, contour-httpproxy, gloo-proxy, crd, empty, skipper-routegroup, openshift-route, ambassador-host)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "node", "istio-gateway", "istio-virtualservice", "cloudfoundry", "contour-ingressroute", "contour-httpproxy", "gloo-proxy", "fake", "connector", "crd", "empty", "skipper-routegroup", "openshift-route", "ambassador-host")
|
||||
app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, node, fake, connector, istio-gateway, istio-virtualservice, cloudfoundry, contour-ingressroute, contour-httpproxy, gloo-proxy, crd, empty, skipper-routegroup, openshift-route, ambassador-host)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "node", "pod", "istio-gateway", "istio-virtualservice", "cloudfoundry", "contour-ingressroute", "contour-httpproxy", "gloo-proxy", "fake", "connector", "crd", "empty", "skipper-routegroup", "openshift-route", "ambassador-host")
|
||||
app.Flag("namespace", "Limit sources of endpoints to a specific namespace (default: all namespaces)").Default(defaultConfig.Namespace).StringVar(&cfg.Namespace)
|
||||
app.Flag("annotation-filter", "Filter sources managed by external-dns via annotation using label selector semantics (default: all sources)").Default(defaultConfig.AnnotationFilter).StringVar(&cfg.AnnotationFilter)
|
||||
app.Flag("label-filter", "Filter sources managed by external-dns via label selector when listing all resources; currently only supported by source CRD").Default(defaultConfig.LabelFilter).StringVar(&cfg.LabelFilter)
|
||||
|
123
source/pod.go
Normal file
123
source/pod.go
Normal file
@ -0,0 +1,123 @@
|
||||
/*
|
||||
Copyright 2021 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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
kubeinformers "k8s.io/client-go/informers"
|
||||
coreinformers "k8s.io/client-go/informers/core/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
)
|
||||
|
||||
type podSource struct {
|
||||
client kubernetes.Interface
|
||||
namespace string
|
||||
podInformer coreinformers.PodInformer
|
||||
nodeInformer coreinformers.NodeInformer
|
||||
}
|
||||
|
||||
// NewPodSource creates a new podSource with the given config.
|
||||
func NewPodSource(kubeClient kubernetes.Interface, namespace string) (Source, error) {
|
||||
informerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(kubeClient, 0, kubeinformers.WithNamespace(namespace))
|
||||
podInformer := informerFactory.Core().V1().Pods()
|
||||
nodeInformer := informerFactory.Core().V1().Nodes()
|
||||
|
||||
podInformer.Informer().AddEventHandler(
|
||||
cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: func(obj interface{}) {
|
||||
},
|
||||
},
|
||||
)
|
||||
nodeInformer.Informer().AddEventHandler(
|
||||
cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: func(obj interface{}) {
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
informerFactory.Start(wait.NeverStop)
|
||||
|
||||
// wait for the local cache to be populated.
|
||||
err := poll(time.Second, 60*time.Second, func() (bool, error) {
|
||||
return podInformer.Informer().HasSynced() &&
|
||||
nodeInformer.Informer().HasSynced(), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sync cache: %v", err)
|
||||
}
|
||||
|
||||
return &podSource{
|
||||
client: kubeClient,
|
||||
podInformer: podInformer,
|
||||
nodeInformer: nodeInformer,
|
||||
namespace: namespace,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (*podSource) AddEventHandler(ctx context.Context, handler func()) {
|
||||
|
||||
}
|
||||
|
||||
func (ps *podSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
|
||||
pods, err := ps.podInformer.Lister().Pods(ps.namespace).List(labels.Everything())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
domains := make(map[string][]string)
|
||||
for _, pod := range pods {
|
||||
if !pod.Spec.HostNetwork {
|
||||
log.Debugf("skipping pod %s. hostNetwork=false", pod.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
if domain, ok := pod.Annotations[internalHostnameAnnotationKey]; ok {
|
||||
if _, ok := domains[domain]; !ok {
|
||||
domains[domain] = []string{}
|
||||
}
|
||||
domains[domain] = append(domains[domain], pod.Status.PodIP)
|
||||
}
|
||||
|
||||
if domain, ok := pod.Annotations[hostnameAnnotationKey]; ok {
|
||||
if _, ok := domains[domain]; !ok {
|
||||
domains[domain] = []string{}
|
||||
}
|
||||
|
||||
node, _ := ps.nodeInformer.Lister().Get(pod.Spec.NodeName)
|
||||
for _, address := range node.Status.Addresses {
|
||||
if address.Type == corev1.NodeExternalIP {
|
||||
domains[domain] = append(domains[domain], address.Address)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
endpoints := []*endpoint.Endpoint{}
|
||||
for domain, targets := range domains {
|
||||
endpoints = append(endpoints, endpoint.NewEndpoint(domain, endpoint.RecordTypeA, targets...))
|
||||
}
|
||||
return endpoints, nil
|
||||
}
|
350
source/pod_test.go
Normal file
350
source/pod_test.go
Normal file
@ -0,0 +1,350 @@
|
||||
/*
|
||||
Copyright 2021 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 (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
)
|
||||
|
||||
// testPodSource tests that various services generate the correct endpoints.
|
||||
func TestPodSource(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
title string
|
||||
targetNamespace string
|
||||
expected []*endpoint.Endpoint
|
||||
expectError bool
|
||||
nodes []*corev1.Node
|
||||
pods []*corev1.Pod
|
||||
}{
|
||||
{
|
||||
"create records based on pod's external and internal IPs",
|
||||
"",
|
||||
[]*endpoint.Endpoint{
|
||||
{DNSName: "a.foo.example.org", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}, RecordType: endpoint.RecordTypeA},
|
||||
{DNSName: "internal.a.foo.example.org", Targets: endpoint.Targets{"10.0.1.1", "10.0.1.2"}, RecordType: endpoint.RecordTypeA},
|
||||
},
|
||||
false,
|
||||
[]*corev1.Node{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-node1",
|
||||
},
|
||||
Status: corev1.NodeStatus{
|
||||
Addresses: []corev1.NodeAddress{
|
||||
{Type: corev1.NodeExternalIP, Address: "54.10.11.1"},
|
||||
{Type: corev1.NodeInternalIP, Address: "10.0.1.1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-node2",
|
||||
},
|
||||
Status: corev1.NodeStatus{
|
||||
Addresses: []corev1.NodeAddress{
|
||||
{Type: corev1.NodeExternalIP, Address: "54.10.11.2"},
|
||||
{Type: corev1.NodeInternalIP, Address: "10.0.1.2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
[]*corev1.Pod{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-pod1",
|
||||
Namespace: "kube-system",
|
||||
Annotations: map[string]string{
|
||||
internalHostnameAnnotationKey: "internal.a.foo.example.org",
|
||||
hostnameAnnotationKey: "a.foo.example.org",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
HostNetwork: true,
|
||||
NodeName: "my-node1",
|
||||
},
|
||||
Status: corev1.PodStatus{
|
||||
PodIP: "10.0.1.1",
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-pod2",
|
||||
Namespace: "kube-system",
|
||||
Annotations: map[string]string{
|
||||
internalHostnameAnnotationKey: "internal.a.foo.example.org",
|
||||
hostnameAnnotationKey: "a.foo.example.org",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
HostNetwork: true,
|
||||
NodeName: "my-node2",
|
||||
},
|
||||
Status: corev1.PodStatus{
|
||||
PodIP: "10.0.1.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"create multiple records",
|
||||
"",
|
||||
[]*endpoint.Endpoint{
|
||||
{DNSName: "a.foo.example.org", Targets: endpoint.Targets{"54.10.11.1"}, RecordType: endpoint.RecordTypeA},
|
||||
{DNSName: "b.foo.example.org", Targets: endpoint.Targets{"54.10.11.2"}, RecordType: endpoint.RecordTypeA},
|
||||
},
|
||||
false,
|
||||
[]*corev1.Node{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-node1",
|
||||
},
|
||||
Status: corev1.NodeStatus{
|
||||
Addresses: []corev1.NodeAddress{
|
||||
{Type: corev1.NodeExternalIP, Address: "54.10.11.1"},
|
||||
{Type: corev1.NodeInternalIP, Address: "10.0.1.1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-node2",
|
||||
},
|
||||
Status: corev1.NodeStatus{
|
||||
Addresses: []corev1.NodeAddress{
|
||||
{Type: corev1.NodeExternalIP, Address: "54.10.11.2"},
|
||||
{Type: corev1.NodeInternalIP, Address: "10.0.1.2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
[]*corev1.Pod{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-pod1",
|
||||
Namespace: "kube-system",
|
||||
Annotations: map[string]string{
|
||||
hostnameAnnotationKey: "a.foo.example.org",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
HostNetwork: true,
|
||||
NodeName: "my-node1",
|
||||
},
|
||||
Status: corev1.PodStatus{
|
||||
PodIP: "10.0.1.1",
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-pod2",
|
||||
Namespace: "kube-system",
|
||||
Annotations: map[string]string{
|
||||
hostnameAnnotationKey: "b.foo.example.org",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
HostNetwork: true,
|
||||
NodeName: "my-node2",
|
||||
},
|
||||
Status: corev1.PodStatus{
|
||||
PodIP: "10.0.1.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"pods with hostNetwore=false should be ignored",
|
||||
"",
|
||||
[]*endpoint.Endpoint{
|
||||
{DNSName: "a.foo.example.org", Targets: endpoint.Targets{"54.10.11.1"}, RecordType: endpoint.RecordTypeA},
|
||||
{DNSName: "internal.a.foo.example.org", Targets: endpoint.Targets{"10.0.1.1"}, RecordType: endpoint.RecordTypeA},
|
||||
},
|
||||
false,
|
||||
[]*corev1.Node{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-node1",
|
||||
},
|
||||
Status: corev1.NodeStatus{
|
||||
Addresses: []corev1.NodeAddress{
|
||||
{Type: corev1.NodeExternalIP, Address: "54.10.11.1"},
|
||||
{Type: corev1.NodeInternalIP, Address: "10.0.1.1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-node2",
|
||||
},
|
||||
Status: corev1.NodeStatus{
|
||||
Addresses: []corev1.NodeAddress{
|
||||
{Type: corev1.NodeExternalIP, Address: "54.10.11.2"},
|
||||
{Type: corev1.NodeInternalIP, Address: "10.0.1.2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
[]*corev1.Pod{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-pod1",
|
||||
Namespace: "kube-system",
|
||||
Annotations: map[string]string{
|
||||
internalHostnameAnnotationKey: "internal.a.foo.example.org",
|
||||
hostnameAnnotationKey: "a.foo.example.org",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
HostNetwork: true,
|
||||
NodeName: "my-node1",
|
||||
},
|
||||
Status: corev1.PodStatus{
|
||||
PodIP: "10.0.1.1",
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-pod2",
|
||||
Namespace: "kube-system",
|
||||
Annotations: map[string]string{
|
||||
internalHostnameAnnotationKey: "internal.a.foo.example.org",
|
||||
hostnameAnnotationKey: "a.foo.example.org",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
HostNetwork: false,
|
||||
NodeName: "my-node2",
|
||||
},
|
||||
Status: corev1.PodStatus{
|
||||
PodIP: "100.0.1.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"only watch a given namespace",
|
||||
"kube-system",
|
||||
[]*endpoint.Endpoint{
|
||||
{DNSName: "a.foo.example.org", Targets: endpoint.Targets{"54.10.11.1"}, RecordType: endpoint.RecordTypeA},
|
||||
{DNSName: "internal.a.foo.example.org", Targets: endpoint.Targets{"10.0.1.1"}, RecordType: endpoint.RecordTypeA},
|
||||
},
|
||||
false,
|
||||
[]*corev1.Node{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-node1",
|
||||
},
|
||||
Status: corev1.NodeStatus{
|
||||
Addresses: []corev1.NodeAddress{
|
||||
{Type: corev1.NodeExternalIP, Address: "54.10.11.1"},
|
||||
{Type: corev1.NodeInternalIP, Address: "10.0.1.1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-node2",
|
||||
},
|
||||
Status: corev1.NodeStatus{
|
||||
Addresses: []corev1.NodeAddress{
|
||||
{Type: corev1.NodeExternalIP, Address: "54.10.11.2"},
|
||||
{Type: corev1.NodeInternalIP, Address: "10.0.1.2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
[]*corev1.Pod{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-pod1",
|
||||
Namespace: "kube-system",
|
||||
Annotations: map[string]string{
|
||||
internalHostnameAnnotationKey: "internal.a.foo.example.org",
|
||||
hostnameAnnotationKey: "a.foo.example.org",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
HostNetwork: true,
|
||||
NodeName: "my-node1",
|
||||
},
|
||||
Status: corev1.PodStatus{
|
||||
PodIP: "10.0.1.1",
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-pod2",
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
internalHostnameAnnotationKey: "internal.a.foo.example.org",
|
||||
hostnameAnnotationKey: "a.foo.example.org",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
HostNetwork: true,
|
||||
NodeName: "my-node2",
|
||||
},
|
||||
Status: corev1.PodStatus{
|
||||
PodIP: "100.0.1.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.title, func(t *testing.T) {
|
||||
// Create a Kubernetes testing client
|
||||
kubernetes := fake.NewSimpleClientset()
|
||||
ctx := context.Background()
|
||||
|
||||
// Create the nodes
|
||||
for _, node := range tc.nodes {
|
||||
if _, err := kubernetes.CoreV1().Nodes().Create(ctx, node, metav1.CreateOptions{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, pod := range tc.pods {
|
||||
pods := kubernetes.CoreV1().Pods(pod.Namespace)
|
||||
|
||||
if _, err := pods.Create(ctx, pod, metav1.CreateOptions{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
client, err := NewPodSource(kubernetes, tc.targetNamespace)
|
||||
require.NoError(t, err)
|
||||
|
||||
endpoints, err := client.Endpoints(ctx)
|
||||
if tc.expectError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// Validate returned endpoints against desired endpoints.
|
||||
validateEndpoints(t, endpoints, tc.expected)
|
||||
})
|
||||
|
||||
}
|
||||
}
|
@ -188,6 +188,12 @@ func BuildWithConfig(source string, p ClientGenerator, cfg *Config) (Source, err
|
||||
return nil, err
|
||||
}
|
||||
return NewIngressSource(client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation, cfg.IgnoreIngressTLSSpec)
|
||||
case "pod":
|
||||
client, err := p.KubeClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewPodSource(client, cfg.Namespace)
|
||||
case "istio-gateway":
|
||||
kubernetesClient, err := p.KubeClient()
|
||||
if err != nil {
|
||||
|
Loading…
Reference in New Issue
Block a user