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 }