Merge remote-tracking branch 'kubernetes-sigs/master' into godaddy

This commit is contained in:
fboltz 2021-01-30 10:22:48 +01:00
commit f7f90733ce
19 changed files with 1167 additions and 296 deletions

View File

@ -17,6 +17,7 @@
- Update contributing section in README (#1760) @seanmalloy - Update contributing section in README (#1760) @seanmalloy
- Option to cache AWS zones list @bpineau - Option to cache AWS zones list @bpineau
- Refactor, enhance and test Akamai provider and documentation (#1846) @edglynes - Refactor, enhance and test Akamai provider and documentation (#1846) @edglynes
- Fix: only use absolute CNAMEs in Scaleway provider (#1859) @Sh4d1
## v0.7.3 - 2020-08-05 ## v0.7.3 - 2020-08-05

View File

@ -1,12 +1,12 @@
# Set up ExternalDNS for Azure Private DNS # Set up ExternalDNS for Azure Private DNS
This tutorial describes how to set up ExternalDNS for managing records in Azure Private DNS. This tutorial describes how to set up ExternalDNS for managing records in Azure Private DNS.
It comprises of the following steps: It comprises of the following steps:
1) Install NGINX Ingress Controller 1) Install NGINX Ingress Controller
2) Provision Azure Private DNS 2) Provision Azure Private DNS
3) Configure service principal for managing the zone 3) Configure service principal for managing the zone
4) Deploy ExternalDNS 4) Deploy ExternalDNS
Everything will be deployed on Kubernetes. Everything will be deployed on Kubernetes.
Therefore, please see the subsequent prerequisites. Therefore, please see the subsequent prerequisites.
@ -26,25 +26,27 @@ $ helm install stable/nginx-ingress \
--name nginx-ingress \ --name nginx-ingress \
--set controller.publishService.enabled=true --set controller.publishService.enabled=true
``` ```
The parameter `controller.publishService.enabled` needs to be set to `true.` The parameter `controller.publishService.enabled` needs to be set to `true.`
It will make the ingress controller update the endpoint records of ingress-resources to contain the external-ip of the loadbalancer serving the ingress-controller. It will make the ingress controller update the endpoint records of ingress-resources to contain the external-ip of the loadbalancer serving the ingress-controller.
This is crucial as ExternalDNS reads those endpoints records when creating DNS-Records from ingress-resources. This is crucial as ExternalDNS reads those endpoints records when creating DNS-Records from ingress-resources.
In the subsequent parameter we will make use of this. If you don't want to work with ingress-resources in your later use, you can leave the parameter out. In the subsequent parameter we will make use of this. If you don't want to work with ingress-resources in your later use, you can leave the parameter out.
Verify the correct propagation of the loadbalancer's ip by listing the ingresses. Verify the correct propagation of the loadbalancer's ip by listing the ingresses.
``` ```
$ kubectl get ingress $ kubectl get ingress
``` ```
The address column should contain the ip for each ingress. ExternalDNS will pick up exactly this piece of information. The address column should contain the ip for each ingress. ExternalDNS will pick up exactly this piece of information.
``` ```
NAME HOSTS ADDRESS PORTS AGE NAME HOSTS ADDRESS PORTS AGE
nginx1 sample1.aks.com 52.167.195.110 80 6d22h nginx1 sample1.aks.com 52.167.195.110 80 6d22h
nginx2 sample2.aks.com 52.167.195.110 80 6d21h nginx2 sample2.aks.com 52.167.195.110 80 6d21h
``` ```
If you do not want to deploy the ingress controller with Helm, ensure to pass the following cmdline-flags to it through the mechanism of your choice: If you do not want to deploy the ingress controller with Helm, ensure to pass the following cmdline-flags to it through the mechanism of your choice:
``` ```
@ -144,6 +146,8 @@ This is per default done through the file `~/.kube/config`.
For general background information on this see [kubernetes-docs](https://kubernetes.io/docs/tasks/access-application-cluster/access-cluster/). For general background information on this see [kubernetes-docs](https://kubernetes.io/docs/tasks/access-application-cluster/access-cluster/).
Azure-CLI features functionality for automatically maintaining this file for AKS-Clusters. See [Azure-Docs](https://docs.microsoft.com/de-de/cli/azure/aks?view=azure-cli-latest#az-aks-get-credentials). Azure-CLI features functionality for automatically maintaining this file for AKS-Clusters. See [Azure-Docs](https://docs.microsoft.com/de-de/cli/azure/aks?view=azure-cli-latest#az-aks-get-credentials).
Follow the steps for [azure-dns provider](./azure.md#creating-configuration-file) to create a configuration file.
Then apply one of the following manifests depending on whether you use RBAC or not. Then apply one of the following manifests depending on whether you use RBAC or not.
The credentials of the service principal are provided to ExternalDNS as environment-variables. The credentials of the service principal are provided to ExternalDNS as environment-variables.
@ -175,13 +179,14 @@ spec:
- --provider=azure-private-dns - --provider=azure-private-dns
- --azure-resource-group=externaldns - --azure-resource-group=externaldns
- --azure-subscription-id=<use the id of your subscription> - --azure-subscription-id=<use the id of your subscription>
env: volumeMounts:
- name: AZURE_TENANT_ID - name: azure-config-file
value: "<use the tenantId discovered during creation of service principal>" mountPath: /etc/kubernetes
- name: AZURE_CLIENT_ID readOnly: true
value: "<use the aadClientId discovered during creation of service principal>" volumes:
- name: AZURE_CLIENT_SECRET - name: azure-config-file
value: "<use the aadClientSecret discovered during creation of service principal>" secret:
secretName: azure-config-file
``` ```
### Manifest (for clusters with RBAC enabled, cluster access) ### Manifest (for clusters with RBAC enabled, cluster access)
@ -200,7 +205,7 @@ rules:
resources: ["services","endpoints","pods"] resources: ["services","endpoints","pods"]
verbs: ["get","watch","list"] verbs: ["get","watch","list"]
- apiGroups: ["extensions","networking.k8s.io"] - apiGroups: ["extensions","networking.k8s.io"]
resources: ["ingresses"] resources: ["ingresses"]
verbs: ["get","watch","list"] verbs: ["get","watch","list"]
- apiGroups: [""] - apiGroups: [""]
resources: ["nodes"] resources: ["nodes"]
@ -245,13 +250,14 @@ spec:
- --provider=azure-private-dns - --provider=azure-private-dns
- --azure-resource-group=externaldns - --azure-resource-group=externaldns
- --azure-subscription-id=<use the id of your subscription> - --azure-subscription-id=<use the id of your subscription>
env: volumeMounts:
- name: AZURE_TENANT_ID - name: azure-config-file
value: "<use the tenantId discovered during creation of service principal>" mountPath: /etc/kubernetes
- name: AZURE_CLIENT_ID readOnly: true
value: "<use the aadClientId discovered during creation of service principal>" volumes:
- name: AZURE_CLIENT_SECRET - name: azure-config-file
value: "<use the aadClientSecret discovered during creation of service principal>" secret:
secretName: azure-config-file
``` ```
### Manifest (for clusters with RBAC enabled, namespace access) ### Manifest (for clusters with RBAC enabled, namespace access)
@ -315,13 +321,14 @@ spec:
- --provider=azure-private-dns - --provider=azure-private-dns
- --azure-resource-group=externaldns - --azure-resource-group=externaldns
- --azure-subscription-id=<use the id of your subscription> - --azure-subscription-id=<use the id of your subscription>
env: volumeMounts:
- name: AZURE_TENANT_ID - name: azure-config-file
value: "<use the tenantId discovered during creation of service principal>" mountPath: /etc/kubernetes
- name: AZURE_CLIENT_ID readOnly: true
value: "<use the aadClientId discovered during creation of service principal>" volumes:
- name: AZURE_CLIENT_SECRET - name: azure-config-file
value: "<use the aadClientSecret discovered during creation of service principal>" secret:
secretName: azure-config-file
``` ```
Create the deployment for ExternalDNS: Create the deployment for ExternalDNS:

View File

@ -78,7 +78,7 @@ rules:
See also current RBAC yaml files: See also current RBAC yaml files:
- [kube-ingress-aws-controller](https://github.com/zalando-incubator/kubernetes-on-aws/blob/dev/cluster/manifests/ingress-controller/01-rbac.yaml) - [kube-ingress-aws-controller](https://github.com/zalando-incubator/kubernetes-on-aws/blob/dev/cluster/manifests/ingress-controller/01-rbac.yaml)
- [skipper](https://github.com/zalando-incubator/kubernetes-on-aws/blob/dev/cluster/manifests/skipper/rbac.yaml) - [skipper](https://github.com/zalando-incubator/kubernetes-on-aws/blob/dev/cluster/manifests/skipper/rbac.yaml)
- [external-dns](https://github.com/zalando-incubator/kubernetes-on-aws/blob/dev/cluster/manifests/external-dns/rbac.yaml) - [external-dns](https://github.com/zalando-incubator/kubernetes-on-aws/blob/dev/cluster/manifests/external-dns/01-rbac.yaml)
[3]: https://opensource.zalando.com/skipper/kubernetes/routegroups/#routegroups [3]: https://opensource.zalando.com/skipper/kubernetes/routegroups/#routegroups
[4]: https://opensource.zalando.com/skipper [4]: https://opensource.zalando.com/skipper

14
go.mod
View File

@ -8,7 +8,6 @@ require (
github.com/Azure/azure-sdk-for-go v45.1.0+incompatible github.com/Azure/azure-sdk-for-go v45.1.0+incompatible
github.com/Azure/go-autorest/autorest v0.11.10 github.com/Azure/go-autorest/autorest v0.11.10
github.com/Azure/go-autorest/autorest/adal v0.9.5 github.com/Azure/go-autorest/autorest/adal v0.9.5
github.com/Azure/go-autorest/autorest/azure/auth v0.5.3
github.com/Azure/go-autorest/autorest/to v0.4.0 github.com/Azure/go-autorest/autorest/to v0.4.0
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.0.0 github.com/akamai/AkamaiOPEN-edgegrid-golang v1.0.0
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 // indirect github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 // indirect
@ -19,6 +18,7 @@ require (
github.com/aws/aws-sdk-go v1.31.4 github.com/aws/aws-sdk-go v1.31.4
github.com/cloudflare/cloudflare-go v0.10.1 github.com/cloudflare/cloudflare-go v0.10.1
github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381 github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381
github.com/datawire/ambassador v1.6.0
github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba
github.com/digitalocean/godo v1.36.0 github.com/digitalocean/godo v1.36.0
github.com/dnsimple/dnsimple-go v0.60.0 github.com/dnsimple/dnsimple-go v0.60.0
@ -46,7 +46,8 @@ require (
github.com/sanyu/dynectsoap v0.0.0-20181203081243-b83de5edc4e0 github.com/sanyu/dynectsoap v0.0.0-20181203081243-b83de5edc4e0
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.6.0.20200623155123-84df6c4b5301 github.com/scaleway/scaleway-sdk-go v1.0.0-beta.6.0.20200623155123-84df6c4b5301
github.com/sirupsen/logrus v1.6.0 github.com/sirupsen/logrus v1.6.0
github.com/stretchr/testify v1.5.1 github.com/smartystreets/gunit v1.3.4 // indirect
github.com/stretchr/testify v1.6.1
github.com/terra-farm/udnssdk v1.3.5 // indirect github.com/terra-farm/udnssdk v1.3.5 // indirect
github.com/transip/gotransip v5.8.2+incompatible github.com/transip/gotransip v5.8.2+incompatible
github.com/ultradns/ultradns-sdk-go v0.0.0-20200616202852-e62052662f60 github.com/ultradns/ultradns-sdk-go v0.0.0-20200616202852-e62052662f60
@ -54,17 +55,20 @@ require (
github.com/vultr/govultr v0.4.2 github.com/vultr/govultr v0.4.2
go.etcd.io/etcd v0.5.0-alpha.5.0.20200401174654-e694b7bb0875 go.etcd.io/etcd v0.5.0-alpha.5.0.20200401174654-e694b7bb0875
go.uber.org/ratelimit v0.1.0 go.uber.org/ratelimit v0.1.0
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e golang.org/x/net v0.0.0-20200625001655-4c5254603344
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208
golang.org/x/tools v0.0.0-20200708003708-134513de8882 // indirect
google.golang.org/api v0.15.0 google.golang.org/api v0.15.0
gopkg.in/ns1/ns1-go.v2 v2.0.0-20190322154155-0dafb5275fd1 gopkg.in/ns1/ns1-go.v2 v2.0.0-20190322154155-0dafb5275fd1
gopkg.in/yaml.v2 v2.2.8 gopkg.in/yaml.v2 v2.3.0
honnef.co/go/tools v0.0.1-2020.1.4 // indirect
istio.io/api v0.0.0-20200529165953-72dad51d4ffc istio.io/api v0.0.0-20200529165953-72dad51d4ffc
istio.io/client-go v0.0.0-20200529172309-31c16ea3f751 istio.io/client-go v0.0.0-20200529172309-31c16ea3f751
k8s.io/api v0.18.8 k8s.io/api v0.18.8
k8s.io/apimachinery v0.18.8 k8s.io/apimachinery v0.18.8
k8s.io/client-go v0.18.8 k8s.io/client-go v0.18.8
k8s.io/kubernetes v1.13.0
) )
replace ( replace (

487
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -193,7 +193,7 @@ func main() {
case "azure-dns", "azure": case "azure-dns", "azure":
p, err = azure.NewAzureProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.DryRun) p, err = azure.NewAzureProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.DryRun)
case "azure-private-dns": case "azure-private-dns":
p, err = azure.NewAzurePrivateDNSProvider(domainFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureSubscriptionID, cfg.DryRun) p, err = azure.NewAzurePrivateDNSProvider(cfg.AzureConfigFile, domainFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.DryRun)
case "vinyldns": case "vinyldns":
p, err = vinyldns.NewVinylDNSProvider(domainFilter, zoneIDFilter, cfg.DryRun) p, err = vinyldns.NewVinylDNSProvider(domainFilter, zoneIDFilter, cfg.DryRun)
case "vultr": case "vultr":

View File

@ -326,7 +326,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) app.Flag("skipper-routegroup-groupversion", "The resource version for skipper routegroup").Default(source.DefaultRoutegroupVersion).StringVar(&cfg.SkipperRouteGroupVersion)
// Flags related to processing sources // 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, crd, empty, skipper-routegroup,openshift-route)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "node", "istio-gateway", "istio-virtualservice", "cloudfoundry", "contour-ingressroute", "contour-httpproxy", "fake", "connector", "crd", "empty", "skipper-routegroup", "openshift-route") 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, 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", "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("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("annotation-filter", "Filter sources managed by external-dns via annotation using label selector semantics (default: all sources)").Default(defaultConfig.AnnotationFilter).StringVar(&cfg.AnnotationFilter)

View File

@ -19,17 +19,12 @@ package azure
import ( import (
"context" "context"
"fmt" "fmt"
"io/ioutil"
"strings" "strings"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
yaml "gopkg.in/yaml.v2"
"github.com/Azure/azure-sdk-for-go/services/dns/mgmt/2018-05-01/dns" "github.com/Azure/azure-sdk-for-go/services/dns/mgmt/2018-05-01/dns"
"github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/adal"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/Azure/go-autorest/autorest/to" "github.com/Azure/go-autorest/autorest/to"
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
@ -41,18 +36,6 @@ const (
azureRecordTTL = 300 azureRecordTTL = 300
) )
type config struct {
Cloud string `json:"cloud" yaml:"cloud"`
TenantID string `json:"tenantId" yaml:"tenantId"`
SubscriptionID string `json:"subscriptionId" yaml:"subscriptionId"`
ResourceGroup string `json:"resourceGroup" yaml:"resourceGroup"`
Location string `json:"location" yaml:"location"`
ClientID string `json:"aadClientId" yaml:"aadClientId"`
ClientSecret string `json:"aadClientSecret" yaml:"aadClientSecret"`
UseManagedIdentityExtension bool `json:"useManagedIdentityExtension" yaml:"useManagedIdentityExtension"`
UserAssignedIdentityID string `json:"userAssignedIdentityID" yaml:"userAssignedIdentityID"`
}
// ZonesClient is an interface of dns.ZoneClient that can be stubbed for testing. // ZonesClient is an interface of dns.ZoneClient that can be stubbed for testing.
type ZonesClient interface { type ZonesClient interface {
ListByResourceGroupComplete(ctx context.Context, resourceGroupName string, top *int32) (result dns.ZoneListResultIterator, err error) ListByResourceGroupComplete(ctx context.Context, resourceGroupName string, top *int32) (result dns.ZoneListResultIterator, err error)
@ -82,46 +65,22 @@ type AzureProvider struct {
// //
// Returns the provider or an error if a provider could not be created. // Returns the provider or an error if a provider could not be created.
func NewAzureProvider(configFile string, domainFilter endpoint.DomainFilter, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, resourceGroup string, userAssignedIdentityClientID string, dryRun bool) (*AzureProvider, error) { func NewAzureProvider(configFile string, domainFilter endpoint.DomainFilter, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, resourceGroup string, userAssignedIdentityClientID string, dryRun bool) (*AzureProvider, error) {
contents, err := ioutil.ReadFile(configFile) cfg, err := getConfig(configFile, resourceGroup, userAssignedIdentityClientID)
if err != nil {
return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err)
}
cfg := config{}
err = yaml.Unmarshal(contents, &cfg)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err) return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err)
} }
// If a resource group was given, override what was present in the config file token, err := getAccessToken(*cfg, cfg.Environment)
if resourceGroup != "" {
cfg.ResourceGroup = resourceGroup
}
// If userAssignedIdentityClientID is provided explicitly, override existing one in config file
if userAssignedIdentityClientID != "" {
cfg.UserAssignedIdentityID = userAssignedIdentityClientID
}
var environment azure.Environment
if cfg.Cloud == "" {
environment = azure.PublicCloud
} else {
environment, err = azure.EnvironmentFromName(cfg.Cloud)
if err != nil {
return nil, fmt.Errorf("invalid cloud value '%s': %v", cfg.Cloud, err)
}
}
token, err := getAccessToken(cfg, environment)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get token: %v", err) return nil, fmt.Errorf("failed to get token: %v", err)
} }
zonesClient := dns.NewZonesClientWithBaseURI(environment.ResourceManagerEndpoint, cfg.SubscriptionID) zonesClient := dns.NewZonesClientWithBaseURI(cfg.Environment.ResourceManagerEndpoint, cfg.SubscriptionID)
zonesClient.Authorizer = autorest.NewBearerAuthorizer(token) zonesClient.Authorizer = autorest.NewBearerAuthorizer(token)
recordSetsClient := dns.NewRecordSetsClientWithBaseURI(environment.ResourceManagerEndpoint, cfg.SubscriptionID) recordSetsClient := dns.NewRecordSetsClientWithBaseURI(cfg.Environment.ResourceManagerEndpoint, cfg.SubscriptionID)
recordSetsClient.Authorizer = autorest.NewBearerAuthorizer(token) recordSetsClient.Authorizer = autorest.NewBearerAuthorizer(token)
provider := &AzureProvider{ return &AzureProvider{
domainFilter: domainFilter, domainFilter: domainFilter,
zoneNameFilter: zoneNameFilter, zoneNameFilter: zoneNameFilter,
zoneIDFilter: zoneIDFilter, zoneIDFilter: zoneIDFilter,
@ -130,61 +89,7 @@ func NewAzureProvider(configFile string, domainFilter endpoint.DomainFilter, zon
userAssignedIdentityClientID: cfg.UserAssignedIdentityID, userAssignedIdentityClientID: cfg.UserAssignedIdentityID,
zonesClient: zonesClient, zonesClient: zonesClient,
recordSetsClient: recordSetsClient, recordSetsClient: recordSetsClient,
} }, nil
return provider, nil
}
// getAccessToken retrieves Azure API access token.
func getAccessToken(cfg config, environment azure.Environment) (*adal.ServicePrincipalToken, error) {
// Try to retrieve token with service principal credentials.
// Try to use service principal first, some AKS clusters are in an intermediate state that `UseManagedIdentityExtension` is `true`
// and service principal exists. In this case, we still want to use service principal to authenticate.
if len(cfg.ClientID) > 0 &&
len(cfg.ClientSecret) > 0 &&
// due to some historical reason, for pure MSI cluster,
// they will use "msi" as placeholder in azure.json.
// In this case, we shouldn't try to use SPN to authenticate.
!strings.EqualFold(cfg.ClientID, "msi") &&
!strings.EqualFold(cfg.ClientSecret, "msi") {
log.Info("Using client_id+client_secret to retrieve access token for Azure API.")
oauthConfig, err := adal.NewOAuthConfig(environment.ActiveDirectoryEndpoint, cfg.TenantID)
if err != nil {
return nil, fmt.Errorf("failed to retrieve OAuth config: %v", err)
}
token, err := adal.NewServicePrincipalToken(*oauthConfig, cfg.ClientID, cfg.ClientSecret, environment.ResourceManagerEndpoint)
if err != nil {
return nil, fmt.Errorf("failed to create service principal token: %v", err)
}
return token, nil
}
// Try to retrieve token with MSI.
if cfg.UseManagedIdentityExtension {
log.Info("Using managed identity extension to retrieve access token for Azure API.")
msiEndpoint, err := adal.GetMSIVMEndpoint()
if err != nil {
return nil, fmt.Errorf("failed to get the managed service identity endpoint: %v", err)
}
if cfg.UserAssignedIdentityID != "" {
log.Infof("Resolving to user assigned identity, client id is %s.", cfg.UserAssignedIdentityID)
token, err := adal.NewServicePrincipalTokenFromMSIWithUserAssignedID(msiEndpoint, environment.ServiceManagementEndpoint, cfg.UserAssignedIdentityID)
if err != nil {
return nil, fmt.Errorf("failed to create the managed service identity token: %v", err)
}
return token, nil
}
log.Info("Resolving to system assigned identity.")
token, err := adal.NewServicePrincipalTokenFromMSI(msiEndpoint, environment.ServiceManagementEndpoint)
if err != nil {
return nil, fmt.Errorf("failed to create the managed service identity token: %v", err)
}
return token, nil
}
return nil, fmt.Errorf("no credentials provided for Azure API")
} }
// Records gets the current records. // Records gets the current records.
@ -352,20 +257,20 @@ func (p *AzureProvider) mapChanges(zones []dns.Zone, changes *plan.Changes) (azu
func (p *AzureProvider) deleteRecords(ctx context.Context, deleted azureChangeMap) { func (p *AzureProvider) deleteRecords(ctx context.Context, deleted azureChangeMap) {
// Delete records first // Delete records first
for zone, endpoints := range deleted { for zone, endpoints := range deleted {
for _, endpoint := range endpoints { for _, ep := range endpoints {
name := p.recordSetNameForZone(zone, endpoint) name := p.recordSetNameForZone(zone, ep)
if !p.domainFilter.Match(endpoint.DNSName) { if !p.domainFilter.Match(ep.DNSName) {
log.Debugf("Skipping deletion of record %s because it was filtered out by the specified --domain-filter", endpoint.DNSName) log.Debugf("Skipping deletion of record %s because it was filtered out by the specified --domain-filter", ep.DNSName)
continue continue
} }
if p.dryRun { if p.dryRun {
log.Infof("Would delete %s record named '%s' for Azure DNS zone '%s'.", endpoint.RecordType, name, zone) log.Infof("Would delete %s record named '%s' for Azure DNS zone '%s'.", ep.RecordType, name, zone)
} else { } else {
log.Infof("Deleting %s record named '%s' for Azure DNS zone '%s'.", endpoint.RecordType, name, zone) log.Infof("Deleting %s record named '%s' for Azure DNS zone '%s'.", ep.RecordType, name, zone)
if _, err := p.recordSetsClient.Delete(ctx, p.resourceGroup, zone, name, dns.RecordType(endpoint.RecordType), ""); err != nil { if _, err := p.recordSetsClient.Delete(ctx, p.resourceGroup, zone, name, dns.RecordType(ep.RecordType), ""); err != nil {
log.Errorf( log.Errorf(
"Failed to delete %s record named '%s' for Azure DNS zone '%s': %v", "Failed to delete %s record named '%s' for Azure DNS zone '%s': %v",
endpoint.RecordType, ep.RecordType,
name, name,
zone, zone,
err, err,
@ -378,18 +283,18 @@ func (p *AzureProvider) deleteRecords(ctx context.Context, deleted azureChangeMa
func (p *AzureProvider) updateRecords(ctx context.Context, updated azureChangeMap) { func (p *AzureProvider) updateRecords(ctx context.Context, updated azureChangeMap) {
for zone, endpoints := range updated { for zone, endpoints := range updated {
for _, endpoint := range endpoints { for _, ep := range endpoints {
name := p.recordSetNameForZone(zone, endpoint) name := p.recordSetNameForZone(zone, ep)
if !p.domainFilter.Match(endpoint.DNSName) { if !p.domainFilter.Match(ep.DNSName) {
log.Debugf("Skipping update of record %s because it was filtered out by the specified --domain-filter", endpoint.DNSName) log.Debugf("Skipping update of record %s because it was filtered out by the specified --domain-filter", ep.DNSName)
continue continue
} }
if p.dryRun { if p.dryRun {
log.Infof( log.Infof(
"Would update %s record named '%s' to '%s' for Azure DNS zone '%s'.", "Would update %s record named '%s' to '%s' for Azure DNS zone '%s'.",
endpoint.RecordType, ep.RecordType,
name, name,
endpoint.Targets, ep.Targets,
zone, zone,
) )
continue continue
@ -397,20 +302,20 @@ func (p *AzureProvider) updateRecords(ctx context.Context, updated azureChangeMa
log.Infof( log.Infof(
"Updating %s record named '%s' to '%s' for Azure DNS zone '%s'.", "Updating %s record named '%s' to '%s' for Azure DNS zone '%s'.",
endpoint.RecordType, ep.RecordType,
name, name,
endpoint.Targets, ep.Targets,
zone, zone,
) )
recordSet, err := p.newRecordSet(endpoint) recordSet, err := p.newRecordSet(ep)
if err == nil { if err == nil {
_, err = p.recordSetsClient.CreateOrUpdate( _, err = p.recordSetsClient.CreateOrUpdate(
ctx, ctx,
p.resourceGroup, p.resourceGroup,
zone, zone,
name, name,
dns.RecordType(endpoint.RecordType), dns.RecordType(ep.RecordType),
recordSet, recordSet,
"", "",
"", "",
@ -419,9 +324,9 @@ func (p *AzureProvider) updateRecords(ctx context.Context, updated azureChangeMa
if err != nil { if err != nil {
log.Errorf( log.Errorf(
"Failed to update %s record named '%s' to '%s' for DNS zone '%s': %v", "Failed to update %s record named '%s' to '%s' for DNS zone '%s': %v",
endpoint.RecordType, ep.RecordType,
name, name,
endpoint.Targets, ep.Targets,
zone, zone,
err, err,
) )

View File

@ -21,9 +21,8 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/Azure/azure-sdk-for-go/profiles/latest/privatedns/mgmt/privatedns" "github.com/Azure/azure-sdk-for-go/services/privatedns/mgmt/2018-09-01/privatedns"
"github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/azure/auth"
"github.com/Azure/go-autorest/autorest/to" "github.com/Azure/go-autorest/autorest/to"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -47,44 +46,43 @@ type PrivateRecordSetsClient interface {
// AzurePrivateDNSProvider implements the DNS provider for Microsoft's Azure Private DNS service // AzurePrivateDNSProvider implements the DNS provider for Microsoft's Azure Private DNS service
type AzurePrivateDNSProvider struct { type AzurePrivateDNSProvider struct {
provider.BaseProvider provider.BaseProvider
domainFilter endpoint.DomainFilter domainFilter endpoint.DomainFilter
zoneIDFilter provider.ZoneIDFilter zoneIDFilter provider.ZoneIDFilter
dryRun bool dryRun bool
subscriptionID string resourceGroup string
resourceGroup string userAssignedIdentityClientID string
zonesClient PrivateZonesClient zonesClient PrivateZonesClient
recordSetsClient PrivateRecordSetsClient recordSetsClient PrivateRecordSetsClient
} }
// NewAzurePrivateDNSProvider creates a new Azure Private DNS provider. // NewAzurePrivateDNSProvider creates a new Azure Private DNS provider.
// //
// Returns the provider or an error if a provider could not be created. // Returns the provider or an error if a provider could not be created.
func NewAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, resourceGroup string, subscriptionID string, dryRun bool) (*AzurePrivateDNSProvider, error) { func NewAzurePrivateDNSProvider(configFile string, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, resourceGroup, userAssignedIdentityClientID string, dryRun bool) (*AzurePrivateDNSProvider, error) {
authorizer, err := auth.NewAuthorizerFromEnvironment() cfg, err := getConfig(configFile, resourceGroup, userAssignedIdentityClientID)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err)
} }
settings, err := auth.GetSettingsFromEnvironment() token, err := getAccessToken(*cfg, cfg.Environment)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failed to get token: %v", err)
} }
zonesClient := privatedns.NewPrivateZonesClientWithBaseURI(settings.Environment.ResourceManagerEndpoint, subscriptionID) zonesClient := privatedns.NewPrivateZonesClientWithBaseURI(cfg.Environment.ResourceManagerEndpoint, cfg.SubscriptionID)
zonesClient.Authorizer = authorizer zonesClient.Authorizer = autorest.NewBearerAuthorizer(token)
recordSetsClient := privatedns.NewRecordSetsClientWithBaseURI(settings.Environment.ResourceManagerEndpoint, subscriptionID) recordSetsClient := privatedns.NewRecordSetsClientWithBaseURI(cfg.Environment.ResourceManagerEndpoint, cfg.SubscriptionID)
recordSetsClient.Authorizer = authorizer recordSetsClient.Authorizer = autorest.NewBearerAuthorizer(token)
provider := &AzurePrivateDNSProvider{ return &AzurePrivateDNSProvider{
domainFilter: domainFilter, domainFilter: domainFilter,
zoneIDFilter: zoneIDFilter, zoneIDFilter: zoneIDFilter,
dryRun: dryRun, dryRun: dryRun,
subscriptionID: subscriptionID, resourceGroup: cfg.ResourceGroup,
resourceGroup: resourceGroup, userAssignedIdentityClientID: cfg.UserAssignedIdentityID,
zonesClient: zonesClient, zonesClient: zonesClient,
recordSetsClient: recordSetsClient, recordSetsClient: recordSetsClient,
} }, nil
return provider, nil
} }
// Records gets the current records. // Records gets the current records.
@ -256,16 +254,16 @@ func (p *AzurePrivateDNSProvider) deleteRecords(ctx context.Context, deleted azu
log.Debugf("Records to be deleted: %d", len(deleted)) log.Debugf("Records to be deleted: %d", len(deleted))
// Delete records first // Delete records first
for zone, endpoints := range deleted { for zone, endpoints := range deleted {
for _, endpoint := range endpoints { for _, ep := range endpoints {
name := p.recordSetNameForZone(zone, endpoint) name := p.recordSetNameForZone(zone, ep)
if p.dryRun { if p.dryRun {
log.Infof("Would delete %s record named '%s' for Azure Private DNS zone '%s'.", endpoint.RecordType, name, zone) log.Infof("Would delete %s record named '%s' for Azure Private DNS zone '%s'.", ep.RecordType, name, zone)
} else { } else {
log.Infof("Deleting %s record named '%s' for Azure Private DNS zone '%s'.", endpoint.RecordType, name, zone) log.Infof("Deleting %s record named '%s' for Azure Private DNS zone '%s'.", ep.RecordType, name, zone)
if _, err := p.recordSetsClient.Delete(ctx, p.resourceGroup, zone, privatedns.RecordType(endpoint.RecordType), name, ""); err != nil { if _, err := p.recordSetsClient.Delete(ctx, p.resourceGroup, zone, privatedns.RecordType(ep.RecordType), name, ""); err != nil {
log.Errorf( log.Errorf(
"Failed to delete %s record named '%s' for Azure Private DNS zone '%s': %v", "Failed to delete %s record named '%s' for Azure Private DNS zone '%s': %v",
endpoint.RecordType, ep.RecordType,
name, name,
zone, zone,
err, err,
@ -279,14 +277,14 @@ func (p *AzurePrivateDNSProvider) deleteRecords(ctx context.Context, deleted azu
func (p *AzurePrivateDNSProvider) updateRecords(ctx context.Context, updated azurePrivateDNSChangeMap) { func (p *AzurePrivateDNSProvider) updateRecords(ctx context.Context, updated azurePrivateDNSChangeMap) {
log.Debugf("Records to be updated: %d", len(updated)) log.Debugf("Records to be updated: %d", len(updated))
for zone, endpoints := range updated { for zone, endpoints := range updated {
for _, endpoint := range endpoints { for _, ep := range endpoints {
name := p.recordSetNameForZone(zone, endpoint) name := p.recordSetNameForZone(zone, ep)
if p.dryRun { if p.dryRun {
log.Infof( log.Infof(
"Would update %s record named '%s' to '%s' for Azure Private DNS zone '%s'.", "Would update %s record named '%s' to '%s' for Azure Private DNS zone '%s'.",
endpoint.RecordType, ep.RecordType,
name, name,
endpoint.Targets, ep.Targets,
zone, zone,
) )
continue continue
@ -294,19 +292,19 @@ func (p *AzurePrivateDNSProvider) updateRecords(ctx context.Context, updated azu
log.Infof( log.Infof(
"Updating %s record named '%s' to '%s' for Azure Private DNS zone '%s'.", "Updating %s record named '%s' to '%s' for Azure Private DNS zone '%s'.",
endpoint.RecordType, ep.RecordType,
name, name,
endpoint.Targets, ep.Targets,
zone, zone,
) )
recordSet, err := p.newRecordSet(endpoint) recordSet, err := p.newRecordSet(ep)
if err == nil { if err == nil {
_, err = p.recordSetsClient.CreateOrUpdate( _, err = p.recordSetsClient.CreateOrUpdate(
ctx, ctx,
p.resourceGroup, p.resourceGroup,
zone, zone,
privatedns.RecordType(endpoint.RecordType), privatedns.RecordType(ep.RecordType),
name, name,
recordSet, recordSet,
"", "",
@ -316,9 +314,9 @@ func (p *AzurePrivateDNSProvider) updateRecords(ctx context.Context, updated azu
if err != nil { if err != nil {
log.Errorf( log.Errorf(
"Failed to update %s record named '%s' to '%s' for Azure Private DNS zone '%s': %v", "Failed to update %s record named '%s' to '%s' for Azure Private DNS zone '%s': %v",
endpoint.RecordType, ep.RecordType,
name, name,
endpoint.Targets, ep.Targets,
zone, zone,
err, err,
) )

View File

@ -18,16 +18,11 @@ package azure
import ( import (
"context" "context"
"os"
"testing" "testing"
"github.com/Azure/azure-sdk-for-go/services/privatedns/mgmt/2018-09-01/privatedns" "github.com/Azure/azure-sdk-for-go/services/privatedns/mgmt/2018-09-01/privatedns"
"github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/Azure/go-autorest/autorest/azure/auth"
"github.com/Azure/go-autorest/autorest/to" "github.com/Azure/go-autorest/autorest/to"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider" "sigs.k8s.io/external-dns/provider"
@ -255,36 +250,6 @@ func newAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneIDFilter
} }
} }
func validateAzurePrivateDNSClientsResourceManager(t *testing.T, environmentName string, expectedResourceManagerEndpoint string) {
err := os.Setenv(auth.EnvironmentName, environmentName)
if err != nil {
t.Fatal(err)
}
azurePrivateDNSProvider, err := NewAzurePrivateDNSProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), "k8s", "sub", true)
if err != nil {
t.Fatal(err)
}
zonesClientBaseURI := azurePrivateDNSProvider.zonesClient.(privatedns.PrivateZonesClient).BaseURI
recordSetsClientBaseURI := azurePrivateDNSProvider.recordSetsClient.(privatedns.RecordSetsClient).BaseURI
assert.Equal(t, zonesClientBaseURI, expectedResourceManagerEndpoint, "expected and actual resource manager endpoints don't match. expected: %s, got: %s", expectedResourceManagerEndpoint, zonesClientBaseURI)
assert.Equal(t, recordSetsClientBaseURI, expectedResourceManagerEndpoint, "expected and actual resource manager endpoints don't match. expected: %s, got: %s", expectedResourceManagerEndpoint, recordSetsClientBaseURI)
}
func TestNewAzurePrivateDNSProvider(t *testing.T) {
// make sure to reset the environment variables at the end again
originalEnv := os.Getenv(auth.EnvironmentName)
defer os.Setenv(auth.EnvironmentName, originalEnv)
validateAzurePrivateDNSClientsResourceManager(t, "", azure.PublicCloud.ResourceManagerEndpoint)
validateAzurePrivateDNSClientsResourceManager(t, "AZURECHINACLOUD", azure.ChinaCloud.ResourceManagerEndpoint)
validateAzurePrivateDNSClientsResourceManager(t, "AZUREGERMANCLOUD", azure.GermanCloud.ResourceManagerEndpoint)
validateAzurePrivateDNSClientsResourceManager(t, "AZUREUSGOVERNMENTCLOUD", azure.USGovernmentCloud.ResourceManagerEndpoint)
}
func TestAzurePrivateDNSRecord(t *testing.T) { func TestAzurePrivateDNSRecord(t *testing.T) {
provider, err := newMockedAzurePrivateDNSProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), true, "k8s", provider, err := newMockedAzurePrivateDNSProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), true, "k8s",
&[]privatedns.PrivateZone{ &[]privatedns.PrivateZone{

129
provider/azure/config.go Normal file
View File

@ -0,0 +1,129 @@
/*
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 azure
import (
"fmt"
"io/ioutil"
"strings"
"github.com/Azure/go-autorest/autorest/adal"
"github.com/Azure/go-autorest/autorest/azure"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
)
// config represents common config items for Azure DNS and Azure Private DNS
type config struct {
Cloud string `json:"cloud" yaml:"cloud"`
Environment azure.Environment `json:"-" yaml:"-"`
TenantID string `json:"tenantId" yaml:"tenantId"`
SubscriptionID string `json:"subscriptionId" yaml:"subscriptionId"`
ResourceGroup string `json:"resourceGroup" yaml:"resourceGroup"`
Location string `json:"location" yaml:"location"`
ClientID string `json:"aadClientId" yaml:"aadClientId"`
ClientSecret string `json:"aadClientSecret" yaml:"aadClientSecret"`
UseManagedIdentityExtension bool `json:"useManagedIdentityExtension" yaml:"useManagedIdentityExtension"`
UserAssignedIdentityID string `json:"userAssignedIdentityID" yaml:"userAssignedIdentityID"`
}
func getConfig(configFile, resourceGroup, userAssignedIdentityClientID string) (*config, error) {
contents, err := ioutil.ReadFile(configFile)
if err != nil {
return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err)
}
cfg := &config{}
err = yaml.Unmarshal(contents, &cfg)
if err != nil {
return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err)
}
// If a resource group was given, override what was present in the config file
if resourceGroup != "" {
cfg.ResourceGroup = resourceGroup
}
// If userAssignedIdentityClientID is provided explicitly, override existing one in config file
if userAssignedIdentityClientID != "" {
cfg.UserAssignedIdentityID = userAssignedIdentityClientID
}
var environment azure.Environment
if cfg.Cloud == "" {
environment = azure.PublicCloud
} else {
environment, err = azure.EnvironmentFromName(cfg.Cloud)
if err != nil {
return nil, fmt.Errorf("invalid cloud value '%s': %v", cfg.Cloud, err)
}
}
cfg.Environment = environment
return cfg, nil
}
// getAccessToken retrieves Azure API access token.
func getAccessToken(cfg config, environment azure.Environment) (*adal.ServicePrincipalToken, error) {
// Try to retrieve token with service principal credentials.
// Try to use service principal first, some AKS clusters are in an intermediate state that `UseManagedIdentityExtension` is `true`
// and service principal exists. In this case, we still want to use service principal to authenticate.
if len(cfg.ClientID) > 0 &&
len(cfg.ClientSecret) > 0 &&
// due to some historical reason, for pure MSI cluster,
// they will use "msi" as placeholder in azure.json.
// In this case, we shouldn't try to use SPN to authenticate.
!strings.EqualFold(cfg.ClientID, "msi") &&
!strings.EqualFold(cfg.ClientSecret, "msi") {
log.Info("Using client_id+client_secret to retrieve access token for Azure API.")
oauthConfig, err := adal.NewOAuthConfig(environment.ActiveDirectoryEndpoint, cfg.TenantID)
if err != nil {
return nil, fmt.Errorf("failed to retrieve OAuth config: %v", err)
}
token, err := adal.NewServicePrincipalToken(*oauthConfig, cfg.ClientID, cfg.ClientSecret, environment.ResourceManagerEndpoint)
if err != nil {
return nil, fmt.Errorf("failed to create service principal token: %v", err)
}
return token, nil
}
// Try to retrieve token with MSI.
if cfg.UseManagedIdentityExtension {
log.Info("Using managed identity extension to retrieve access token for Azure API.")
msiEndpoint, err := adal.GetMSIVMEndpoint()
if err != nil {
return nil, fmt.Errorf("failed to get the managed service identity endpoint: %v", err)
}
if cfg.UserAssignedIdentityID != "" {
log.Infof("Resolving to user assigned identity, client id is %s.", cfg.UserAssignedIdentityID)
token, err := adal.NewServicePrincipalTokenFromMSIWithUserAssignedID(msiEndpoint, environment.ServiceManagementEndpoint, cfg.UserAssignedIdentityID)
if err != nil {
return nil, fmt.Errorf("failed to create the managed service identity token: %v", err)
}
return token, nil
}
log.Info("Resolving to system assigned identity.")
token, err := adal.NewServicePrincipalTokenFromMSI(msiEndpoint, environment.ServiceManagementEndpoint)
if err != nil {
return nil, fmt.Errorf("failed to create the managed service identity token: %v", err)
}
return token, nil
}
return nil, fmt.Errorf("no credentials provided for Azure API")
}

View File

@ -0,0 +1,67 @@
/*
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 azure
import (
"fmt"
"github.com/Azure/go-autorest/autorest/azure"
"io/ioutil"
"os"
"reflect"
"testing"
)
func TestGetAzureEnvironmentConfig(t *testing.T) {
tmp, err := ioutil.TempFile("", "azureconf")
if err != nil {
t.Errorf("couldn't write temp file %v", err)
}
defer os.Remove(tmp.Name())
tests := map[string]struct {
cloud string
err error
}{
"AzureChinaCloud": {"AzureChinaCloud", nil},
"AzureGermanCloud": {"AzureGermanCloud", nil},
"AzurePublicCloud": {"", nil},
"AzureUSGovernment": {"AzureUSGovernmentCloud", nil},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
_, _ = tmp.Seek(0, 0)
_, _ = tmp.Write([]byte(fmt.Sprintf(`{"cloud": "%s"}`, test.cloud)))
got, err := getConfig(tmp.Name(), "", "")
if err != nil {
t.Errorf("got unexpected err %v", err)
}
if test.cloud == "" {
test.cloud = "AzurePublicCloud"
}
want, err := azure.EnvironmentFromName(test.cloud)
if err != nil {
t.Errorf("couldn't get azure environment from provided name %v", err)
}
if !reflect.DeepEqual(want, got.Environment) {
t.Errorf("got %v, want %v", got.Environment, want)
}
})
}
}

View File

@ -269,8 +269,13 @@ func endpointToScalewayRecords(zoneName string, ep *endpoint.Endpoint) []*domain
records := []*domain.Record{} records := []*domain.Record{}
for _, target := range ep.Targets { for _, target := range ep.Targets {
finalTargetName := target
if domain.RecordType(ep.RecordType) == domain.RecordTypeCNAME {
finalTargetName = provider.EnsureTrailingDot(target)
}
records = append(records, &domain.Record{ records = append(records, &domain.Record{
Data: target, Data: finalTargetName,
Name: strings.Trim(strings.TrimSuffix(ep.DNSName, zoneName), ". "), Name: strings.Trim(strings.TrimSuffix(ep.DNSName, zoneName), ". "),
Priority: priority, Priority: priority,
TTL: ttl, TTL: ttl,
@ -285,9 +290,14 @@ func endpointToScalewayRecordsChangeDelete(zoneName string, ep *endpoint.Endpoin
records := []*domain.RecordChange{} records := []*domain.RecordChange{}
for _, target := range ep.Targets { for _, target := range ep.Targets {
finalTargetName := target
if domain.RecordType(ep.RecordType) == domain.RecordTypeCNAME {
finalTargetName = provider.EnsureTrailingDot(target)
}
records = append(records, &domain.RecordChange{ records = append(records, &domain.RecordChange{
Delete: &domain.RecordChangeDelete{ Delete: &domain.RecordChangeDelete{
Data: target, Data: finalTargetName,
Name: strings.Trim(strings.TrimSuffix(ep.DNSName, zoneName), ". "), Name: strings.Trim(strings.TrimSuffix(ep.DNSName, zoneName), ". "),
Type: domain.RecordType(ep.RecordType), Type: domain.RecordType(ep.RecordType),
}, },

View File

@ -93,7 +93,7 @@ func (m *mockScalewayDomain) ListDNSZoneRecords(req *domain.ListDNSZoneRecordsRe
Type: domain.RecordTypeA, Type: domain.RecordTypeA,
}, },
{ {
Data: "test.example.com", Data: "test.example.com.",
Name: "two", Name: "two",
TTL: 600, TTL: 600,
Priority: 30, Priority: 30,
@ -330,7 +330,7 @@ func TestScalewayProvider_generateApplyRequests(t *testing.T) {
Add: &domain.RecordChangeAdd{ Add: &domain.RecordChangeAdd{
Records: []*domain.Record{ Records: []*domain.Record{
{ {
Data: "example.com", Data: "example.com.",
Name: "", Name: "",
TTL: 600, TTL: 600,
Type: domain.RecordTypeCNAME, Type: domain.RecordTypeCNAME,

283
source/ambassador_host.go Normal file
View File

@ -0,0 +1,283 @@
/*
Copyright 2020 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"
"sort"
"strings"
"time"
ambassador "github.com/datawire/ambassador/pkg/api/getambassador.io/v2"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/dynamic/dynamicinformer"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/tools/cache"
api "k8s.io/kubernetes/pkg/apis/core"
"sigs.k8s.io/external-dns/endpoint"
)
// ambHostAnnotation is the annotation in the Host that maps to a Service
const ambHostAnnotation = "external-dns.ambassador-service"
// groupName is the group name for the Ambassador API
const groupName = "getambassador.io"
var schemeGroupVersion = schema.GroupVersion{Group: groupName, Version: "v2"}
var ambHostGVR = schemeGroupVersion.WithResource("hosts")
// ambassadorHostSource is an implementation of Source for Ambassador Host objects.
// The IngressRoute implementation uses the spec.virtualHost.fqdn value for the hostname.
// Use targetAnnotationKey to explicitly set Endpoint.
type ambassadorHostSource struct {
dynamicKubeClient dynamic.Interface
kubeClient kubernetes.Interface
namespace string
ambassadorHostInformer informers.GenericInformer
unstructuredConverter *unstructuredConverter
}
// NewAmbassadorHostSource creates a new ambassadorHostSource with the given config.
func NewAmbassadorHostSource(
dynamicKubeClient dynamic.Interface,
kubeClient kubernetes.Interface,
namespace string) (Source, error) {
var err error
// Use shared informer to listen for add/update/delete of Host in the specified namespace.
// Set resync period to 0, to prevent processing when nothing has changed.
informerFactory := dynamicinformer.NewFilteredDynamicSharedInformerFactory(dynamicKubeClient, 0, namespace, nil)
ambassadorHostInformer := informerFactory.ForResource(ambHostGVR)
// Add default resource event handlers to properly initialize informer.
ambassadorHostInformer.Informer().AddEventHandler(
cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
},
},
)
// TODO informer is not explicitly stopped since controller is not passing in its channel.
informerFactory.Start(wait.NeverStop)
// wait for the local cache to be populated.
err = poll(time.Second, 60*time.Second, func() (bool, error) {
return ambassadorHostInformer.Informer().HasSynced(), nil
})
if err != nil {
return nil, errors.Wrapf(err, "failed to sync cache")
}
uc, err := newUnstructuredConverter()
if err != nil {
return nil, errors.Wrapf(err, "failed to setup Unstructured Converter")
}
return &ambassadorHostSource{
dynamicKubeClient: dynamicKubeClient,
kubeClient: kubeClient,
namespace: namespace,
ambassadorHostInformer: ambassadorHostInformer,
unstructuredConverter: uc,
}, nil
}
// Endpoints returns endpoint objects for each host-target combination that should be processed.
// Retrieves all Hosts in the source's namespace(s).
func (sc *ambassadorHostSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
hosts, err := sc.ambassadorHostInformer.Lister().ByNamespace(sc.namespace).List(labels.Everything())
if err != nil {
return nil, err
}
endpoints := []*endpoint.Endpoint{}
for _, hostObj := range hosts {
unstructuredHost, ok := hostObj.(*unstructured.Unstructured)
if !ok {
return nil, errors.New("could not convert")
}
host := &ambassador.Host{}
err := sc.unstructuredConverter.scheme.Convert(unstructuredHost, host, nil)
if err != nil {
return nil, err
}
fullname := fmt.Sprintf("%s/%s", host.Namespace, host.Name)
// look for the "exernal-dns.ambassador-service" annotation. If it is not there then just ignore this `Host`
service, found := host.Annotations[ambHostAnnotation]
if !found {
log.Debugf("Host %s ignored: no annotation %q found", fullname, ambHostAnnotation)
continue
}
targets, err := sc.targetsFromAmbassadorLoadBalancer(ctx, service)
if err != nil {
return nil, err
}
hostEndpoints, err := sc.endpointsFromHost(ctx, host, targets)
if err != nil {
return nil, err
}
if len(hostEndpoints) == 0 {
log.Debugf("No endpoints could be generated from Host %s", fullname)
continue
}
log.Debugf("Endpoints generated from Host: %s: %v", fullname, hostEndpoints)
endpoints = append(endpoints, hostEndpoints...)
}
for _, ep := range endpoints {
sort.Sort(ep.Targets)
}
return endpoints, nil
}
// endpointsFromHost extracts the endpoints from a Host object
func (sc *ambassadorHostSource) endpointsFromHost(ctx context.Context, host *ambassador.Host, targets endpoint.Targets) ([]*endpoint.Endpoint, error) {
var endpoints []*endpoint.Endpoint
providerSpecific := endpoint.ProviderSpecific{}
setIdentifier := ""
annotations := host.Annotations
ttl, err := getTTLFromAnnotations(annotations)
if err != nil {
return nil, err
}
if host.Spec != nil {
hostname := host.Spec.Hostname
if hostname != "" {
endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier)...)
}
}
return endpoints, nil
}
func (sc *ambassadorHostSource) targetsFromAmbassadorLoadBalancer(ctx context.Context, service string) (targets endpoint.Targets, err error) {
lbNamespace, lbName, err := parseAmbLoadBalancerService(service)
if err != nil {
return nil, err
}
svc, err := sc.kubeClient.CoreV1().Services(lbNamespace).Get(ctx, lbName, metav1.GetOptions{})
if err != nil {
return nil, err
}
for _, lb := range svc.Status.LoadBalancer.Ingress {
if lb.IP != "" {
targets = append(targets, lb.IP)
}
if lb.Hostname != "" {
targets = append(targets, lb.Hostname)
}
}
return
}
// parseAmbLoadBalancerService returns a name/namespace tuple from the annotation in
// an Ambassador Host CRD
//
// This is a thing because Ambassador has historically supported cross-namespace
// references using a name.namespace syntax, but here we want to also support
// namespace/name.
//
// Returns namespace, name, error.
func parseAmbLoadBalancerService(service string) (namespace, name string, err error) {
// Start by assuming that we have namespace/name.
parts := strings.Split(service, "/")
if len(parts) == 1 {
// No "/" at all, so let's try for name.namespace. To be consistent with the
// rest of Ambassador, use SplitN to limit this to one split, so that e.g.
// svc.foo.bar uses service "svc" in namespace "foo.bar".
parts = strings.SplitN(service, ".", 2)
if len(parts) == 2 {
// We got a namespace, great.
name := parts[0]
namespace := parts[1]
return namespace, name, nil
}
// If here, we have no separator, so the whole string is the service, and
// we can assume the default namespace.
name := service
namespace := api.NamespaceDefault
return namespace, name, nil
} else if len(parts) == 2 {
// This is "namespace/name". Note that the name could be qualified,
// which is fine.
namespace := parts[0]
name := parts[1]
return namespace, name, nil
}
// If we got here, this string is simply ill-formatted. Return an error.
return "", "", errors.New(fmt.Sprintf("invalid external-dns service: %s", service))
}
func (sc *ambassadorHostSource) AddEventHandler(ctx context.Context, handler func()) {
}
// unstructuredConverter handles conversions between unstructured.Unstructured and Ambassador types
type unstructuredConverter struct {
// scheme holds an initializer for converting Unstructured to a type
scheme *runtime.Scheme
}
// newUnstructuredConverter returns a new unstructuredConverter initialized
func newUnstructuredConverter() (*unstructuredConverter, error) {
uc := &unstructuredConverter{
scheme: runtime.NewScheme(),
}
// Setup converter to understand custom CRD types
ambassador.AddToScheme(uc.scheme)
// Add the core types we need
if err := scheme.AddToScheme(uc.scheme); err != nil {
return nil, err
}
return uc, nil
}

View File

@ -0,0 +1,78 @@
/*
Copyright 2019 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 (
"testing"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
type AmbassadorSuite struct {
suite.Suite
}
func TestAmbassadorSource(t *testing.T) {
suite.Run(t, new(AmbassadorSuite))
t.Run("Interface", testAmbassadorSourceImplementsSource)
}
// testAmbassadorSourceImplementsSource tests that ambassadorHostSource is a valid Source.
func testAmbassadorSourceImplementsSource(t *testing.T) {
require.Implements(t, (*Source)(nil), new(ambassadorHostSource))
}
// TestParseAmbLoadBalancerService tests our parsing of Ambassador service info.
func TestParseAmbLoadBalancerService(t *testing.T) {
vectors := []struct {
input string
ns string
svc string
errstr string
}{
{"svc", "default", "svc", ""},
{"ns/svc", "ns", "svc", ""},
{"svc.ns", "ns", "svc", ""},
{"svc.ns.foo.bar", "ns.foo.bar", "svc", ""},
{"ns/svc/foo/bar", "", "", "invalid external-dns service: ns/svc/foo/bar"},
{"ns/svc/foo.bar", "", "", "invalid external-dns service: ns/svc/foo.bar"},
{"ns.foo/svc/bar", "", "", "invalid external-dns service: ns.foo/svc/bar"},
}
for _, v := range vectors {
ns, svc, err := parseAmbLoadBalancerService(v.input)
errstr := ""
if err != nil {
errstr = err.Error()
}
if v.ns != ns {
t.Errorf("%s: got ns \"%s\", wanted \"%s\"", v.input, ns, v.ns)
}
if v.svc != svc {
t.Errorf("%s: got svc \"%s\", wanted \"%s\"", v.input, svc, v.svc)
}
if v.errstr != errstr {
t.Errorf("%s: got err \"%s\", wanted \"%s\"", v.input, errstr, v.errstr)
}
}
}

View File

@ -628,14 +628,17 @@ func (sc *serviceSource) extractNodePortEndpoints(svc *v1.Service, nodeTargets e
for _, port := range svc.Spec.Ports { for _, port := range svc.Spec.Ports {
if port.NodePort > 0 { if port.NodePort > 0 {
// following the RFC 2782, SRV record must have a following format
// _service._proto.name. TTL class SRV priority weight port
// see https://en.wikipedia.org/wiki/SRV_record
// build a target with a priority of 0, weight of 0, and pointing the given port on the given host // build a target with a priority of 0, weight of 0, and pointing the given port on the given host
target := fmt.Sprintf("0 50 %d %s", port.NodePort, hostname) target := fmt.Sprintf("0 50 %d %s", port.NodePort, hostname)
// figure out the portname // take the service name from the K8s Service object
portName := port.Name // it is safe to use since it is DNS compatible
if portName == "" { // see https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names
portName = fmt.Sprintf("%d", port.NodePort) serviceName := svc.ObjectMeta.Name
}
// figure out the protocol // figure out the protocol
protocol := strings.ToLower(string(port.Protocol)) protocol := strings.ToLower(string(port.Protocol))
@ -643,7 +646,7 @@ func (sc *serviceSource) extractNodePortEndpoints(svc *v1.Service, nodeTargets e
protocol = "tcp" protocol = "tcp"
} }
recordName := fmt.Sprintf("_%s._%s.%s", portName, protocol, hostname) recordName := fmt.Sprintf("_%s._%s.%s", serviceName, protocol, hostname)
var ep *endpoint.Endpoint var ep *endpoint.Endpoint
if ttl.IsConfigured() { if ttl.IsConfigured() {

View File

@ -1642,7 +1642,7 @@ func TestNodePortServices(t *testing.T) {
}, },
nil, nil,
[]*endpoint.Endpoint{ []*endpoint.Endpoint{
{DNSName: "_30192._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV}, {DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}, RecordType: endpoint.RecordTypeA}, {DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}, RecordType: endpoint.RecordTypeA},
}, },
false, false,
@ -1729,7 +1729,7 @@ func TestNodePortServices(t *testing.T) {
map[string]string{}, map[string]string{},
nil, nil,
[]*endpoint.Endpoint{ []*endpoint.Endpoint{
{DNSName: "_30192._tcp.foo.bar.example.com", Targets: endpoint.Targets{"0 50 30192 foo.bar.example.com"}, RecordType: endpoint.RecordTypeSRV}, {DNSName: "_foo._tcp.foo.bar.example.com", Targets: endpoint.Targets{"0 50 30192 foo.bar.example.com"}, RecordType: endpoint.RecordTypeSRV},
{DNSName: "foo.bar.example.com", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}, RecordType: endpoint.RecordTypeA}, {DNSName: "foo.bar.example.com", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}, RecordType: endpoint.RecordTypeA},
}, },
false, false,
@ -1775,7 +1775,7 @@ func TestNodePortServices(t *testing.T) {
}, },
nil, nil,
[]*endpoint.Endpoint{ []*endpoint.Endpoint{
{DNSName: "_30192._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV}, {DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"10.0.1.1", "10.0.1.2"}, RecordType: endpoint.RecordTypeA}, {DNSName: "foo.example.org", Targets: endpoint.Targets{"10.0.1.1", "10.0.1.2"}, RecordType: endpoint.RecordTypeA},
}, },
false, false,
@ -1819,7 +1819,7 @@ func TestNodePortServices(t *testing.T) {
}, },
nil, nil,
[]*endpoint.Endpoint{ []*endpoint.Endpoint{
{DNSName: "_30192._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV}, {DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.2"}, RecordType: endpoint.RecordTypeA}, {DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.2"}, RecordType: endpoint.RecordTypeA},
}, },
false, false,
@ -1865,7 +1865,7 @@ func TestNodePortServices(t *testing.T) {
}, },
nil, nil,
[]*endpoint.Endpoint{ []*endpoint.Endpoint{
{DNSName: "_30192._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV}, {DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.2"}, RecordType: endpoint.RecordTypeA}, {DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.2"}, RecordType: endpoint.RecordTypeA},
}, },
false, false,
@ -1912,7 +1912,7 @@ func TestNodePortServices(t *testing.T) {
}, },
nil, nil,
[]*endpoint.Endpoint{ []*endpoint.Endpoint{
{DNSName: "_30192._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV}, {DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"10.0.1.1", "10.0.1.2"}, RecordType: endpoint.RecordTypeA}, {DNSName: "foo.example.org", Targets: endpoint.Targets{"10.0.1.1", "10.0.1.2"}, RecordType: endpoint.RecordTypeA},
}, },
false, false,
@ -1959,7 +1959,7 @@ func TestNodePortServices(t *testing.T) {
}, },
nil, nil,
[]*endpoint.Endpoint{ []*endpoint.Endpoint{
{DNSName: "_30192._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV}, {DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}, RecordType: endpoint.RecordTypeA}, {DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}, RecordType: endpoint.RecordTypeA},
}, },
false, false,

View File

@ -83,12 +83,12 @@ type SingletonClientGenerator struct {
kubeClient kubernetes.Interface kubeClient kubernetes.Interface
istioClient *istioclient.Clientset istioClient *istioclient.Clientset
cfClient *cfclient.Client cfClient *cfclient.Client
contourClient dynamic.Interface dynKubeClient dynamic.Interface
openshiftClient openshift.Interface openshiftClient openshift.Interface
kubeOnce sync.Once kubeOnce sync.Once
istioOnce sync.Once istioOnce sync.Once
cfOnce sync.Once cfOnce sync.Once
contourOnce sync.Once dynCliOnce sync.Once
openshiftOnce sync.Once openshiftOnce sync.Once
} }
@ -134,13 +134,13 @@ func NewCFClient(cfAPIEndpoint string, cfUsername string, cfPassword string) (*c
return client, nil return client, nil
} }
// DynamicKubernetesClient generates a contour client if it was not created before // DynamicKubernetesClient generates a dynamic client if it was not created before
func (p *SingletonClientGenerator) DynamicKubernetesClient() (dynamic.Interface, error) { func (p *SingletonClientGenerator) DynamicKubernetesClient() (dynamic.Interface, error) {
var err error var err error
p.contourOnce.Do(func() { p.dynCliOnce.Do(func() {
p.contourClient, err = NewDynamicKubernetesClient(p.KubeConfig, p.APIServerURL, p.RequestTimeout) p.dynKubeClient, err = NewDynamicKubernetesClient(p.KubeConfig, p.APIServerURL, p.RequestTimeout)
}) })
return p.contourClient, err return p.dynKubeClient, err
} }
// OpenShiftClient generates an openshift client if it was not created before // OpenShiftClient generates an openshift client if it was not created before
@ -213,6 +213,16 @@ func BuildWithConfig(source string, p ClientGenerator, cfg *Config) (Source, err
return nil, err return nil, err
} }
return NewCloudFoundrySource(cfClient) return NewCloudFoundrySource(cfClient)
case "ambassador-host":
kubernetesClient, err := p.KubeClient()
if err != nil {
return nil, err
}
dynamicClient, err := p.DynamicKubernetesClient()
if err != nil {
return nil, err
}
return NewAmbassadorHostSource(dynamicClient, kubernetesClient, cfg.Namespace)
case "contour-ingressroute": case "contour-ingressroute":
kubernetesClient, err := p.KubeClient() kubernetesClient, err := p.KubeClient()
if err != nil { if err != nil {