Commit df47592d authored by Kubernetes Submit Queue's avatar Kubernetes Submit Queue Committed by GitHub

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
parents 726184ca cd177dcd
......@@ -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
......
......@@ -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
}
if az.Cloud == "" {
az.Environment = azure.PublicCloud
} else {
az.Environment, err = azure.EnvironmentFromName(az.Cloud)
if err != nil {
return nil, err
}
az := Cloud{
Config: *config,
Environment: *env,
}
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) {}
......
......@@ -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",
],
)
......
......@@ -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)
return false
}
if err := a.loadConfig(contents); err != nil {
glog.Errorf("Failed to parse azure credential file: %v", err)
glog.Errorf("Failed to load config from file: %s", *a.file)
return false
}
defer f.Close()
oauthConfig, err := adal.NewOAuthConfig(a.environment.ActiveDirectoryEndpoint, a.config.TenantID)
err = a.loadConfig(f)
if err != nil {
glog.Errorf("Failed to get oauth config: %v", err)
glog.Errorf("Failed to load config from file: %s", *a.file)
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
......
......@@ -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()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment