Merge pull request #48854 from colemickens/msi

Automatic merge from submit-queue (batch tested with PRs 47066, 48892, 48933, 48854, 48894)

azure: msi: add managed identity field, logic

**What this PR does / why we need it**: Enables managed service identity support for the Azure cloudprovider. "Managed Service Identity" allows us to ask the Azure Compute infra to provision an identity for the VM. Users can then retrieve the identity and assign it RBAC permissions to talk to Azure ARM APIs for the purpose of the cloudprovider needs.

Per the commit text:
```
The azure cloudprovider will now use the Managed Service Identity
to retrieve access tokens for the Azure ARM APIs, rather than
requiring hard-coded, user-specified credentials.
```

**Which issue this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close that issue when PR gets merged)*: n/a 

**Special notes for your reviewer**: none

**Release note**:

```release-note
azure: support retrieving access tokens via managed identity extension
```

cc: @brendandburns @jdumars @anhowe
pull/6/head
Kubernetes Submit Queue 2017-07-14 12:50:55 -07:00 committed by GitHub
commit df47592d5a
5 changed files with 81 additions and 67 deletions

View File

@ -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

View File

@ -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) {}

View File

@ -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",
],
)

View File

@ -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

View File

@ -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()