diff --git a/staging/src/k8s.io/legacy-cloud-providers/azure/BUILD b/staging/src/k8s.io/legacy-cloud-providers/azure/BUILD index 1a315a5410f..71509f32feb 100644 --- a/staging/src/k8s.io/legacy-cloud-providers/azure/BUILD +++ b/staging/src/k8s.io/legacy-cloud-providers/azure/BUILD @@ -14,6 +14,7 @@ go_library( "azure_blobDiskController.go", "azure_cache.go", "azure_client.go", + "azure_config.go", "azure_controller_common.go", "azure_controller_standard.go", "azure_controller_vmss.go", @@ -39,6 +40,7 @@ go_library( deps = [ "//staging/src/k8s.io/api/core/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/api/resource:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/types:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/errors:go_default_library", @@ -80,6 +82,7 @@ go_test( srcs = [ "azure_backoff_test.go", "azure_cache_test.go", + "azure_config_test.go", "azure_controller_common_test.go", "azure_instances_test.go", "azure_loadbalancer_test.go", @@ -100,6 +103,7 @@ go_test( "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/types:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library", + "//staging/src/k8s.io/client-go/kubernetes/fake:go_default_library", "//staging/src/k8s.io/client-go/tools/record:go_default_library", "//staging/src/k8s.io/cloud-provider:go_default_library", "//staging/src/k8s.io/cloud-provider/service/helpers:go_default_library", @@ -110,6 +114,7 @@ go_test( "//vendor/github.com/Azure/go-autorest/autorest:go_default_library", "//vendor/github.com/Azure/go-autorest/autorest/to:go_default_library", "//vendor/github.com/stretchr/testify/assert:go_default_library", + "//vendor/sigs.k8s.io/yaml:go_default_library", ], ) diff --git a/staging/src/k8s.io/legacy-cloud-providers/azure/auth/azure_auth.go b/staging/src/k8s.io/legacy-cloud-providers/azure/auth/azure_auth.go index 2e051d47b7b..6ca04436f15 100644 --- a/staging/src/k8s.io/legacy-cloud-providers/azure/auth/azure_auth.go +++ b/staging/src/k8s.io/legacy-cloud-providers/azure/auth/azure_auth.go @@ -36,25 +36,25 @@ var ( // AzureAuthConfig holds auth related part of cloud config type AzureAuthConfig struct { // The cloud environment identifier. Takes values from https://github.com/Azure/go-autorest/blob/ec5f4903f77ed9927ac95b19ab8e44ada64c1356/autorest/azure/environments.go#L13 - Cloud string `json:"cloud" yaml:"cloud"` + Cloud string `json:"cloud,omitempty" yaml:"cloud,omitempty"` // The AAD Tenant ID for the Subscription that the cluster is deployed in - TenantID string `json:"tenantId" yaml:"tenantId"` + TenantID string `json:"tenantId,omitempty" yaml:"tenantId,omitempty"` // The ClientID for an AAD application with RBAC access to talk to Azure RM APIs - AADClientID string `json:"aadClientId" yaml:"aadClientId"` + AADClientID string `json:"aadClientId,omitempty" yaml:"aadClientId,omitempty"` // The ClientSecret for an AAD application with RBAC access to talk to Azure RM APIs - AADClientSecret string `json:"aadClientSecret" yaml:"aadClientSecret"` + AADClientSecret string `json:"aadClientSecret,omitempty" yaml:"aadClientSecret,omitempty"` // The path of a client certificate for an AAD application with RBAC access to talk to Azure RM APIs - AADClientCertPath string `json:"aadClientCertPath" yaml:"aadClientCertPath"` + AADClientCertPath string `json:"aadClientCertPath,omitempty" yaml:"aadClientCertPath,omitempty"` // The password of the client certificate for an AAD application with RBAC access to talk to Azure RM APIs - AADClientCertPassword string `json:"aadClientCertPassword" yaml:"aadClientCertPassword"` + AADClientCertPassword string `json:"aadClientCertPassword,omitempty" yaml:"aadClientCertPassword,omitempty"` // Use managed service identity for the virtual machine to access Azure ARM APIs - UseManagedIdentityExtension bool `json:"useManagedIdentityExtension" yaml:"useManagedIdentityExtension"` + UseManagedIdentityExtension bool `json:"useManagedIdentityExtension,omitempty" yaml:"useManagedIdentityExtension,omitempty"` // UserAssignedIdentityID contains the Client ID of the user assigned MSI which is assigned to the underlying VMs. If empty the user assigned identity is not used. // More details of the user assigned identity can be found at: https://docs.microsoft.com/en-us/azure/active-directory/managed-service-identity/overview // For the user assigned identity specified here to be used, the UseManagedIdentityExtension has to be set to true. - UserAssignedIdentityID string `json:"userAssignedIdentityID" yaml:"userAssignedIdentityID"` + UserAssignedIdentityID string `json:"userAssignedIdentityID,omitempty" yaml:"userAssignedIdentityID,omitempty"` // The ID of the Azure Subscription that the cluster is deployed in - SubscriptionID string `json:"subscriptionId" yaml:"subscriptionId"` + SubscriptionID string `json:"subscriptionId,omitempty" yaml:"subscriptionId,omitempty"` } // GetServicePrincipalToken creates a new service principal token based on the configuration diff --git a/staging/src/k8s.io/legacy-cloud-providers/azure/azure.go b/staging/src/k8s.io/legacy-cloud-providers/azure/azure.go index 48585179f8f..161aed3621c 100644 --- a/staging/src/k8s.io/legacy-cloud-providers/azure/azure.go +++ b/staging/src/k8s.io/legacy-cloud-providers/azure/azure.go @@ -36,13 +36,13 @@ import ( "k8s.io/client-go/tools/record" "k8s.io/client-go/util/flowcontrol" cloudprovider "k8s.io/cloud-provider" + "k8s.io/klog" "k8s.io/legacy-cloud-providers/azure/auth" + "sigs.k8s.io/yaml" "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2019-03-01/compute" "github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/azure" - "k8s.io/klog" - "sigs.k8s.io/yaml" ) const ( @@ -83,78 +83,81 @@ type Config struct { auth.AzureAuthConfig // The name of the resource group that the cluster is deployed in - ResourceGroup string `json:"resourceGroup" yaml:"resourceGroup"` + ResourceGroup string `json:"resourceGroup,omitempty" yaml:"resourceGroup,omitempty"` // The location of the resource group that the cluster is deployed in - Location string `json:"location" yaml:"location"` + Location string `json:"location,omitempty" yaml:"location,omitempty"` // The name of the VNet that the cluster is deployed in - VnetName string `json:"vnetName" yaml:"vnetName"` + VnetName string `json:"vnetName,omitempty" yaml:"vnetName,omitempty"` // The name of the resource group that the Vnet is deployed in - VnetResourceGroup string `json:"vnetResourceGroup" yaml:"vnetResourceGroup"` + VnetResourceGroup string `json:"vnetResourceGroup,omitempty" yaml:"vnetResourceGroup,omitempty"` // The name of the subnet that the cluster is deployed in - SubnetName string `json:"subnetName" yaml:"subnetName"` + SubnetName string `json:"subnetName,omitempty" yaml:"subnetName,omitempty"` // The name of the security group attached to the cluster's subnet - SecurityGroupName string `json:"securityGroupName" yaml:"securityGroupName"` + SecurityGroupName string `json:"securityGroupName,omitempty" yaml:"securityGroupName,omitempty"` // (Optional in 1.6) The name of the route table attached to the subnet that the cluster is deployed in - RouteTableName string `json:"routeTableName" yaml:"routeTableName"` + RouteTableName string `json:"routeTableName,omitempty" yaml:"routeTableName,omitempty"` // The name of the resource group that the RouteTable is deployed in - RouteTableResourceGroup string `json:"routeTableResourceGroup" yaml:"routeTableResourceGroup"` + RouteTableResourceGroup string `json:"routeTableResourceGroup,omitempty" yaml:"routeTableResourceGroup,omitempty"` // (Optional) The name of the availability set that should be used as the load balancer backend // If this is set, the Azure cloudprovider will only add nodes from that availability set to the load // balancer backend pool. If this is not set, and multiple agent pools (availability sets) are used, then // the cloudprovider will try to add all nodes to a single backend pool which is forbidden. // In other words, if you use multiple agent pools (availability sets), you MUST set this field. - PrimaryAvailabilitySetName string `json:"primaryAvailabilitySetName" yaml:"primaryAvailabilitySetName"` + PrimaryAvailabilitySetName string `json:"primaryAvailabilitySetName,omitempty" yaml:"primaryAvailabilitySetName,omitempty"` // The type of azure nodes. Candidate values are: vmss and standard. // If not set, it will be default to standard. - VMType string `json:"vmType" yaml:"vmType"` + VMType string `json:"vmType,omitempty" yaml:"vmType,omitempty"` // The name of the scale set that should be used as the load balancer backend. // If this is set, the Azure cloudprovider will only add nodes from that scale set to the load // balancer backend pool. If this is not set, and multiple agent pools (scale sets) are used, then // the cloudprovider will try to add all nodes to a single backend pool which is forbidden. // In other words, if you use multiple agent pools (scale sets), you MUST set this field. - PrimaryScaleSetName string `json:"primaryScaleSetName" yaml:"primaryScaleSetName"` + PrimaryScaleSetName string `json:"primaryScaleSetName,omitempty" yaml:"primaryScaleSetName,omitempty"` // Enable exponential backoff to manage resource request retries - CloudProviderBackoff bool `json:"cloudProviderBackoff" yaml:"cloudProviderBackoff"` + CloudProviderBackoff bool `json:"cloudProviderBackoff,omitempty" yaml:"cloudProviderBackoff,omitempty"` // Backoff retry limit - CloudProviderBackoffRetries int `json:"cloudProviderBackoffRetries" yaml:"cloudProviderBackoffRetries"` + CloudProviderBackoffRetries int `json:"cloudProviderBackoffRetries,omitempty" yaml:"cloudProviderBackoffRetries,omitempty"` // Backoff exponent - CloudProviderBackoffExponent float64 `json:"cloudProviderBackoffExponent" yaml:"cloudProviderBackoffExponent"` + CloudProviderBackoffExponent float64 `json:"cloudProviderBackoffExponent,omitempty" yaml:"cloudProviderBackoffExponent,omitempty"` // Backoff duration - CloudProviderBackoffDuration int `json:"cloudProviderBackoffDuration" yaml:"cloudProviderBackoffDuration"` + CloudProviderBackoffDuration int `json:"cloudProviderBackoffDuration,omitempty" yaml:"cloudProviderBackoffDuration,omitempty"` // Backoff jitter - CloudProviderBackoffJitter float64 `json:"cloudProviderBackoffJitter" yaml:"cloudProviderBackoffJitter"` + CloudProviderBackoffJitter float64 `json:"cloudProviderBackoffJitter,omitempty" yaml:"cloudProviderBackoffJitter,omitempty"` // Backoff mode, options are v2 and default. // * default means two-layer backoff retrying, one in the cloud provider and the other in the Azure SDK. // * v2 means only backoff in the Azure SDK is used. In such mode, CloudProviderBackoffDuration and // CloudProviderBackoffJitter are omitted. // "default" will be used if not specified. - CloudProviderBackoffMode string `json:"cloudProviderBackoffMode" yaml:"cloudProviderBackoffMode"` + CloudProviderBackoffMode string `json:"cloudProviderBackoffMode,omitempty" yaml:"cloudProviderBackoffMode,omitempty"` // Enable rate limiting - CloudProviderRateLimit bool `json:"cloudProviderRateLimit" yaml:"cloudProviderRateLimit"` + CloudProviderRateLimit bool `json:"cloudProviderRateLimit,omitempty" yaml:"cloudProviderRateLimit,omitempty"` // Rate limit QPS (Read) - CloudProviderRateLimitQPS float32 `json:"cloudProviderRateLimitQPS" yaml:"cloudProviderRateLimitQPS"` + CloudProviderRateLimitQPS float32 `json:"cloudProviderRateLimitQPS,omitempty" yaml:"cloudProviderRateLimitQPS,omitempty"` // Rate limit Bucket Size - CloudProviderRateLimitBucket int `json:"cloudProviderRateLimitBucket" yaml:"cloudProviderRateLimitBucket"` + CloudProviderRateLimitBucket int `json:"cloudProviderRateLimitBucket,omitempty" yaml:"cloudProviderRateLimitBucket,omitempty"` // Rate limit QPS (Write) - CloudProviderRateLimitQPSWrite float32 `json:"cloudProviderRateLimitQPSWrite" yaml:"cloudProviderRateLimitQPSWrite"` + CloudProviderRateLimitQPSWrite float32 `json:"cloudProviderRateLimitQPSWrite,omitempty" yaml:"cloudProviderRateLimitQPSWrite,omitempty"` // Rate limit Bucket Size - CloudProviderRateLimitBucketWrite int `json:"cloudProviderRateLimitBucketWrite" yaml:"cloudProviderRateLimitBucketWrite"` + CloudProviderRateLimitBucketWrite int `json:"cloudProviderRateLimitBucketWrite,omitempty" yaml:"cloudProviderRateLimitBucketWrite,omitempty"` // Use instance metadata service where possible - UseInstanceMetadata bool `json:"useInstanceMetadata" yaml:"useInstanceMetadata"` + UseInstanceMetadata bool `json:"useInstanceMetadata,omitempty" yaml:"useInstanceMetadata,omitempty"` // Sku of Load Balancer and Public IP. Candidate values are: basic and standard. // If not set, it will be default to basic. - LoadBalancerSku string `json:"loadBalancerSku" yaml:"loadBalancerSku"` + LoadBalancerSku string `json:"loadBalancerSku,omitempty" yaml:"loadBalancerSku,omitempty"` // ExcludeMasterFromStandardLB excludes master nodes from standard load balancer. // If not set, it will be default to true. - ExcludeMasterFromStandardLB *bool `json:"excludeMasterFromStandardLB" yaml:"excludeMasterFromStandardLB"` + ExcludeMasterFromStandardLB *bool `json:"excludeMasterFromStandardLB,omitempty" yaml:"excludeMasterFromStandardLB,omitempty"` // DisableOutboundSNAT disables the outbound SNAT for public load balancer rules. // It should only be set when loadBalancerSku is standard. If not set, it will be default to false. - DisableOutboundSNAT *bool `json:"disableOutboundSNAT" yaml:"disableOutboundSNAT"` + DisableOutboundSNAT *bool `json:"disableOutboundSNAT,omitempty" yaml:"disableOutboundSNAT,omitempty"` // Maximum allowed LoadBalancer Rule Count is the limit enforced by Azure Load balancer - MaximumLoadBalancerRuleCount int `json:"maximumLoadBalancerRuleCount" yaml:"maximumLoadBalancerRuleCount"` + MaximumLoadBalancerRuleCount int `json:"maximumLoadBalancerRuleCount,omitempty" yaml:"maximumLoadBalancerRuleCount,omitempty"` + + // The cloud configure type for Azure cloud provider. Supported values are file, secret and merge. + CloudConfigType cloudConfigType `json:"cloudConfigType,omitempty" yaml:"cloudConfigType,omitempty"` } var _ cloudprovider.Interface = (*Cloud)(nil) @@ -233,6 +236,28 @@ func NewCloud(configReader io.Reader) (cloudprovider.Interface, error) { return nil, err } + az := &Cloud{ + nodeZones: map[string]sets.String{}, + nodeResourceGroups: map[string]string{}, + unmanagedNodes: sets.NewString(), + routeCIDRs: map[string]string{}, + } + err = az.InitializeCloudFromConfig(config, false) + if err != nil { + return nil, err + } + + return az, nil +} + +// InitializeCloudFromConfig initializes the Cloud from config. +func (az *Cloud) InitializeCloudFromConfig(config *Config, fromSecret bool) error { + // cloud-config not set, return nil so that it would be initialized from secret. + if config == nil { + klog.Warning("cloud-config is not provided, Azure cloud provider would be initialized from secret") + return nil + } + if config.RouteTableResourceGroup == "" { config.RouteTableResourceGroup = config.ResourceGroup } @@ -242,21 +267,43 @@ func NewCloud(configReader io.Reader) (cloudprovider.Interface, error) { config.VMType = vmTypeStandard } + if config.CloudConfigType == "" { + // The default cloud config type is cloudConfigTypeMerge. + config.CloudConfigType = cloudConfigTypeMerge + } else { + supportedCloudConfigTypes := sets.NewString( + string(cloudConfigTypeMerge), + string(cloudConfigTypeFile), + string(cloudConfigTypeSecret)) + if !supportedCloudConfigTypes.Has(string(config.CloudConfigType)) { + return fmt.Errorf("cloudConfigType %v is not supported, supported values are %v", config.CloudConfigType, supportedCloudConfigTypes.List()) + } + } + env, err := auth.ParseAzureEnvironment(config.Cloud) if err != nil { - return nil, err + return err } servicePrincipalToken, err := auth.GetServicePrincipalToken(&config.AzureAuthConfig, env) if err == auth.ErrorNoAuth { - if !config.UseInstanceMetadata { - // No credentials provided, useInstanceMetadata should be enabled. - return nil, fmt.Errorf("useInstanceMetadata must be enabled without Azure credentials") + // Only controller-manager would lazy-initialize from secret, and credentials are required for such case. + if fromSecret { + err := fmt.Errorf("No credentials provided for Azure cloud provider") + klog.Fatalf("%v", err) + return err + } + + // No credentials provided, useInstanceMetadata should be enabled for Kubelet. + // TODO(feiskyer): print different error message for Kubelet and controller-manager, as they're + // requiring different credential settings. + if !config.UseInstanceMetadata && az.Config.CloudConfigType == cloudConfigTypeFile { + return fmt.Errorf("useInstanceMetadata must be enabled without Azure credentials") } klog.V(2).Infof("Azure cloud provider is starting without credentials") } else if err != nil { - return nil, err + return err } // operationPollRateLimiter.Accept() is a no-op if rate limits are configured off. @@ -351,28 +398,22 @@ func NewCloud(configReader io.Reader) (cloudprovider.Interface, error) { } } else { if config.DisableOutboundSNAT != nil && *config.DisableOutboundSNAT { - return nil, fmt.Errorf("disableOutboundSNAT should only set when loadBalancerSku is standard") + return fmt.Errorf("disableOutboundSNAT should only set when loadBalancerSku is standard") } } - az := Cloud{ - Config: *config, - Environment: *env, - nodeZones: map[string]sets.String{}, - nodeResourceGroups: map[string]string{}, - unmanagedNodes: sets.NewString(), - routeCIDRs: map[string]string{}, - resourceRequestBackoff: resourceRequestBackoff, - } + az.Config = *config + az.Environment = *env + az.resourceRequestBackoff = resourceRequestBackoff az.metadata, err = NewInstanceMetadataService(metadataURL) if err != nil { - return nil, err + return err } // No credentials provided, InstanceMetadataService would be used for getting Azure resources. // Note that this only applies to Kubelet, controller-manager should configure credentials for managing Azure resources. if servicePrincipalToken == nil { - return &az, nil + return nil } // Initialize Azure clients. @@ -407,52 +448,53 @@ func NewCloud(configReader io.Reader) (cloudprovider.Interface, error) { } if strings.EqualFold(vmTypeVMSS, az.Config.VMType) { - az.vmSet, err = newScaleSet(&az) + az.vmSet, err = newScaleSet(az) if err != nil { - return nil, err + return err } } else { - az.vmSet = newAvailabilitySet(&az) + az.vmSet = newAvailabilitySet(az) } az.vmCache, err = az.newVMCache() if err != nil { - return nil, err + return err } az.lbCache, err = az.newLBCache() if err != nil { - return nil, err + return err } az.nsgCache, err = az.newNSGCache() if err != nil { - return nil, err + return err } az.rtCache, err = az.newRouteTableCache() if err != nil { - return nil, err + return err } - if err := initDiskControllers(&az); err != nil { - return nil, err + if err := initDiskControllers(az); err != nil { + return err } - return &az, nil + + return nil } // parseConfig returns a parsed configuration for an Azure cloudprovider config file func parseConfig(configReader io.Reader) (*Config, error) { var config Config - if configReader == nil { - return &config, nil + return nil, nil } configContents, err := ioutil.ReadAll(configReader) if err != nil { return nil, err } + err = yaml.Unmarshal(configContents, &config) if err != nil { return nil, err @@ -470,6 +512,7 @@ func (az *Cloud) Initialize(clientBuilder cloudprovider.ControllerClientBuilder, az.eventBroadcaster = record.NewBroadcaster() az.eventBroadcaster.StartRecordingToSink(&v1core.EventSinkImpl{Interface: az.kubeClient.CoreV1().Events("")}) az.eventRecorder = az.eventBroadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: "azure-cloud-provider"}) + az.InitializeCloudFromSecret() } // LoadBalancer returns a balancer interface. Also returns true if the interface is supported, false otherwise. diff --git a/staging/src/k8s.io/legacy-cloud-providers/azure/azure_config.go b/staging/src/k8s.io/legacy-cloud-providers/azure/azure_config.go new file mode 100644 index 00000000000..569f44c48b1 --- /dev/null +++ b/staging/src/k8s.io/legacy-cloud-providers/azure/azure_config.go @@ -0,0 +1,91 @@ +/* +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 azure + +import ( + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/klog" + "sigs.k8s.io/yaml" +) + +const ( + cloudConfigNamespace = "kube-system" + cloudConfigKey = "cloud-config" + cloudConfigSecretName = "azure-cloud-provider" +) + +// The config type for Azure cloud provider secret. Supported values are: +// * file : The values are read from local cloud-config file. +// * secret : The values from secret would override all configures from local cloud-config file. +// * merge : The values from secret would override only configurations that are explicitly set in the secret. This is the default value. +type cloudConfigType string + +const ( + cloudConfigTypeFile cloudConfigType = "file" + cloudConfigTypeSecret cloudConfigType = "secret" + cloudConfigTypeMerge cloudConfigType = "merge" +) + +// InitializeCloudFromSecret initializes Azure cloud provider from Kubernetes secret. +func (az *Cloud) InitializeCloudFromSecret() { + config, err := az.getConfigFromSecret() + if err != nil { + klog.Warningf("Failed to get cloud-config from secret: %v, skip initializing from secret", err) + return + } + + if config == nil { + // Skip re-initialization if the config is not override. + return + } + + if err := az.InitializeCloudFromConfig(config, true); err != nil { + klog.Errorf("Failed to initialize Azure cloud provider: %v", err) + } +} + +func (az *Cloud) getConfigFromSecret() (*Config, error) { + // Read config from file and no override, return nil. + if az.Config.CloudConfigType == cloudConfigTypeFile { + return nil, nil + } + + secret, err := az.kubeClient.CoreV1().Secrets(cloudConfigNamespace).Get(cloudConfigSecretName, metav1.GetOptions{}) + if err != nil { + return nil, fmt.Errorf("Failed to get secret %s: %v", cloudConfigSecretName, err) + } + + cloudConfigData, ok := secret.Data[cloudConfigKey] + if !ok { + return nil, fmt.Errorf("cloud-config is not set in the secret (%s)", cloudConfigSecretName) + } + + config := Config{} + if az.Config.CloudConfigType == "" || az.Config.CloudConfigType == cloudConfigTypeMerge { + // Merge cloud config, set default value to existing config. + config = az.Config + } + + err = yaml.Unmarshal(cloudConfigData, &config) + if err != nil { + return nil, fmt.Errorf("Failed to parse Azure cloud-config: %v", err) + } + + return &config, nil +} diff --git a/staging/src/k8s.io/legacy-cloud-providers/azure/azure_config_test.go b/staging/src/k8s.io/legacy-cloud-providers/azure/azure_config_test.go new file mode 100644 index 00000000000..e505900d491 --- /dev/null +++ b/staging/src/k8s.io/legacy-cloud-providers/azure/azure_config_test.go @@ -0,0 +1,168 @@ +/* +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 azure + +import ( + "testing" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + fakeclient "k8s.io/client-go/kubernetes/fake" + "k8s.io/legacy-cloud-providers/azure/auth" + "sigs.k8s.io/yaml" + + "github.com/Azure/go-autorest/autorest/to" + "github.com/stretchr/testify/assert" +) + +func getTestConfig() *Config { + return &Config{ + AzureAuthConfig: auth.AzureAuthConfig{ + TenantID: "TenantID", + SubscriptionID: "SubscriptionID", + AADClientID: "AADClientID", + AADClientSecret: "AADClientSecret", + }, + ResourceGroup: "ResourceGroup", + RouteTableName: "RouteTableName", + RouteTableResourceGroup: "RouteTableResourceGroup", + Location: "Location", + SubnetName: "SubnetName", + VnetName: "VnetName", + PrimaryAvailabilitySetName: "PrimaryAvailabilitySetName", + PrimaryScaleSetName: "PrimaryScaleSetName", + LoadBalancerSku: "LoadBalancerSku", + ExcludeMasterFromStandardLB: to.BoolPtr(true), + } +} + +func getTestCloudConfigTypeSecretConfig() *Config { + return &Config{ + AzureAuthConfig: auth.AzureAuthConfig{ + TenantID: "TenantID", + SubscriptionID: "SubscriptionID", + }, + ResourceGroup: "ResourceGroup", + RouteTableName: "RouteTableName", + RouteTableResourceGroup: "RouteTableResourceGroup", + SecurityGroupName: "SecurityGroupName", + CloudConfigType: cloudConfigTypeSecret, + } +} + +func getTestCloudConfigTypeMergeConfig() *Config { + return &Config{ + AzureAuthConfig: auth.AzureAuthConfig{ + TenantID: "TenantID", + SubscriptionID: "SubscriptionID", + }, + ResourceGroup: "ResourceGroup", + RouteTableName: "RouteTableName", + RouteTableResourceGroup: "RouteTableResourceGroup", + SecurityGroupName: "SecurityGroupName", + CloudConfigType: cloudConfigTypeMerge, + } +} + +func getTestCloudConfigTypeMergeConfigExpected() *Config { + config := getTestConfig() + config.SecurityGroupName = "SecurityGroupName" + config.CloudConfigType = cloudConfigTypeMerge + return config +} + +func TestGetConfigFromSecret(t *testing.T) { + emptyConfig := &Config{} + tests := []struct { + name string + existingConfig *Config + secretConfig *Config + expected *Config + expectErr bool + }{ + { + name: "Azure config shouldn't be override when cloud config type is file", + existingConfig: &Config{ + ResourceGroup: "ResourceGroup1", + CloudConfigType: cloudConfigTypeFile, + }, + secretConfig: getTestConfig(), + expected: nil, + }, + { + name: "Azure config should be override when cloud config type is secret", + existingConfig: getTestCloudConfigTypeSecretConfig(), + secretConfig: getTestConfig(), + expected: getTestConfig(), + }, + { + name: "Azure config should be override when cloud config type is merge", + existingConfig: getTestCloudConfigTypeMergeConfig(), + secretConfig: getTestConfig(), + expected: getTestCloudConfigTypeMergeConfigExpected(), + }, + { + name: "Error should be reported when secret doesn't exists", + existingConfig: getTestCloudConfigTypeMergeConfig(), + expectErr: true, + }, + { + name: "Error should be reported when secret exists but cloud-config data is not provided", + existingConfig: getTestCloudConfigTypeMergeConfig(), + secretConfig: emptyConfig, + expectErr: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + az := &Cloud{ + kubeClient: fakeclient.NewSimpleClientset(), + } + if test.existingConfig != nil { + az.Config = *test.existingConfig + } + if test.secretConfig != nil { + secret := &v1.Secret{ + Type: v1.SecretTypeOpaque, + ObjectMeta: metav1.ObjectMeta{ + Name: "azure-cloud-provider", + Namespace: "kube-system", + }, + } + if test.secretConfig != emptyConfig { + secretData, err := yaml.Marshal(test.secretConfig) + assert.NoError(t, err, test.name) + secret.Data = map[string][]byte{ + "cloud-config": secretData, + } + } + _, err := az.kubeClient.CoreV1().Secrets(cloudConfigNamespace).Create(secret) + assert.NoError(t, err, test.name) + } + + real, err := az.getConfigFromSecret() + if test.expectErr { + assert.Error(t, err, test.name) + return + } + + assert.NoError(t, err, test.name) + assert.Equal(t, test.expected, real, test.name) + }) + } +}