Skip to content

Commit

Permalink
copy azure external-dns provider
Browse files Browse the repository at this point in the history
  • Loading branch information
philbrookes authored and ehearneRedHat committed Jul 23, 2024
1 parent 014e8da commit 6339d8b
Show file tree
Hide file tree
Showing 8 changed files with 2,231 additions and 0 deletions.
462 changes: 462 additions & 0 deletions internal/external-dns/azure/azure.go

Large diffs are not rendered by default.

445 changes: 445 additions & 0 deletions internal/external-dns/azure/azure_private_dns.go

Large diffs are not rendered by default.

432 changes: 432 additions & 0 deletions internal/external-dns/azure/azure_privatedns_test.go

Large diffs are not rendered by default.

558 changes: 558 additions & 0 deletions internal/external-dns/azure/azure_test.go

Large diffs are not rendered by default.

47 changes: 47 additions & 0 deletions internal/external-dns/azure/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
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.
*/

//nolint:staticcheck // Required due to the current dependency on a deprecated version of azure-sdk-for-go
package azure

import (
"fmt"
"strconv"
"strings"

"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
dns "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns"
privatedns "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns"
)

// Helper function (shared with test code)
func parseMxTarget[T dns.MxRecord | privatedns.MxRecord](mxTarget string) (T, error) {
targetParts := strings.SplitN(mxTarget, " ", 2)
if len(targetParts) != 2 {
return T{}, fmt.Errorf("mx target needs to be of form '10 example.com'")
}

preferenceRaw, exchange := targetParts[0], targetParts[1]
preference, err := strconv.ParseInt(preferenceRaw, 10, 32)
if err != nil {
return T{}, fmt.Errorf("invalid preference specified")
}

return T{
Preference: to.Ptr(int32(preference)),
Exchange: to.Ptr(exchange),
}, nil
}
87 changes: 87 additions & 0 deletions internal/external-dns/azure/common_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
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"
"testing"

"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
dns "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns"
privatedns "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns"

"github.com/stretchr/testify/assert"
)

