github.com/opentofu/opentofu@v1.7.1/internal/backend/remote-state/azure/helpers_test.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package azure
     7  
     8  import (
     9  	"context"
    10  	"fmt"
    11  	"log"
    12  	"os"
    13  	"strings"
    14  	"testing"
    15  	"time"
    16  
    17  	"github.com/Azure/azure-sdk-for-go/profiles/2017-03-09/resources/mgmt/resources"
    18  	armStorage "github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2021-01-01/storage"
    19  	"github.com/Azure/go-autorest/autorest"
    20  	sasStorage "github.com/hashicorp/go-azure-helpers/storage"
    21  	"github.com/tombuildsstuff/giovanni/storage/2018-11-09/blob/containers"
    22  )
    23  
    24  const (
    25  	// required for Azure Stack
    26  	sasSignedVersion = "2015-04-05"
    27  )
    28  
    29  // verify that we are doing ACC tests or the Azure tests specifically
    30  func testAccAzureBackend(t *testing.T) {
    31  	skip := os.Getenv("TF_ACC") == "" && os.Getenv("TF_AZURE_TEST") == ""
    32  	if skip {
    33  		t.Log("azure backend tests require setting TF_ACC or TF_AZURE_TEST")
    34  		t.Skip()
    35  	}
    36  }
    37  
    38  // these kind of tests can only run when within Azure (e.g. MSI)
    39  func testAccAzureBackendRunningInAzure(t *testing.T) {
    40  	testAccAzureBackend(t)
    41  
    42  	if os.Getenv("TF_RUNNING_IN_AZURE") == "" {
    43  		t.Skip("Skipping test since not running in Azure")
    44  	}
    45  }
    46  
    47  // these kind of tests can only run when within GitHub Actions (e.g. OIDC)
    48  func testAccAzureBackendRunningInGitHubActions(t *testing.T) {
    49  	testAccAzureBackend(t)
    50  
    51  	if os.Getenv("TF_RUNNING_IN_GITHUB_ACTIONS") == "" {
    52  		t.Skip("Skipping test since not running in GitHub Actions")
    53  	}
    54  }
    55  
    56  func buildTestClient(t *testing.T, res resourceNames) *ArmClient {
    57  	subscriptionID := os.Getenv("ARM_SUBSCRIPTION_ID")
    58  	tenantID := os.Getenv("ARM_TENANT_ID")
    59  	clientID := os.Getenv("ARM_CLIENT_ID")
    60  	clientSecret := os.Getenv("ARM_CLIENT_SECRET")
    61  	msiEnabled := strings.EqualFold(os.Getenv("ARM_USE_MSI"), "true")
    62  	environment := os.Getenv("ARM_ENVIRONMENT")
    63  
    64  	hasCredentials := (clientID != "" && clientSecret != "") || msiEnabled
    65  	if !hasCredentials {
    66  		t.Fatal("Azure credentials missing or incomplete")
    67  	}
    68  
    69  	if subscriptionID == "" {
    70  		t.Fatalf("Missing ARM_SUBSCRIPTION_ID")
    71  	}
    72  
    73  	if tenantID == "" {
    74  		t.Fatalf("Missing ARM_TENANT_ID")
    75  	}
    76  
    77  	if environment == "" {
    78  		t.Fatalf("Missing ARM_ENVIRONMENT")
    79  	}
    80  
    81  	// location isn't used in this method, but is in the other test methods
    82  	location := os.Getenv("ARM_LOCATION")
    83  	if location == "" {
    84  		t.Fatalf("Missing ARM_LOCATION")
    85  	}
    86  
    87  	// Endpoint is optional (only for Stack)
    88  	endpoint := os.Getenv("ARM_ENDPOINT")
    89  
    90  	armClient, err := buildArmClient(context.TODO(), BackendConfig{
    91  		SubscriptionID:                subscriptionID,
    92  		TenantID:                      tenantID,
    93  		ClientID:                      clientID,
    94  		ClientSecret:                  clientSecret,
    95  		CustomResourceManagerEndpoint: endpoint,
    96  		Environment:                   environment,
    97  		ResourceGroupName:             res.resourceGroup,
    98  		StorageAccountName:            res.storageAccountName,
    99  		UseMsi:                        msiEnabled,
   100  		UseAzureADAuthentication:      res.useAzureADAuth,
   101  	})
   102  	if err != nil {
   103  		t.Fatalf("Failed to build ArmClient: %+v", err)
   104  	}
   105  
   106  	return armClient
   107  }
   108  
   109  func buildSasToken(accountName, accessKey string) (*string, error) {
   110  	// grant full access to Objects in the Blob Storage Account
   111  	permissions := "rwdlacup" // full control
   112  	resourceTypes := "sco"    // service, container, object
   113  	services := "b"           // blob
   114  
   115  	// Details on how to do this are here:
   116  	// https://docs.microsoft.com/en-us/rest/api/storageservices/Constructing-an-Account-SAS
   117  	signedProtocol := "https,http"
   118  	signedIp := ""
   119  	signedVersion := sasSignedVersion
   120  
   121  	utcNow := time.Now().UTC()
   122  
   123  	// account for servers being up to 5 minutes out
   124  	startDate := utcNow.Add(time.Minute * -5).Format(time.RFC3339)
   125  	endDate := utcNow.Add(time.Hour * 24).Format(time.RFC3339)
   126  
   127  	sasToken, err := sasStorage.ComputeAccountSASToken(accountName, accessKey, permissions, services, resourceTypes,
   128  		startDate, endDate, signedProtocol, signedIp, signedVersion)
   129  	if err != nil {
   130  		return nil, fmt.Errorf("Error computing SAS Token: %w", err)
   131  	}
   132  	log.Printf("SAS Token should be %q", sasToken)
   133  	return &sasToken, nil
   134  }
   135  
   136  type resourceNames struct {
   137  	resourceGroup           string
   138  	location                string
   139  	storageAccountName      string
   140  	storageContainerName    string
   141  	storageKeyName          string
   142  	storageAccountAccessKey string
   143  	useAzureADAuth          bool
   144  }
   145  
   146  func testResourceNames(rString string, keyName string) resourceNames {
   147  	return resourceNames{
   148  		resourceGroup:        fmt.Sprintf("acctestRG-backend-%s-%s", strings.Replace(time.Now().Local().Format("060102150405.00"), ".", "", 1), rString),
   149  		location:             os.Getenv("ARM_LOCATION"),
   150  		storageAccountName:   fmt.Sprintf("acctestsa%s", rString),
   151  		storageContainerName: "acctestcont",
   152  		storageKeyName:       keyName,
   153  		useAzureADAuth:       false,
   154  	}
   155  }
   156  
   157  func (c *ArmClient) buildTestResources(ctx context.Context, names *resourceNames) error {
   158  	log.Printf("Creating Resource Group %q", names.resourceGroup)
   159  	_, err := c.groupsClient.CreateOrUpdate(ctx, names.resourceGroup, resources.Group{Location: &names.location})
   160  	if err != nil {
   161  		return fmt.Errorf("failed to create test resource group: %w", err)
   162  	}
   163  
   164  	log.Printf("Creating Storage Account %q in Resource Group %q", names.storageAccountName, names.resourceGroup)
   165  	storageProps := armStorage.AccountCreateParameters{
   166  		Sku: &armStorage.Sku{
   167  			Name: armStorage.StandardLRS,
   168  			Tier: armStorage.Standard,
   169  		},
   170  		Location: &names.location,
   171  	}
   172  	if names.useAzureADAuth {
   173  		allowSharedKeyAccess := false
   174  		storageProps.AccountPropertiesCreateParameters = &armStorage.AccountPropertiesCreateParameters{
   175  			AllowSharedKeyAccess: &allowSharedKeyAccess,
   176  		}
   177  	}
   178  	future, err := c.storageAccountsClient.Create(ctx, names.resourceGroup, names.storageAccountName, storageProps)
   179  	if err != nil {
   180  		return fmt.Errorf("failed to create test storage account: %w", err)
   181  	}
   182  
   183  	err = future.WaitForCompletionRef(ctx, c.storageAccountsClient.Client)
   184  	if err != nil {
   185  		return fmt.Errorf("failed waiting for the creation of storage account: %w", err)
   186  	}
   187  
   188  	containersClient := containers.NewWithEnvironment(c.environment)
   189  	if names.useAzureADAuth {
   190  		containersClient.Client.Authorizer = *c.azureAdStorageAuth
   191  	} else {
   192  		log.Printf("fetching access key for storage account")
   193  		resp, err := c.storageAccountsClient.ListKeys(ctx, names.resourceGroup, names.storageAccountName, "")
   194  		if err != nil {
   195  			return fmt.Errorf("failed to list storage account keys %w:", err)
   196  		}
   197  
   198  		keys := *resp.Keys
   199  		accessKey := *keys[0].Value
   200  		names.storageAccountAccessKey = accessKey
   201  
   202  		storageAuth, err := autorest.NewSharedKeyAuthorizer(names.storageAccountName, accessKey, autorest.SharedKey)
   203  		if err != nil {
   204  			return fmt.Errorf("Error building Authorizer: %w", err)
   205  		}
   206  
   207  		containersClient.Client.Authorizer = storageAuth
   208  	}
   209  
   210  	log.Printf("Creating Container %q in Storage Account %q (Resource Group %q)", names.storageContainerName, names.storageAccountName, names.resourceGroup)
   211  	_, err = containersClient.Create(ctx, names.storageAccountName, names.storageContainerName, containers.CreateInput{})
   212  	if err != nil {
   213  		return fmt.Errorf("failed to create storage container: %w", err)
   214  	}
   215  
   216  	return nil
   217  }
   218  
   219  func (c ArmClient) destroyTestResources(ctx context.Context, resources resourceNames) error {
   220  	log.Printf("[DEBUG] Deleting Resource Group %q..", resources.resourceGroup)
   221  	future, err := c.groupsClient.Delete(ctx, resources.resourceGroup)
   222  	if err != nil {
   223  		return fmt.Errorf("Error deleting Resource Group: %w", err)
   224  	}
   225  
   226  	log.Printf("[DEBUG] Waiting for deletion of Resource Group %q..", resources.resourceGroup)
   227  	err = future.WaitForCompletionRef(ctx, c.groupsClient.Client)
   228  	if err != nil {
   229  		return fmt.Errorf("Error waiting for the deletion of Resource Group: %w", err)
   230  	}
   231  
   232  	return nil
   233  }