diff --git a/hack/.linted_packages b/hack/.linted_packages index 970aab80cd..7ab9eb282f 100644 --- a/hack/.linted_packages +++ b/hack/.linted_packages @@ -205,6 +205,7 @@ pkg/controller/volume/attachdetach/util pkg/conversion pkg/conversion/queryparams pkg/credentialprovider/aws +pkg/credentialprovider/azure pkg/fieldpath pkg/fields pkg/hyperkube diff --git a/pkg/cloudprovider/providers/azure/azure.go b/pkg/cloudprovider/providers/azure/azure.go index 26a3fa66e1..8ae82707de 100644 --- a/pkg/cloudprovider/providers/azure/azure.go +++ b/pkg/cloudprovider/providers/azure/azure.go @@ -108,6 +108,9 @@ type Config struct { // Use instance metadata service where possible UseInstanceMetadata bool `json:"useInstanceMetadata" yaml:"useInstanceMetadata"` + + // Use managed service identity for the virtual machine to access Azure ARM APIs + UseManagedIdentityExtension bool `json:"useManagedIdentityExtension"` } // Cloud holds the config and clients @@ -151,62 +154,62 @@ func decodePkcs12(pkcs []byte, password string) (*x509.Certificate, *rsa.Private return certificate, rsaPrivateKey, nil } -// newServicePrincipalToken creates a new service principal token based on the configuration -func newServicePrincipalToken(az *Cloud) (*adal.ServicePrincipalToken, error) { - oauthConfig, err := adal.NewOAuthConfig(az.Environment.ActiveDirectoryEndpoint, az.TenantID) +// GetServicePrincipalToken creates a new service principal token based on the configuration +func GetServicePrincipalToken(config *Config, env *azure.Environment) (*adal.ServicePrincipalToken, error) { + oauthConfig, err := adal.NewOAuthConfig(env.ActiveDirectoryEndpoint, config.TenantID) if err != nil { return nil, fmt.Errorf("creating the OAuth config: %v", err) } - if len(az.AADClientSecret) > 0 { + if config.UseManagedIdentityExtension { + glog.V(2).Infoln("azure: using managed identity extension to retrieve access token") + return adal.NewServicePrincipalTokenFromMSI( + *oauthConfig, + env.ServiceManagementEndpoint) + } + + if len(config.AADClientSecret) > 0 { + glog.V(2).Infoln("azure: using client_id+client_secret to retrieve access token") return adal.NewServicePrincipalToken( *oauthConfig, - az.AADClientID, - az.AADClientSecret, - az.Environment.ServiceManagementEndpoint) - } else if len(az.AADClientCertPath) > 0 && len(az.AADClientCertPassword) > 0 { - certData, err := ioutil.ReadFile(az.AADClientCertPath) + config.AADClientID, + config.AADClientSecret, + env.ServiceManagementEndpoint) + } + + if len(config.AADClientCertPath) > 0 && len(config.AADClientCertPassword) > 0 { + glog.V(2).Infoln("azure: using jwt client_assertion (client_cert+client_private_key) to retrieve access token") + certData, err := ioutil.ReadFile(config.AADClientCertPath) if err != nil { - return nil, fmt.Errorf("reading the client certificate from file %s: %v", az.AADClientCertPath, err) + return nil, fmt.Errorf("reading the client certificate from file %s: %v", config.AADClientCertPath, err) } - certificate, privateKey, err := decodePkcs12(certData, az.AADClientCertPassword) + certificate, privateKey, err := decodePkcs12(certData, config.AADClientCertPassword) if err != nil { return nil, fmt.Errorf("decoding the client certificate: %v", err) } return adal.NewServicePrincipalTokenFromCertificate( *oauthConfig, - az.AADClientID, + config.AADClientID, certificate, privateKey, - az.Environment.ServiceManagementEndpoint) - } else { - return nil, fmt.Errorf("No credentials provided for AAD application %s", az.AADClientID) + env.ServiceManagementEndpoint) } + + return nil, fmt.Errorf("No credentials provided for AAD application %s", config.AADClientID) } // NewCloud returns a Cloud with initialized clients func NewCloud(configReader io.Reader) (cloudprovider.Interface, error) { - var az Cloud - - configContents, err := ioutil.ReadAll(configReader) + config, env, err := ParseConfig(configReader) if err != nil { return nil, err } - err = yaml.Unmarshal(configContents, &az) - if err != nil { - return nil, err + az := Cloud{ + Config: *config, + Environment: *env, } - if az.Cloud == "" { - az.Environment = azure.PublicCloud - } else { - az.Environment, err = azure.EnvironmentFromName(az.Cloud) - if err != nil { - return nil, err - } - } - - servicePrincipalToken, err := newServicePrincipalToken(&az) + servicePrincipalToken, err := GetServicePrincipalToken(config, env) if err != nil { return nil, err } @@ -321,6 +324,31 @@ func NewCloud(configReader io.Reader) (cloudprovider.Interface, error) { return &az, nil } +// ParseConfig returns a parsed configuration and azure.Environment for an Azure cloudprovider config file +func ParseConfig(configReader io.Reader) (*Config, *azure.Environment, error) { + var config Config + + configContents, err := ioutil.ReadAll(configReader) + if err != nil { + return nil, nil, err + } + err = yaml.Unmarshal(configContents, &config) + if err != nil { + return nil, nil, err + } + + var env azure.Environment + if config.Cloud == "" { + env = azure.PublicCloud + } else { + env, err = azure.EnvironmentFromName(config.Cloud) + if err != nil { + return nil, nil, err + } + } + return &config, &env, nil +} + // Initialize passes a Kubernetes clientBuilder interface to the cloud provider func (az *Cloud) Initialize(clientBuilder controller.ControllerClientBuilder) {} diff --git a/pkg/credentialprovider/azure/BUILD b/pkg/credentialprovider/azure/BUILD index 4f52afc4d3..aacd43a55a 100644 --- a/pkg/credentialprovider/azure/BUILD +++ b/pkg/credentialprovider/azure/BUILD @@ -17,11 +17,9 @@ go_library( "//pkg/credentialprovider:go_default_library", "//vendor/github.com/Azure/azure-sdk-for-go/arm/containerregistry:go_default_library", "//vendor/github.com/Azure/go-autorest/autorest:go_default_library", - "//vendor/github.com/Azure/go-autorest/autorest/adal:go_default_library", "//vendor/github.com/Azure/go-autorest/autorest/azure:go_default_library", "//vendor/github.com/golang/glog:go_default_library", "//vendor/github.com/spf13/pflag:go_default_library", - "//vendor/gopkg.in/yaml.v2:go_default_library", ], ) diff --git a/pkg/credentialprovider/azure/azure_credentials.go b/pkg/credentialprovider/azure/azure_credentials.go index bdf618b96d..257cbee5b8 100644 --- a/pkg/credentialprovider/azure/azure_credentials.go +++ b/pkg/credentialprovider/azure/azure_credentials.go @@ -17,14 +17,12 @@ limitations under the License. package azure import ( - "io/ioutil" + "io" + "os" "time" - yaml "gopkg.in/yaml.v2" - "github.com/Azure/azure-sdk-for-go/arm/containerregistry" "github.com/Azure/go-autorest/autorest" - "github.com/Azure/go-autorest/autorest/adal" azureapi "github.com/Azure/go-autorest/autorest/azure" "github.com/golang/glog" "github.com/spf13/pflag" @@ -47,10 +45,12 @@ func init() { }) } +// RegistriesClient is a testable interface for the ACR client List operation. type RegistriesClient interface { List() (containerregistry.RegistryListResult, error) } +// NewACRProvider parses the specified configFile and returns a DockerConfigProvider func NewACRProvider(configFile *string) credentialprovider.DockerConfigProvider { return &acrProvider{ file: configFile, @@ -59,24 +59,16 @@ func NewACRProvider(configFile *string) credentialprovider.DockerConfigProvider type acrProvider struct { file *string - config azure.Config - environment azureapi.Environment + config *azure.Config + environment *azureapi.Environment registryClient RegistriesClient } -func (a *acrProvider) loadConfig(contents []byte) error { - err := yaml.Unmarshal(contents, &a.config) +func (a *acrProvider) loadConfig(rdr io.Reader) error { + var err error + a.config, a.environment, err = azure.ParseConfig(rdr) if err != nil { - return err - } - - if a.config.Cloud == "" { - a.environment = azureapi.PublicCloud - } else { - a.environment, err = azureapi.EnvironmentFromName(a.config.Cloud) - if err != nil { - return err - } + glog.Errorf("Failed to load azure credential file: %v", err) } return nil } @@ -86,27 +78,21 @@ func (a *acrProvider) Enabled() bool { glog.V(5).Infof("Azure config unspecified, disabling") return false } - contents, err := ioutil.ReadFile(*a.file) + + f, err := os.Open(*a.file) if err != nil { - glog.Errorf("Failed to load azure credential file: %v", err) + glog.Errorf("Failed to load config from file: %s", *a.file) return false } - if err := a.loadConfig(contents); err != nil { - glog.Errorf("Failed to parse azure credential file: %v", err) + defer f.Close() + + err = a.loadConfig(f) + if err != nil { + glog.Errorf("Failed to load config from file: %s", *a.file) return false } - oauthConfig, err := adal.NewOAuthConfig(a.environment.ActiveDirectoryEndpoint, a.config.TenantID) - if err != nil { - glog.Errorf("Failed to get oauth config: %v", err) - return false - } - - servicePrincipalToken, err := adal.NewServicePrincipalToken( - *oauthConfig, - a.config.AADClientID, - a.config.AADClientSecret, - a.environment.ServiceManagementEndpoint) + servicePrincipalToken, err := azure.GetServicePrincipalToken(a.config, a.environment) if err != nil { glog.Errorf("Failed to create service principal token: %v", err) return false diff --git a/pkg/credentialprovider/azure/azure_credentials_test.go b/pkg/credentialprovider/azure/azure_credentials_test.go index 8f69738776..9d966fe6be 100644 --- a/pkg/credentialprovider/azure/azure_credentials_test.go +++ b/pkg/credentialprovider/azure/azure_credentials_test.go @@ -17,6 +17,7 @@ limitations under the License. package azure import ( + "bytes" "testing" "github.com/Azure/azure-sdk-for-go/arm/containerregistry" @@ -66,7 +67,7 @@ func Test(t *testing.T) { provider := &acrProvider{ registryClient: fakeClient, } - provider.loadConfig([]byte(configStr)) + provider.loadConfig(bytes.NewBufferString(configStr)) creds := provider.Provide()