func Test_parseMxTarget(t *testing.T) {
type testCase[T interface {
dns.MxRecord | privatedns.MxRecord
}] struct {
name string
args string
want T
wantErr assert.ErrorAssertionFunc
}

tests := []testCase[dns.MxRecord]{
{
name: "valid mx target",
args: "10 example.com",
want: dns.MxRecord{
Preference: to.Ptr(int32(10)),
Exchange: to.Ptr("example.com"),
},
wantErr: assert.NoError,
},
{
name: "valid mx target with a subdomain",
args: "99 foo-bar.example.com",
want: dns.MxRecord{
Preference: to.Ptr(int32(99)),
Exchange: to.Ptr("foo-bar.example.com"),
},
wantErr: assert.NoError,
},
{
name: "invalid mx target with misplaced preference and exchange",
args: "example.com 10",
want: dns.MxRecord{},
wantErr: assert.Error,
},
{
name: "invalid mx target without preference",
args: "example.com",
want: dns.MxRecord{},
wantErr: assert.Error,
},
{
name: "invalid mx target with non numeric preference",
args: "aa example.com",
want: dns.MxRecord{},
wantErr: assert.Error,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseMxTarget[dns.MxRecord](tt.args)
if !tt.wantErr(t, err, fmt.Sprintf("parseMxTarget(%v)", tt.args)) {
return
}
assert.Equalf(t, tt.want, got, "parseMxTarget(%v)", tt.args)
})
}
}
154 changes: 154 additions & 0 deletions internal/external-dns/azure/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/*
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"
"os"
"strings"

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
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"`
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"`
UseWorkloadIdentityExtension bool `json:"useWorkloadIdentityExtension" yaml:"useWorkloadIdentityExtension"`
UserAssignedIdentityID string `json:"userAssignedIdentityID" yaml:"userAssignedIdentityID"`
}

func getConfig(configFile, resourceGroup, userAssignedIdentityClientID string) (*config, error) {
contents, err := os.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
}
return cfg, nil
}

// getAccessToken retrieves Azure API access token.
func getCredentials(cfg config) (azcore.TokenCredential, *arm.ClientOptions, error) {
cloudCfg, err := getCloudConfiguration(cfg.Cloud)
if err != nil {
return nil, nil, fmt.Errorf("failed to get cloud configuration: %w", err)
}
clientOpts := azcore.ClientOptions{
Cloud: cloudCfg,
}
armClientOpts := &arm.ClientOptions{
ClientOptions: clientOpts,
}

// 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.")
opts := &azidentity.ClientSecretCredentialOptions{
ClientOptions: clientOpts,
}
cred, err := azidentity.NewClientSecretCredential(cfg.TenantID, cfg.ClientID, cfg.ClientSecret, opts)
if err != nil {
return nil, nil, fmt.Errorf("failed to create service principal token: %w", err)
}
return cred, armClientOpts, nil
}

// Try to retrieve token with Workload Identity.
if cfg.UseWorkloadIdentityExtension {
log.Info("Using workload identity extension to retrieve access token for Azure API.")

wiOpt := azidentity.WorkloadIdentityCredentialOptions{
ClientOptions: clientOpts,
// In a standard scenario, Client ID and Tenant ID are expected to be read from environment variables.
// Though, in certain cases, it might be important to have an option to override those (e.g. when AZURE_TENANT_ID is not set
// through a webhook or azure.workload.identity/client-id service account annotation is absent). When any of those values are
// empty in our config, they will automatically be read from environment variables by azidentity
TenantID: cfg.TenantID,
ClientID: cfg.ClientID,
}

cred, err := azidentity.NewWorkloadIdentityCredential(&wiOpt)
if err != nil {
return nil, nil, fmt.Errorf("failed to create a workload identity token: %w", err)
}

return cred, armClientOpts, nil
}

// Try to retrieve token with MSI.
if cfg.UseManagedIdentityExtension {
log.Info("Using managed identity extension to retrieve access token for Azure API.")
msiOpt := azidentity.ManagedIdentityCredentialOptions{
ClientOptions: clientOpts,
}
if cfg.UserAssignedIdentityID != "" {
msiOpt.ID = azidentity.ClientID(cfg.UserAssignedIdentityID)
}
cred, err := azidentity.NewManagedIdentityCredential(&msiOpt)
if err != nil {
return nil, nil, fmt.Errorf("failed to create the managed service identity token: %w", err)
}
return cred, armClientOpts, nil
}

return nil, nil, fmt.Errorf("no credentials provided for Azure API")
}

func getCloudConfiguration(name string) (cloud.Configuration, error) {
name = strings.ToUpper(name)
switch name {
case "AZURECLOUD", "AZUREPUBLICCLOUD", "":
return cloud.AzurePublic, nil
case "AZUREUSGOVERNMENT", "AZUREUSGOVERNMENTCLOUD":
return cloud.AzureGovernment, nil
case "AZURECHINACLOUD":
return cloud.AzureChina, nil
}
return cloud.Configuration{}, fmt.Errorf("unknown cloud name: %s", name)
}
46 changes: 46 additions & 0 deletions internal/external-dns/azure/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
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 (
"testing"

"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
)

func TestGetCloudConfiguration(t *testing.T) {
tests := map[string]struct {
cloudName string
expected cloud.Configuration
}{
"AzureChinaCloud": {"AzureChinaCloud", cloud.AzureChina},
"AzurePublicCloud": {"", cloud.AzurePublic},
"AzureUSGovernment": {"AzureUSGovernmentCloud", cloud.AzureGovernment},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
cloudCfg, err := getCloudConfiguration(test.cloudName)
if err != nil {
t.Errorf("got unexpected err %v", err)
}
if cloudCfg.ActiveDirectoryAuthorityHost != test.expected.ActiveDirectoryAuthorityHost {
t.Errorf("got %v, want %v", cloudCfg, test.expected)
}
})
}
}

0 comments on commit 6339d8b

Please sign in to comment.