-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
014e8da
commit 6339d8b
Showing
8 changed files
with
2,231 additions
and
0 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
}) | ||
} | ||
} |