Skip to content

Commit

Permalink
backend/azurerm: upgrading the SDK / support for proxies (#19414)
Browse files Browse the repository at this point in the history
* vendor updates

- updating to v21.3.0 of github.com/Azure/azure-sdk-for-go
- updating to v10.15.4 of github.com/Azure/go-autorest
- vendoring github.com/hashicorp/go-azure-helpers @ 0.1.1

* backend/azurerm: refactoring to use the new auth package

- refactoring the backend to use a shared client via the new auth package
- adding tests covering both Service Principal and Access Key auth
- support for authenticating using a proxy
- rewriting the backend documentation to include examples of both authentication types

* switching to use the build-in logging function

* documenting it's also possible to retrieve the access key from an env var
  • Loading branch information
tombuildsstuff authored Nov 21, 2018
1 parent 7d5db95 commit 0ec109b
Show file tree
Hide file tree
Showing 146 changed files with 13,031 additions and 4,784 deletions.
131 changes: 131 additions & 0 deletions backend/remote-state/azure/arm_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package azure

import (
"context"
"fmt"
"os"
"time"

"github.com/Azure/azure-sdk-for-go/profiles/2017-03-09/resources/mgmt/resources"
armStorage "github.com/Azure/azure-sdk-for-go/profiles/2017-03-09/storage/mgmt/storage"
"github.com/Azure/azure-sdk-for-go/storage"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/adal"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/hashicorp/go-azure-helpers/authentication"
"github.com/hashicorp/terraform/httpclient"
)

type ArmClient struct {
// These Clients are only initialized if an Access Key isn't provided
groupsClient *resources.GroupsClient
storageAccountsClient *armStorage.AccountsClient

accessKey string
environment azure.Environment
resourceGroupName string
storageAccountName string
}

func buildArmClient(config BackendConfig) (*ArmClient, error) {
env, err := authentication.DetermineEnvironment(config.Environment)
if err != nil {
return nil, err
}
client := ArmClient{
environment: *env,
resourceGroupName: config.ResourceGroupName,
storageAccountName: config.StorageAccountName,
}

// if we have an Access Key - we don't need the other clients
if config.AccessKey != "" {
client.accessKey = config.AccessKey
return &client, nil
}

builder := authentication.Builder{
ClientID: config.ClientID,
ClientSecret: config.ClientSecret,
SubscriptionID: config.SubscriptionID,
TenantID: config.TenantID,
Environment: config.Environment,

// Feature Toggles
SupportsClientSecretAuth: true,
// TODO: support for Azure CLI / Client Certificate / MSI
}
armConfig, err := builder.Build()
if err != nil {
return nil, fmt.Errorf("Error building ARM Config: %+v", err)
}

oauthConfig, err := adal.NewOAuthConfig(env.ActiveDirectoryEndpoint, armConfig.TenantID)
if err != nil {
return nil, err
}

auth, err := armConfig.GetAuthorizationToken(oauthConfig, env.ResourceManagerEndpoint)
if err != nil {
return nil, err
}

accountsClient := armStorage.NewAccountsClientWithBaseURI(env.ResourceManagerEndpoint, armConfig.SubscriptionID)
client.configureClient(&accountsClient.Client, auth)
client.storageAccountsClient = &accountsClient

groupsClient := resources.NewGroupsClientWithBaseURI(env.ResourceManagerEndpoint, armConfig.SubscriptionID)
client.configureClient(&groupsClient.Client, auth)
client.groupsClient = &groupsClient

return &client, nil
}

func (c ArmClient) getBlobClient(ctx context.Context) (*storage.BlobStorageClient, error) {
if c.accessKey != "" {
storageClient, err := storage.NewBasicClientOnSovereignCloud(c.storageAccountName, c.accessKey, c.environment)
if err != nil {
return nil, fmt.Errorf("Error creating storage client for storage account %q: %s", c.storageAccountName, err)
}
client := storageClient.GetBlobService()
return &client, nil
}

keys, err := c.storageAccountsClient.ListKeys(ctx, c.resourceGroupName, c.storageAccountName)
if err != nil {
return nil, fmt.Errorf("Error retrieving keys for Storage Account %q: %s", c.storageAccountName, err)
}

if keys.Keys == nil {
return nil, fmt.Errorf("Nil key returned for storage account %q", c.storageAccountName)
}

accessKeys := *keys.Keys
accessKey := accessKeys[0].Value

storageClient, err := storage.NewBasicClientOnSovereignCloud(c.storageAccountName, *accessKey, c.environment)
if err != nil {
return nil, fmt.Errorf("Error creating storage client for storage account %q: %s", c.storageAccountName, err)
}
client := storageClient.GetBlobService()
return &client, nil
}

func (c *ArmClient) configureClient(client *autorest.Client, auth autorest.Authorizer) {
client.UserAgent = buildUserAgent()
client.Authorizer = auth
client.Sender = buildSender()
client.SkipResourceProviderRegistration = false
client.PollingDuration = 60 * time.Minute
}

func buildUserAgent() string {
userAgent := httpclient.UserAgentString()

// append the CloudShell version to the user agent if it exists
if azureAgent := os.Getenv("AZURE_HTTP_USER_AGENT"); azureAgent != "" {
userAgent = fmt.Sprintf("%s %s", userAgent, azureAgent)
}

return userAgent
}
121 changes: 21 additions & 100 deletions backend/remote-state/azure/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,11 @@ import (
"context"
"fmt"

armStorage "github.com/Azure/azure-sdk-for-go/arm/storage"
"github.com/Azure/azure-sdk-for-go/storage"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/adal"
"github.com/Azure/go-autorest/autorest/azure"

"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/helper/schema"
)

// New creates a new backend for S3 remote state.
// New creates a new backend for Azure remote state.
func New() backend.Backend {
s := &schema.Backend{
Schema: map[string]*schema.Schema{
Expand All @@ -40,7 +34,7 @@ func New() backend.Backend {
Type: schema.TypeString,
Optional: true,
Description: "The Azure cloud environment.",
DefaultFunc: schema.EnvDefaultFunc("ARM_ENVIRONMENT", ""),
DefaultFunc: schema.EnvDefaultFunc("ARM_ENVIRONMENT", "public"),
},

"access_key": {
Expand Down Expand Up @@ -83,6 +77,9 @@ func New() backend.Backend {
Description: "The Tenant ID.",
DefaultFunc: schema.EnvDefaultFunc("ARM_TENANT_ID", ""),
},

// TODO: rename these fields
// TODO: support for custom resource manager endpoints
},
}

Expand All @@ -95,22 +92,23 @@ type Backend struct {
*schema.Backend

// The fields below are set from configure
blobClient storage.BlobStorageClient

armClient *ArmClient
containerName string
keyName string
leaseID string
}

type BackendConfig struct {
AccessKey string
Environment string
ClientID string
ClientSecret string
ResourceGroupName string
// Required
StorageAccountName string
SubscriptionID string
TenantID string

// Optional
AccessKey string
ClientID string
ClientSecret string
Environment string
ResourceGroupName string
SubscriptionID string
TenantID string
}

func (b *Backend) configure(ctx context.Context) error {
Expand All @@ -135,92 +133,15 @@ func (b *Backend) configure(ctx context.Context) error {
TenantID: data.Get("arm_tenant_id").(string),
}

blobClient, err := getBlobClient(config)
armClient, err := buildArmClient(config)
if err != nil {
return err
}
b.blobClient = blobClient

return nil
}

func getBlobClient(config BackendConfig) (storage.BlobStorageClient, error) {
var client storage.BlobStorageClient

env, err := getAzureEnvironment(config.Environment)
if err != nil {
return client, err
}

accessKey, err := getAccessKey(config, env)
if err != nil {
return client, err
}

storageClient, err := storage.NewClient(config.StorageAccountName, accessKey, env.StorageEndpointSuffix,
storage.DefaultAPIVersion, true)
if err != nil {
return client, fmt.Errorf("Error creating storage client for storage account %q: %s", config.StorageAccountName, err)
}

client = storageClient.GetBlobService()
return client, nil
}

func getAccessKey(config BackendConfig, env azure.Environment) (string, error) {
if config.AccessKey != "" {
return config.AccessKey, nil
}

rgOk := config.ResourceGroupName != ""
subOk := config.SubscriptionID != ""
clientIDOk := config.ClientID != ""
clientSecretOK := config.ClientSecret != ""
tenantIDOk := config.TenantID != ""
if !rgOk || !subOk || !clientIDOk || !clientSecretOK || !tenantIDOk {
return "", fmt.Errorf("resource_group_name and credentials must be provided when access_key is absent")
if config.AccessKey == "" && config.ResourceGroupName == "" {
return fmt.Errorf("Either an Access Key or the Resource Group for the Storage Account must be specified")
}

oauthConfig, err := adal.NewOAuthConfig(env.ActiveDirectoryEndpoint, config.TenantID)
if err != nil {
return "", err
}

spt, err := adal.NewServicePrincipalToken(*oauthConfig, config.ClientID, config.ClientSecret, env.ResourceManagerEndpoint)
if err != nil {
return "", err
}

accountsClient := armStorage.NewAccountsClientWithBaseURI(env.ResourceManagerEndpoint, config.SubscriptionID)
accountsClient.Authorizer = autorest.NewBearerAuthorizer(spt)

keys, err := accountsClient.ListKeys(config.ResourceGroupName, config.StorageAccountName)
if err != nil {
return "", fmt.Errorf("Error retrieving keys for storage account %q: %s", config.StorageAccountName, err)
}

if keys.Keys == nil {
return "", fmt.Errorf("Nil key returned for storage account %q", config.StorageAccountName)
}

accessKeys := *keys.Keys
return *accessKeys[0].Value, nil
}

func getAzureEnvironment(environment string) (azure.Environment, error) {
if environment == "" {
return azure.PublicCloud, nil
}

env, err := azure.EnvironmentFromName(environment)
if err != nil {
// try again with wrapped value to support readable values like german instead of AZUREGERMANCLOUD
var innerErr error
env, innerErr = azure.EnvironmentFromName(fmt.Sprintf("AZURE%sCLOUD", environment))
if innerErr != nil {
return env, fmt.Errorf("invalid 'environment' configuration: %s", err)
}
}

return env, nil
b.armClient = armClient
return nil
}
25 changes: 21 additions & 4 deletions backend/remote-state/azure/backend_state.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package azure

import (
"context"
"fmt"
"sort"
"strings"

"github.com/Azure/azure-sdk-for-go/storage"

"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/state/remote"
Expand All @@ -25,7 +25,12 @@ func (b *Backend) Workspaces() ([]string, error) {
Prefix: prefix,
}

container := b.blobClient.GetContainerReference(b.containerName)
ctx := context.TODO()
client, err := b.armClient.getBlobClient(ctx)
if err != nil {
return nil, err
}
container := client.GetContainerReference(b.containerName)
resp, err := container.ListBlobs(params)
if err != nil {
return nil, err
Expand Down Expand Up @@ -58,16 +63,28 @@ func (b *Backend) DeleteWorkspace(name string) error {
return fmt.Errorf("can't delete default state")
}

containerReference := b.blobClient.GetContainerReference(b.containerName)
ctx := context.TODO()
client, err := b.armClient.getBlobClient(ctx)
if err != nil {
return err
}

containerReference := client.GetContainerReference(b.containerName)
blobReference := containerReference.GetBlobReference(b.path(name))
options := &storage.DeleteBlobOptions{}

return blobReference.Delete(options)
}

func (b *Backend) StateMgr(name string) (state.State, error) {
ctx := context.TODO()
blobClient, err := b.armClient.getBlobClient(ctx)
if err != nil {
return nil, err
}

client := &RemoteClient{
blobClient: b.blobClient,
blobClient: *blobClient,
containerName: b.containerName,
keyName: b.path(name),
}
Expand Down
Loading

0 comments on commit 0ec109b

Please sign in to comment.