Merge pull request #8121 from paulmey/clientconfig

[azure] Refactor client config
This commit is contained in:
Paul Meyer 2019-09-26 10:49:13 -07:00 committed by GitHub
commit 698c9c44d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 359 additions and 322 deletions

View File

@ -57,13 +57,10 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// User's intent to use MSI is indicated with empty subscription id, tenant, client id, client cert, client secret and jwt.
// FillParameters function will set subscription and tenant id here. Therefore getServicePrincipalTokens won't select right auth type.
// If we run this after getServicePrincipalTokens call then getServicePrincipalTokens won't have tenant id.
if !b.config.useMSI() {
if err := newConfigRetriever().FillParameters(b.config); err != nil {
return nil, err
}
// FillParameters function captures authType and sets defaults.
err := b.config.ClientConfig.FillParameters()
if err != nil {
return nil, err
}
log.Print(":: Configuration")
@ -77,19 +74,12 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
return nil, err
}
// We need subscription id and tenant id for arm operations. Users hasn't specified one so we try to detect them here.
if b.config.useMSI() {
if err := newConfigRetriever().FillParameters(b.config); err != nil {
return nil, err
}
}
ui.Message("Creating Azure Resource Manager (ARM) client ...")
azureClient, err := NewAzureClient(
b.config.SubscriptionID,
b.config.ClientConfig.SubscriptionID,
b.config.ResourceGroupName,
b.config.StorageAccount,
b.config.cloudEnvironment,
b.config.ClientConfig.CloudEnvironment,
b.config.SharedGalleryTimeout,
spnCloud,
spnKeyVault)
@ -102,13 +92,13 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
if err := resolver.Resolve(b.config); err != nil {
return nil, err
}
if b.config.ObjectID == "" {
b.config.ObjectID = getObjectIdFromToken(ui, spnCloud)
if b.config.ClientConfig.ObjectID == "" {
b.config.ClientConfig.ObjectID = getObjectIdFromToken(ui, spnCloud)
} else {
ui.Message("You have provided Object_ID which is no longer needed, azure packer builder determines this dynamically from the authentication token")
}
if b.config.ObjectID == "" && b.config.OSType != constants.Target_Linux {
if b.config.ClientConfig.ObjectID == "" && b.config.OSType != constants.Target_Linux {
return nil, fmt.Errorf("could not determine the ObjectID for the user, which is required for Windows builds")
}
@ -302,7 +292,8 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
}
if b.config.isManagedImage() {
managedImageID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/images/%s", b.config.SubscriptionID, b.config.ManagedImageResourceGroupName, b.config.ManagedImageName)
managedImageID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/images/%s",
b.config.ClientConfig.SubscriptionID, b.config.ManagedImageResourceGroupName, b.config.ManagedImageName)
if b.config.SharedGalleryDestination.SigDestinationGalleryName != "" {
return NewManagedImageArtifactWithSIGAsDestination(b.config.OSType, b.config.ManagedImageResourceGroupName, b.config.ManagedImageName, b.config.manageImageLocation, managedImageID, b.config.ManagedImageOSDiskSnapshotName, b.config.ManagedImageDataDiskSnapshotPrefix, b.stateBag.Get(constants.ArmManagedImageSharedGalleryId).(string))
}
@ -405,7 +396,7 @@ func (b *Builder) configureStateBag(stateBag multistep.StateBag) {
stateBag.Put(constants.ArmManagedImageSharedGalleryName, b.config.SharedGalleryDestination.SigDestinationGalleryName)
stateBag.Put(constants.ArmManagedImageSharedGalleryImageName, b.config.SharedGalleryDestination.SigDestinationImageName)
stateBag.Put(constants.ArmManagedImageSharedGalleryImageVersion, b.config.SharedGalleryDestination.SigDestinationImageVersion)
stateBag.Put(constants.ArmManagedImageSubscription, b.config.SubscriptionID)
stateBag.Put(constants.ArmManagedImageSubscription, b.config.ClientConfig.SubscriptionID)
}
}
@ -424,7 +415,7 @@ func (b *Builder) setImageParameters(stateBag multistep.StateBag) {
}
func (b *Builder) getServicePrincipalTokens(say func(string)) (*adal.ServicePrincipalToken, *adal.ServicePrincipalToken, error) {
return b.config.ClientConfig.getServicePrincipalTokens(say)
return b.config.ClientConfig.GetServicePrincipalTokens(say)
}
func getObjectIdFromToken(ui packer.Ui, token *adal.ServicePrincipalToken) string {

View File

@ -20,6 +20,8 @@ import (
"github.com/Azure/go-autorest/autorest/to"
"github.com/masterzen/winrm"
azcommon "github.com/hashicorp/packer/builder/azure/common"
"github.com/hashicorp/packer/builder/azure/common/client"
"github.com/hashicorp/packer/builder/azure/common/constants"
"github.com/hashicorp/packer/builder/azure/pkcs12"
"github.com/hashicorp/packer/common"
@ -92,7 +94,7 @@ type Config struct {
common.PackerConfig `mapstructure:",squash"`
// Authentication via OAUTH
ClientConfig `mapstructure:",squash"`
ClientConfig client.Config `mapstructure:",squash"`
// Capture
CaptureNamePrefix string `mapstructure:"capture_name_prefix"`
@ -385,7 +387,7 @@ func (c *Config) toVMID() string {
} else {
resourceGroupName = c.BuildResourceGroupName
}
return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/virtualMachines/%s", c.SubscriptionID, resourceGroupName, c.tmpComputeName)
return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/virtualMachines/%s", c.ClientConfig.SubscriptionID, resourceGroupName, c.tmpComputeName)
}
func (c *Config) isManagedImage() bool {
@ -477,7 +479,7 @@ func (c *Config) createCertificate() (string, error) {
func newConfig(raws ...interface{}) (*Config, []string, error) {
var c Config
c.ctx.Funcs = TemplateFuncs
c.ctx.Funcs = azcommon.TemplateFuncs
err := config.Decode(&c, &config.DecodeOpts{
Interpolate: true,
InterpolateContext: &c.ctx,
@ -490,7 +492,7 @@ func newConfig(raws ...interface{}) (*Config, []string, error) {
provideDefaultValues(&c)
setRuntimeValues(&c)
setUserNamePassword(&c)
err = c.ClientConfig.setCloudEnvironment()
err = c.ClientConfig.SetDefaultValues()
if err != nil {
return nil, nil, err
}
@ -650,7 +652,7 @@ func provideDefaultValues(c *Config) {
c.ImageVersion = DefaultImageVersion
}
c.provideDefaultValues()
c.ClientConfig.SetDefaultValues()
}
func assertTagProperties(c *Config, errs *packer.MultiError) {
@ -669,7 +671,7 @@ func assertTagProperties(c *Config, errs *packer.MultiError) {
}
func assertRequiredParametersSet(c *Config, errs *packer.MultiError) {
c.ClientConfig.assertRequiredParametersSet(errs)
c.ClientConfig.Validate(errs)
/////////////////////////////////////////////
// Capture

View File

@ -1,75 +0,0 @@
package arm
// Method to resolve information about the user so that a client can be
// constructed to communicated with Azure.
//
// The following data are resolved.
//
// 1. TenantID
import (
"encoding/json"
"io/ioutil"
"net/http"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/hashicorp/packer/builder/azure/common"
)
type configRetriever struct {
// test seams
findTenantID func(azure.Environment, string) (string, error)
}
func newConfigRetriever() configRetriever {
return configRetriever{
common.FindTenantID,
}
}
func (cr configRetriever) FillParameters(c *Config) error {
if c.SubscriptionID == "" {
subscriptionID, err := cr.getSubscriptionFromIMDS()
if err != nil {
return err
}
c.SubscriptionID = subscriptionID
}
if c.TenantID == "" {
tenantID, err := cr.findTenantID(*c.cloudEnvironment, c.SubscriptionID)
if err != nil {
return err
}
c.TenantID = tenantID
}
return nil
}
func (cr configRetriever) getSubscriptionFromIMDS() (string, error) {
client := &http.Client{}
req, _ := http.NewRequest("GET", "http://169.254.169.254/metadata/instance/compute", nil)
req.Header.Add("Metadata", "True")
q := req.URL.Query()
q.Add("format", "json")
q.Add("api-version", "2017-08-01")
req.URL.RawQuery = q.Encode()
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
resp_body, _ := ioutil.ReadAll(resp.Body)
result := map[string]string{}
err = json.Unmarshal(resp_body, &result)
if err != nil {
return "", err
}
return result["subscriptionId"], nil
}

View File

@ -1,62 +0,0 @@
package arm
import (
"errors"
"testing"
"github.com/Azure/go-autorest/autorest/azure"
)
func TestConfigRetrieverFillsTenantIDWhenEmpty(t *testing.T) {
c, _, _ := newConfig(getArmBuilderConfiguration(), getPackerConfiguration())
if expected := ""; c.TenantID != expected {
t.Errorf("Expected TenantID to be %q but got %q", expected, c.TenantID)
}
sut := newTestConfigRetriever()
retrievedTid := "my-tenant-id"
sut.findTenantID = func(azure.Environment, string) (string, error) { return retrievedTid, nil }
if err := sut.FillParameters(c); err != nil {
t.Errorf("Unexpected error when calling sut.FillParameters: %v", err)
}
if expected := retrievedTid; c.TenantID != expected {
t.Errorf("Expected TenantID to be %q but got %q", expected, c.TenantID)
}
}
func TestConfigRetrieverLeavesTenantIDWhenNotEmpty(t *testing.T) {
c, _, _ := newConfig(getArmBuilderConfiguration(), getPackerConfiguration())
userSpecifiedTid := "not-empty"
c.TenantID = userSpecifiedTid
sut := newTestConfigRetriever()
sut.findTenantID = nil // assert that this not even called
if err := sut.FillParameters(c); err != nil {
t.Errorf("Unexpected error when calling sut.FillParameters: %v", err)
}
if expected := userSpecifiedTid; c.TenantID != expected {
t.Errorf("Expected TenantID to be %q but got %q", expected, c.TenantID)
}
}
func TestConfigRetrieverReturnsErrorWhenTenantIDEmptyAndRetrievalFails(t *testing.T) {
c, _, _ := newConfig(getArmBuilderConfiguration(), getPackerConfiguration())
if expected := ""; c.TenantID != expected {
t.Errorf("Expected TenantID to be %q but got %q", expected, c.TenantID)
}
sut := newTestConfigRetriever()
errorString := "sorry, I failed"
sut.findTenantID = func(azure.Environment, string) (string, error) { return "", errors.New(errorString) }
if err := sut.FillParameters(c); err != nil && err.Error() != errorString {
t.Errorf("Unexpected error when calling sut.FillParameters: %v", err)
}
}
func newTestConfigRetriever() configRetriever {
return configRetriever{
findTenantID: func(azure.Environment, string) (string, error) { return "findTenantID is mocked", nil },
}
}

View File

@ -41,8 +41,8 @@ func TestConfigShouldProvideReasonableDefaultValues(t *testing.T) {
t.Error("Expected 'VMSize' to be populated, but it was empty!")
}
if c.ObjectID != "" {
t.Errorf("Expected 'ObjectID' to be nil, but it was '%s'!", c.ObjectID)
if c.ClientConfig.ObjectID != "" {
t.Errorf("Expected 'ObjectID' to be nil, but it was '%s'!", c.ClientConfig.ObjectID)
}
if c.managedImageStorageAccountType == "" {
@ -273,12 +273,12 @@ func TestConfigVirtualNetworkSubnetNameMustBeSetWithVirtualNetworkName(t *testin
func TestConfigShouldDefaultToPublicCloud(t *testing.T) {
c, _, _ := newConfig(getArmBuilderConfiguration(), getPackerConfiguration())
if c.CloudEnvironmentName != "Public" {
t.Errorf("Expected 'CloudEnvironmentName' to default to 'Public', but got '%s'.", c.CloudEnvironmentName)
if c.ClientConfig.CloudEnvironmentName != "Public" {
t.Errorf("Expected 'CloudEnvironmentName' to default to 'Public', but got '%s'.", c.ClientConfig.CloudEnvironmentName)
}
if c.cloudEnvironment == nil || c.cloudEnvironment.Name != "AzurePublicCloud" {
t.Errorf("Expected 'cloudEnvironment' to be set to 'AzurePublicCloud', but got '%s'.", c.cloudEnvironment)
if c.ClientConfig.CloudEnvironment == nil || c.ClientConfig.CloudEnvironment.Name != "AzurePublicCloud" {
t.Errorf("Expected 'cloudEnvironment' to be set to 'AzurePublicCloud', but got '%s'.", c.ClientConfig.CloudEnvironment)
}
}
@ -327,8 +327,8 @@ func TestConfigInstantiatesCorrectAzureEnvironment(t *testing.T) {
t.Fatal(err)
}
if c.cloudEnvironment == nil || c.cloudEnvironment.Name != x.environmentName {
t.Errorf("Expected 'cloudEnvironment' to be set to '%s', but got '%s'.", x.environmentName, c.cloudEnvironment)
if c.ClientConfig.CloudEnvironment == nil || c.ClientConfig.CloudEnvironment.Name != x.environmentName {
t.Errorf("Expected 'cloudEnvironment' to be set to '%s', but got '%s'.", x.environmentName, c.ClientConfig.CloudEnvironment)
}
}
}

View File

@ -18,8 +18,8 @@ func GetKeyVaultDeployment(config *Config) (*resources.Deployment, error) {
params := &template.TemplateParameters{
KeyVaultName: &template.TemplateParameter{Value: config.tmpKeyVaultName},
KeyVaultSecretValue: &template.TemplateParameter{Value: config.winrmCertificate},
ObjectId: &template.TemplateParameter{Value: config.ObjectID},
TenantId: &template.TemplateParameter{Value: config.TenantID},
ObjectId: &template.TemplateParameter{Value: config.ClientConfig.ObjectID},
TenantId: &template.TemplateParameter{Value: config.ClientConfig.TenantID},
}
builder, _ := template.NewTemplateBuilder(template.KeyVault)
@ -64,7 +64,7 @@ func GetVirtualMachineDeployment(config *Config) (*resources.Deployment, error)
builder.SetManagedDiskUrl(config.customManagedImageID, config.managedImageStorageAccountType, config.diskCachingType)
} else if config.ManagedImageName != "" && config.ImagePublisher != "" {
imageID := fmt.Sprintf("/subscriptions/%s/providers/Microsoft.Compute/locations/%s/publishers/%s/ArtifactTypes/vmimage/offers/%s/skus/%s/versions/%s",
config.SubscriptionID,
config.ClientConfig.SubscriptionID,
config.Location,
config.ImagePublisher,
config.ImageOffer,

View File

@ -488,11 +488,11 @@ func TestKeyVaultDeployment02(t *testing.T) {
t.Fatal(err)
}
if params.ObjectId.Value != c.ObjectID {
t.Errorf("Expected template parameter 'ObjectId' to be %s, but got %s.", params.ObjectId.Value, c.ObjectID)
if params.ObjectId.Value != c.ClientConfig.ObjectID {
t.Errorf("Expected template parameter 'ObjectId' to be %s, but got %s.", params.ObjectId.Value, c.ClientConfig.ObjectID)
}
if params.TenantId.Value != c.TenantID {
t.Errorf("Expected template parameter 'TenantId' to be %s, but got %s.", params.TenantId.Value, c.TenantID)
if params.TenantId.Value != c.ClientConfig.TenantID {
t.Errorf("Expected template parameter 'TenantId' to be %s, but got %s.", params.TenantId.Value, c.ClientConfig.TenantID)
}
if params.KeyVaultName.Value != c.tmpKeyVaultName {
t.Errorf("Expected template parameter 'KeyVaultName' to be %s, but got %s.", params.KeyVaultName.Value, c.tmpKeyVaultName)

View File

@ -1,9 +1,10 @@
//go:generate struct-markdown
package arm
package client
import (
"fmt"
"github.com/hashicorp/packer/builder/azure/common"
"os"
"strings"
"time"
@ -14,41 +15,65 @@ import (
"github.com/hashicorp/packer/packer"
)
// ClientConfig allows for various ways to authenticate Azure clients
type ClientConfig struct {
// Config allows for various ways to authenticate Azure clients.
// When `client_id` and `subscription_id` are specified, Packer will use the
// specified Azure Active Directoty (AAD) Service Principal (SP).
// If only `subscription_id` is specified, Packer will try to interactively
// log on the current user (tokens will be cached).
// If none of these options are specified, Packer will attempt to use the
// Managed Identity and subscription of the VM that Packer is running on.
// This will only work if Packer is running on an Azure VM.
type Config struct {
// One of Public, China, Germany, or
// USGovernment. Defaults to Public. Long forms such as
// USGovernmentCloud and AzureUSGovernmentCloud are also supported.
CloudEnvironmentName string `mapstructure:"cloud_environment_name" required:"false"`
cloudEnvironment *azure.Environment
CloudEnvironment *azure.Environment
// Authentication fields
// Client ID
// The application ID of the AAD Service Principal.
// Requires either `client_secret`, `client_cert_path` or `client_jwt` to be set as well.
ClientID string `mapstructure:"client_id"`
// Client secret/password
// A password/secret registered for the AAD SP.
ClientSecret string `mapstructure:"client_secret"`
// Certificate path for client auth
// The path to a certificate that will be used to authenticate as the specified AAD SP.
ClientCertPath string `mapstructure:"client_cert_path"`
// JWT bearer token for client auth (RFC 7523, Sec. 2.2)
// A JWT bearer token for client auth (RFC 7523, Sec. 2.2) that will be used
// to authenticate the AAD SP. Provides more control over token the expiration
// when using certificate authentication than when using `client_cert_path`.
ClientJWT string `mapstructure:"client_jwt"`
ObjectID string `mapstructure:"object_id"`
// The account identifier with which your client_id and
// subscription_id are associated. If not specified, tenant_id will be
// looked up using subscription_id.
TenantID string `mapstructure:"tenant_id" required:"false"`
// The object ID for the AAD SP. Optional, will be derived from the oAuth token if left empty.
ObjectID string `mapstructure:"object_id"`
// The Active Directory tenant identifier with which your `client_id` and
// `subscription_id` are associated. If not specified, `tenant_id` will be
// looked up using `subscription_id`.
TenantID string `mapstructure:"tenant_id" required:"false"`
// The subscription to use.
SubscriptionID string `mapstructure:"subscription_id"`
authType string
}
const (
authTypeDeviceLogin = "DeviceLogin"
authTypeMSI = "ManagedIdentity"
authTypeClientSecret = "ClientSecret"
authTypeClientCert = "ClientCertificate"
authTypeClientBearerJWT = "ClientBearerJWT"
)
const DefaultCloudEnvironmentName = "Public"
func (c *ClientConfig) provideDefaultValues() {
func (c *Config) SetDefaultValues() error {
if c.CloudEnvironmentName == "" {
c.CloudEnvironmentName = DefaultCloudEnvironmentName
}
return c.setCloudEnvironment()
}
func (c *ClientConfig) setCloudEnvironment() error {
func (c *Config) setCloudEnvironment() error {
lookup := map[string]string{
"CHINA": "AzureChinaCloud",
"CHINACLOUD": "AzureChinaCloud",
@ -78,11 +103,11 @@ func (c *ClientConfig) setCloudEnvironment() error {
}
env, err := azure.EnvironmentFromName(envName)
c.cloudEnvironment = &env
c.CloudEnvironment = &env
return err
}
func (c ClientConfig) assertRequiredParametersSet(errs *packer.MultiError) {
func (c Config) Validate(errs *packer.MultiError) {
/////////////////////////////////////////////
// Authentication via OAUTH
@ -95,7 +120,7 @@ func (c ClientConfig) assertRequiredParametersSet(errs *packer.MultiError) {
// readable by the ObjectID of the App. There may be another way to handle
// this case, but I am not currently aware of it - send feedback.
if c.useMSI() {
if c.UseMSI() {
return
}
@ -156,7 +181,7 @@ func (c ClientConfig) assertRequiredParametersSet(errs *packer.MultiError) {
" - subscription_id, client_id and client_jwt."))
}
func (c ClientConfig) useDeviceLogin() bool {
func (c Config) useDeviceLogin() bool {
return c.SubscriptionID != "" &&
c.ClientID == "" &&
c.ClientSecret == "" &&
@ -164,7 +189,7 @@ func (c ClientConfig) useDeviceLogin() bool {
c.ClientCertPath == ""
}
func (c ClientConfig) useMSI() bool {
func (c Config) UseMSI() bool {
return c.SubscriptionID == "" &&
c.ClientID == "" &&
c.ClientSecret == "" &&
@ -173,7 +198,7 @@ func (c ClientConfig) useMSI() bool {
c.TenantID == ""
}
func (c ClientConfig) getServicePrincipalTokens(
func (c Config) GetServicePrincipalTokens(
say func(string)) (
servicePrincipalToken *adal.ServicePrincipalToken,
servicePrincipalTokenVault *adal.ServicePrincipalToken,
@ -182,25 +207,27 @@ func (c ClientConfig) getServicePrincipalTokens(
tenantID := c.TenantID
var auth oAuthTokenProvider
if c.useDeviceLogin() {
switch c.authType {
case authTypeDeviceLogin:
say("Getting tokens using device flow")
auth = NewDeviceFlowOAuthTokenProvider(*c.cloudEnvironment, say, tenantID)
} else if c.useMSI() {
auth = NewDeviceFlowOAuthTokenProvider(*c.CloudEnvironment, say, tenantID)
case authTypeMSI:
say("Getting tokens using Managed Identity for Azure")
auth = NewMSIOAuthTokenProvider(*c.cloudEnvironment)
} else if c.ClientSecret != "" {
auth = NewMSIOAuthTokenProvider(*c.CloudEnvironment)
case authTypeClientSecret:
say("Getting tokens using client secret")
auth = NewSecretOAuthTokenProvider(*c.cloudEnvironment, c.ClientID, c.ClientSecret, tenantID)
} else if c.ClientCertPath != "" {
auth = NewSecretOAuthTokenProvider(*c.CloudEnvironment, c.ClientID, c.ClientSecret, tenantID)
case authTypeClientCert:
say("Getting tokens using client certificate")
auth, err = NewCertOAuthTokenProvider(*c.cloudEnvironment, c.ClientID, c.ClientCertPath, tenantID)
auth, err = NewCertOAuthTokenProvider(*c.CloudEnvironment, c.ClientID, c.ClientCertPath, tenantID)
if err != nil {
return nil, nil, err
}
} else {
case authTypeClientBearerJWT:
say("Getting tokens using client bearer JWT")
auth = NewJWTOAuthTokenProvider(*c.cloudEnvironment, c.ClientID, c.ClientJWT, tenantID)
auth = NewJWTOAuthTokenProvider(*c.CloudEnvironment, c.ClientID, c.ClientJWT, tenantID)
default:
panic("authType not set, call FillParameters, or set explicitly")
}
servicePrincipalToken, err = auth.getServicePrincipalToken()
@ -214,7 +241,7 @@ func (c ClientConfig) getServicePrincipalTokens(
}
servicePrincipalTokenVault, err = auth.getServicePrincipalTokenWithResource(
strings.TrimRight(c.cloudEnvironment.KeyVaultEndpoint, "/"))
strings.TrimRight(c.CloudEnvironment.KeyVaultEndpoint, "/"))
if err != nil {
return nil, nil, err
}
@ -226,3 +253,50 @@ func (c ClientConfig) getServicePrincipalTokens(
return servicePrincipalToken, servicePrincipalTokenVault, nil
}
// FillParameters capture the user intent from the supplied parameter set in authType, retrieves the TenantID and CloudEnvironment if not specified.
// The SubscriptionID is also retrieved in case MSI auth is requested.
func (c *Config) FillParameters() error {
if c.authType == "" {
if c.useDeviceLogin() {
c.authType = authTypeDeviceLogin
} else if c.UseMSI() {
c.authType = authTypeMSI
} else if c.ClientSecret != "" {
c.authType = authTypeClientSecret
} else if c.ClientCertPath != "" {
c.authType = authTypeClientCert
} else {
c.authType = authTypeClientBearerJWT
}
}
if c.authType == authTypeMSI && c.SubscriptionID == "" {
subscriptionID, err := getSubscriptionFromIMDS()
if err != nil {
return fmt.Errorf("error fetching subscriptionID from VM metadata service for Managed Identity authentication: %v", err)
}
c.SubscriptionID = subscriptionID
}
if c.CloudEnvironment == nil {
err := c.setCloudEnvironment()
if err != nil {
return err
}
}
if c.TenantID == "" {
tenantID, err := findTenantID(*c.CloudEnvironment, c.SubscriptionID)
if err != nil {
return err
}
c.TenantID = tenantID
}
return nil
}
// allow override for unit tests
var findTenantID = common.FindTenantID

View File

@ -0,0 +1,37 @@
package client
import (
"encoding/json"
"io/ioutil"
"net/http"
)
// allow override for unit tests
var getSubscriptionFromIMDS = _getSubscriptionFromIMDS
func _getSubscriptionFromIMDS() (string, error) {
client := &http.Client{}
req, _ := http.NewRequest("GET", "http://169.254.169.254/metadata/instance/compute", nil)
req.Header.Add("Metadata", "True")
q := req.URL.Query()
q.Add("format", "json")
q.Add("api-version", "2017-08-01")
req.URL.RawQuery = q.Encode()
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
resp_body, _ := ioutil.ReadAll(resp.Body)
result := map[string]string{}
err = json.Unmarshal(resp_body, &result)
if err != nil {
return "", err
}
return result["subscriptionId"], nil
}

View File

@ -0,0 +1,56 @@
package client
import (
"errors"
"testing"
"github.com/Azure/go-autorest/autorest/azure"
)
func TestConfigRetrieverFillsTenantIDWhenEmpty(t *testing.T) {
c := Config{CloudEnvironmentName: "AzurePublicCloud"}
if expected := ""; c.TenantID != expected {
t.Errorf("Expected TenantID to be %q but got %q", expected, c.TenantID)
}
retrievedTid := "my-tenant-id"
findTenantID = func(azure.Environment, string) (string, error) { return retrievedTid, nil }
getSubscriptionFromIMDS = func() (string, error) { return "unittest", nil }
if err := c.FillParameters(); err != nil {
t.Errorf("Unexpected error when calling c.FillParameters: %v", err)
}
if expected := retrievedTid; c.TenantID != expected {
t.Errorf("Expected TenantID to be %q but got %q", expected, c.TenantID)
}
}
func TestConfigRetrieverLeavesTenantIDWhenNotEmpty(t *testing.T) {
c := Config{CloudEnvironmentName: "AzurePublicCloud"}
userSpecifiedTid := "not-empty"
c.TenantID = userSpecifiedTid
findTenantID = nil // assert that this not even called
getSubscriptionFromIMDS = func() (string, error) { return "unittest", nil }
if err := c.FillParameters(); err != nil {
t.Errorf("Unexpected error when calling c.FillParameters: %v", err)
}
if expected := userSpecifiedTid; c.TenantID != expected {
t.Errorf("Expected TenantID to be %q but got %q", expected, c.TenantID)
}
}
func TestConfigRetrieverReturnsErrorWhenTenantIDEmptyAndRetrievalFails(t *testing.T) {
c := Config{CloudEnvironmentName: "AzurePublicCloud"}
if expected := ""; c.TenantID != expected {
t.Errorf("Expected TenantID to be %q but got %q", expected, c.TenantID)
}
errorString := "sorry, I failed"
findTenantID = func(azure.Environment, string) (string, error) { return "", errors.New(errorString) }
getSubscriptionFromIMDS = func() (string, error) { return "unittest", nil }
if err := c.FillParameters(); err != nil && err.Error() != errorString {
t.Errorf("Unexpected error when calling c.FillParameters: %v", err)
}
}

View File

@ -1,4 +1,4 @@
package arm
package client
import (
crand "crypto/rand"
@ -21,52 +21,52 @@ func Test_ClientConfig_RequiredParametersSet(t *testing.T) {
tests := []struct {
name string
config ClientConfig
config Config
wantErr bool
}{
{
name: "no client_id, client_secret or subscription_id should enable MSI auth",
config: ClientConfig{},
config: Config{},
wantErr: false,
},
{
name: "subscription_id is set will trigger device flow",
config: ClientConfig{
config: Config{
SubscriptionID: "error",
},
wantErr: false,
},
{
name: "client_id without client_secret, client_cert_path or client_jwt should error",
config: ClientConfig{
config: Config{
ClientID: "error",
},
wantErr: true,
},
{
name: "client_secret without client_id should error",
config: ClientConfig{
config: Config{
ClientSecret: "error",
},
wantErr: true,
},
{
name: "client_cert_path without client_id should error",
config: ClientConfig{
config: Config{
ClientCertPath: "/dev/null",
},
wantErr: true,
},
{
name: "client_jwt without client_id should error",
config: ClientConfig{
config: Config{
ClientJWT: "error",
},
wantErr: true,
},
{
name: "missing subscription_id when using secret",
config: ClientConfig{
config: Config{
ClientID: "ok",
ClientSecret: "ok",
},
@ -74,7 +74,7 @@ func Test_ClientConfig_RequiredParametersSet(t *testing.T) {
},
{
name: "missing subscription_id when using certificate",
config: ClientConfig{
config: Config{
ClientID: "ok",
ClientCertPath: "ok",
},
@ -82,7 +82,7 @@ func Test_ClientConfig_RequiredParametersSet(t *testing.T) {
},
{
name: "missing subscription_id when using JWT",
config: ClientConfig{
config: Config{
ClientID: "ok",
ClientJWT: "ok",
},
@ -90,7 +90,7 @@ func Test_ClientConfig_RequiredParametersSet(t *testing.T) {
},
{
name: "too many client_* values",
config: ClientConfig{
config: Config{
SubscriptionID: "ok",
ClientID: "ok",
ClientSecret: "ok",
@ -100,7 +100,7 @@ func Test_ClientConfig_RequiredParametersSet(t *testing.T) {
},
{
name: "too many client_* values (2)",
config: ClientConfig{
config: Config{
SubscriptionID: "ok",
ClientID: "ok",
ClientSecret: "ok",
@ -110,7 +110,7 @@ func Test_ClientConfig_RequiredParametersSet(t *testing.T) {
},
{
name: "tenant_id alone should fail",
config: ClientConfig{
config: Config{
TenantID: "ok",
},
wantErr: true,
@ -120,7 +120,7 @@ func Test_ClientConfig_RequiredParametersSet(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
errs := &packer.MultiError{}
tt.config.assertRequiredParametersSet(errs)
tt.config.Validate(errs)
if (len(errs.Errors) != 0) != tt.wantErr {
t.Errorf("newConfig() error = %v, wantErr %v", errs, tt.wantErr)
return
@ -131,13 +131,13 @@ func Test_ClientConfig_RequiredParametersSet(t *testing.T) {
func Test_ClientConfig_DeviceLogin(t *testing.T) {
getEnvOrSkip(t, "AZURE_DEVICE_LOGIN")
cfg := ClientConfig{
cfg := Config{
SubscriptionID: getEnvOrSkip(t, "AZURE_SUBSCRIPTION"),
cloudEnvironment: getCloud(),
CloudEnvironment: getCloud(),
}
assertValid(t, cfg)
spt, sptkv, err := cfg.getServicePrincipalTokens(
spt, sptkv, err := cfg.GetServicePrincipalTokens(
func(s string) { fmt.Printf("SAY: %s\n", s) })
if err != nil {
t.Fatalf("Expected nil err, but got: %v", err)
@ -159,16 +159,16 @@ func Test_ClientConfig_DeviceLogin(t *testing.T) {
}
func Test_ClientConfig_ClientPassword(t *testing.T) {
cfg := ClientConfig{
cfg := Config{
SubscriptionID: getEnvOrSkip(t, "AZURE_SUBSCRIPTION"),
ClientID: getEnvOrSkip(t, "AZURE_CLIENTID"),
ClientSecret: getEnvOrSkip(t, "AZURE_CLIENTSECRET"),
TenantID: getEnvOrSkip(t, "AZURE_TENANTID"),
cloudEnvironment: getCloud(),
CloudEnvironment: getCloud(),
}
assertValid(t, cfg)
spt, sptkv, err := cfg.getServicePrincipalTokens(func(s string) { fmt.Printf("SAY: %s\n", s) })
spt, sptkv, err := cfg.GetServicePrincipalTokens(func(s string) { fmt.Printf("SAY: %s\n", s) })
if err != nil {
t.Fatalf("Expected nil err, but got: %v", err)
}
@ -189,16 +189,16 @@ func Test_ClientConfig_ClientPassword(t *testing.T) {
}
func Test_ClientConfig_ClientCert(t *testing.T) {
cfg := ClientConfig{
cfg := Config{
SubscriptionID: getEnvOrSkip(t, "AZURE_SUBSCRIPTION"),
ClientID: getEnvOrSkip(t, "AZURE_CLIENTID"),
ClientCertPath: getEnvOrSkip(t, "AZURE_CLIENTCERT"),
TenantID: getEnvOrSkip(t, "AZURE_TENANTID"),
cloudEnvironment: getCloud(),
CloudEnvironment: getCloud(),
}
assertValid(t, cfg)
spt, sptkv, err := cfg.getServicePrincipalTokens(func(s string) { fmt.Printf("SAY: %s\n", s) })
spt, sptkv, err := cfg.GetServicePrincipalTokens(func(s string) { fmt.Printf("SAY: %s\n", s) })
if err != nil {
t.Fatalf("Expected nil err, but got: %v", err)
}
@ -219,16 +219,16 @@ func Test_ClientConfig_ClientCert(t *testing.T) {
}
func Test_ClientConfig_ClientJWT(t *testing.T) {
cfg := ClientConfig{
cfg := Config{
SubscriptionID: getEnvOrSkip(t, "AZURE_SUBSCRIPTION"),
ClientID: getEnvOrSkip(t, "AZURE_CLIENTID"),
ClientJWT: getEnvOrSkip(t, "AZURE_CLIENTJWT"),
TenantID: getEnvOrSkip(t, "AZURE_TENANTID"),
cloudEnvironment: getCloud(),
CloudEnvironment: getCloud(),
}
assertValid(t, cfg)
spt, sptkv, err := cfg.getServicePrincipalTokens(func(s string) { fmt.Printf("SAY: %s\n", s) })
spt, sptkv, err := cfg.GetServicePrincipalTokens(func(s string) { fmt.Printf("SAY: %s\n", s) })
if err != nil {
t.Fatalf("Expected nil err, but got: %v", err)
}
@ -270,106 +270,109 @@ func getCloud() *azure.Environment {
func Test_ClientConfig_CanUseDeviceCode(t *testing.T) {
// TenantID is optional, but Builder will look up tenant ID before requesting
t.Run("without TenantID", func(t *testing.T) {
cfg := emptyClientConfig()
cfg.SubscriptionID = "12345"
cfg := Config{
SubscriptionID: "12345",
}
assertValid(t, cfg)
})
t.Run("with TenantID", func(t *testing.T) {
cfg := emptyClientConfig()
cfg.SubscriptionID = "12345"
cfg.TenantID = "12345"
cfg := Config{
SubscriptionID: "12345",
TenantID: "12345",
}
assertValid(t, cfg)
})
}
func assertValid(t *testing.T, cfg ClientConfig) {
func assertValid(t *testing.T, cfg Config) {
errs := &packer.MultiError{}
cfg.assertRequiredParametersSet(errs)
cfg.Validate(errs)
if len(errs.Errors) != 0 {
t.Fatal("Expected errs to be empty: ", errs)
}
}
func assertInvalid(t *testing.T, cfg ClientConfig) {
func assertInvalid(t *testing.T, cfg Config) {
errs := &packer.MultiError{}
cfg.assertRequiredParametersSet(errs)
cfg.Validate(errs)
if len(errs.Errors) == 0 {
t.Fatal("Expected errs to be non-empty")
}
}
func Test_ClientConfig_CanUseClientSecret(t *testing.T) {
cfg := emptyClientConfig()
cfg.SubscriptionID = "12345"
cfg.ClientID = "12345"
cfg.ClientSecret = "12345"
cfg := Config{
SubscriptionID: "12345",
ClientID: "12345",
ClientSecret: "12345",
}
assertValid(t, cfg)
}
func Test_ClientConfig_CanUseClientSecretWithTenantID(t *testing.T) {
cfg := emptyClientConfig()
cfg.SubscriptionID = "12345"
cfg.ClientID = "12345"
cfg.ClientSecret = "12345"
cfg.TenantID = "12345"
cfg := Config{
SubscriptionID: "12345",
ClientID: "12345",
ClientSecret: "12345",
TenantID: "12345",
}
assertValid(t, cfg)
}
func Test_ClientConfig_CanUseClientJWT(t *testing.T) {
cfg := emptyClientConfig()
cfg.SubscriptionID = "12345"
cfg.ClientID = "12345"
cfg.ClientJWT = getJWT(10*time.Minute, true)
cfg := Config{
SubscriptionID: "12345",
ClientID: "12345",
ClientJWT: getJWT(10*time.Minute, true),
}
assertValid(t, cfg)
}
func Test_ClientConfig_CanUseClientJWTWithTenantID(t *testing.T) {
cfg := emptyClientConfig()
cfg.SubscriptionID = "12345"
cfg.ClientID = "12345"
cfg.ClientJWT = getJWT(10*time.Minute, true)
cfg.TenantID = "12345"
cfg := Config{
SubscriptionID: "12345",
ClientID: "12345",
ClientJWT: getJWT(10*time.Minute, true),
TenantID: "12345",
}
assertValid(t, cfg)
}
func Test_ClientConfig_CannotUseBothClientJWTAndSecret(t *testing.T) {
cfg := emptyClientConfig()
cfg.SubscriptionID = "12345"
cfg.ClientID = "12345"
cfg.ClientSecret = "12345"
cfg.ClientJWT = getJWT(10*time.Minute, true)
cfg := Config{
SubscriptionID: "12345",
ClientID: "12345",
ClientSecret: "12345",
ClientJWT: getJWT(10*time.Minute, true),
}
assertInvalid(t, cfg)
}
func Test_ClientConfig_ClientJWTShouldBeValidForAtLeast5Minutes(t *testing.T) {
cfg := emptyClientConfig()
cfg.SubscriptionID = "12345"
cfg.ClientID = "12345"
cfg.ClientJWT = getJWT(time.Minute, true)
cfg := Config{
SubscriptionID: "12345",
ClientID: "12345",
ClientJWT: getJWT(time.Minute, true),
}
assertInvalid(t, cfg)
}
func Test_ClientConfig_ClientJWTShouldHaveThumbprint(t *testing.T) {
cfg := emptyClientConfig()
cfg.SubscriptionID = "12345"
cfg.ClientID = "12345"
cfg.ClientJWT = getJWT(10*time.Minute, false)
cfg := Config{
SubscriptionID: "12345",
ClientID: "12345",
ClientJWT: getJWT(10*time.Minute, false),
}
assertInvalid(t, cfg)
}
func emptyClientConfig() ClientConfig {
cfg := ClientConfig{}
_ = cfg.setCloudEnvironment()
return cfg
}
func Test_getJWT(t *testing.T) {
if getJWT(time.Minute, true) == "" {
t.Fatalf("getJWT is broken")

View File

@ -1,4 +1,4 @@
package arm
package client
import (
"github.com/Azure/go-autorest/autorest/adal"

View File

@ -1,4 +1,4 @@
package arm
package client
import (
"crypto/ecdsa"
@ -14,7 +14,7 @@ import (
"time"
"github.com/Azure/go-autorest/autorest/azure"
jwt "github.com/dgrijalva/jwt-go"
"github.com/dgrijalva/jwt-go"
)
func NewCertOAuthTokenProvider(env azure.Environment, clientID, clientCertPath, tenantID string) (oAuthTokenProvider, error) {

View File

@ -1,4 +1,4 @@
package arm
package client
import (
"fmt"

View File

@ -1,4 +1,4 @@
package arm
package client
import (
"net/url"

View File

@ -1,4 +1,4 @@
package arm
package client
import (
"github.com/Azure/go-autorest/autorest/adal"

View File

@ -1,4 +1,4 @@
package arm
package client
import (
"github.com/Azure/go-autorest/autorest/adal"

View File

@ -1,4 +1,4 @@
package arm
package client
import (
"testing"

View File

@ -1,4 +1,4 @@
package arm
package common
import (
"bytes"
@ -23,9 +23,6 @@ func isValidByteValue(b byte) bool {
// Clean up image name by replacing invalid characters with "-"
// Names are not allowed to end in '.', '-', or '_' and are trimmed.
func templateCleanImageName(s string) string {
if ok, _ := assertManagedImageName(s, ""); ok {
return s
}
b := []byte(s)
newb := make([]byte, len(b))
for i := range newb {

View File

@ -1,4 +1,4 @@
package arm
package common
import "testing"

View File

@ -157,10 +157,12 @@ To use an existing resource group you **must** provide:
Providing `temp_resource_group_name` or `location` in combination with
`build_resource_group_name` is not allowed.
<%= partial "partials/builder/azure/common/client/_Config" %>
### Optional:
<%= partial "partials/builder/azure/arm/Config-not-required" %>
<%= partial "partials/builder/azure/arm/ClientConfig-not-required" %>
<%= partial "partials/builder/azure/common/client/_Config-not-required" %>
## Basic Example

View File

@ -1,20 +0,0 @@
<!-- Code generated from the comments of the ClientConfig struct in builder/azure/arm/clientconfig.go; DO NOT EDIT MANUALLY -->
- `cloud_environment_name` (string) - One of Public, China, Germany, or
USGovernment. Defaults to Public. Long forms such as
USGovernmentCloud and AzureUSGovernmentCloud are also supported.
- `client_id` (string) - Client ID
- `client_secret` (string) - Client secret/password
- `client_cert_path` (string) - Certificate path for client auth
- `client_jwt` (string) - JWT bearer token for client auth (RFC 7523, Sec. 2.2)
- `object_id` (string) - Object ID
- `tenant_id` (string) - The account identifier with which your client_id and
subscription_id are associated. If not specified, tenant_id will be
looked up using subscription_id.
- `subscription_id` (string) - Subscription ID

View File

@ -1,2 +0,0 @@
<!-- Code generated from the comments of the ClientConfig struct in builder/azure/arm/clientconfig.go; DO NOT EDIT MANUALLY -->
ClientConfig allows for various ways to authenticate Azure clients

View File

@ -0,0 +1,25 @@
<!-- Code generated from the comments of the Config struct in builder/azure/common/client/config.go; DO NOT EDIT MANUALLY -->
- `cloud_environment_name` (string) - One of Public, China, Germany, or
USGovernment. Defaults to Public. Long forms such as
USGovernmentCloud and AzureUSGovernmentCloud are also supported.
- `client_id` (string) - The application ID of the AAD Service Principal.
Requires either `client_secret`, `client_cert_path` or `client_jwt` to be set as well.
- `client_secret` (string) - A password/secret registered for the AAD SP.
- `client_cert_path` (string) - The path to a certificate that will be used to authenticate as the specified AAD SP.
- `client_jwt` (string) - A JWT bearer token for client auth (RFC 7523, Sec. 2.2) that will be used
to authenticate the AAD SP. Provides more control over token the expiration
when using certificate authentication than when using `client_cert_path`.
- `object_id` (string) - The object ID for the AAD SP. Optional, will be derived from the oAuth token if left empty.
- `tenant_id` (string) - The Active Directory tenant identifier with which your `client_id` and
`subscription_id` are associated. If not specified, `tenant_id` will be
looked up using `subscription_id`.
- `subscription_id` (string) - The subscription to use.

View File

@ -0,0 +1,9 @@
<!-- Code generated from the comments of the Config struct in builder/azure/common/client/config.go; DO NOT EDIT MANUALLY -->
Config allows for various ways to authenticate Azure clients.
When `client_id` and `subscription_id` are specified, Packer will use the
specified Azure Active Directoty (AAD) Service Principal (SP).
If only `subscription_id` is specified, Packer will try to interactively
log on the current user (tokens will be cached).
If none of these options are specified, Packer will attempt to use the
Managed Identity and subscription of the VM that Packer is running on.
This will only work if Packer is running on an Azure VM.