mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-22 03:07:51 -05:00
519 lines
18 KiB
Go
519 lines
18 KiB
Go
// Copyright (c) The OpenTofu Authors
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package azure
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
|
"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
|
|
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
|
|
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
|
|
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
|
|
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
|
|
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
|
|
"github.com/opentofu/opentofu/internal/backend"
|
|
"github.com/opentofu/opentofu/internal/backend/remote-state/azure/auth"
|
|
"github.com/opentofu/opentofu/internal/encryption"
|
|
"github.com/opentofu/opentofu/internal/httpclient"
|
|
"github.com/opentofu/opentofu/internal/legacy/helper/acctest"
|
|
)
|
|
|
|
func TestBackend_impl(t *testing.T) {
|
|
var _ backend.Backend = new(Backend)
|
|
}
|
|
|
|
func TestBackendConfig(t *testing.T) {
|
|
// This test just instantiates the client. Shouldn't make any actual
|
|
// requests nor incur any costs.
|
|
|
|
config := map[string]interface{}{
|
|
"storage_account_name": "tfaccount",
|
|
"container_name": "tfcontainer",
|
|
"key": "state",
|
|
"snapshot": false,
|
|
// Access Key must be Base64
|
|
"access_key": "QUNDRVNTX0tFWQ0K",
|
|
}
|
|
|
|
b := backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), backend.TestWrapConfig(config)).(*Backend)
|
|
|
|
if b.containerName != "tfcontainer" {
|
|
t.Fatalf("Incorrect bucketName was populated")
|
|
}
|
|
if b.keyName != "state" {
|
|
t.Fatalf("Incorrect keyName was populated")
|
|
}
|
|
if b.snapshot != false {
|
|
t.Fatalf("Incorrect snapshot was populated")
|
|
}
|
|
}
|
|
|
|
func TestBackendConfig_Timeout(t *testing.T) {
|
|
config := map[string]any{
|
|
"storage_account_name": "tfaccount",
|
|
"container_name": "tfcontainer",
|
|
"key": "state",
|
|
"snapshot": false,
|
|
// Access Key must be Base64
|
|
"access_key": "QUNDRVNTX0tFWQ0K",
|
|
}
|
|
testCases := []struct {
|
|
name string
|
|
timeoutSeconds any
|
|
expectError bool
|
|
}{
|
|
{
|
|
name: "string timeout",
|
|
timeoutSeconds: "Nonsense",
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "negative timeout",
|
|
timeoutSeconds: -10,
|
|
expectError: true,
|
|
},
|
|
{
|
|
// 0 is a valid timeout value, it disables the timeout
|
|
name: "zero timeout",
|
|
timeoutSeconds: 0,
|
|
},
|
|
{
|
|
name: "positive timeout",
|
|
timeoutSeconds: 10,
|
|
},
|
|
}
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
config["timeout_seconds"] = tc.timeoutSeconds
|
|
b, _, errors := backend.TestBackendConfigWarningsAndErrors(t, New(encryption.StateEncryptionDisabled()), backend.TestWrapConfig(config))
|
|
if tc.expectError {
|
|
if len(errors) == 0 {
|
|
t.Fatalf("Expected an error")
|
|
}
|
|
return
|
|
}
|
|
if !tc.expectError && len(errors) > 0 {
|
|
t.Fatalf("Expected no errors, got: %v", errors)
|
|
}
|
|
be, ok := b.(*Backend)
|
|
if !ok || be == nil {
|
|
t.Fatalf("Expected initialized Backend, got %T", b)
|
|
}
|
|
if int(be.timeout.Seconds()) != tc.timeoutSeconds {
|
|
t.Fatalf("Expected timeoutSeconds to be %d, got %d", tc.timeoutSeconds, int(be.timeout.Seconds()))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type mockClient struct {
|
|
marker string
|
|
}
|
|
|
|
func (p mockClient) NewListBlobsFlatPager(params *container.ListBlobsFlatOptions) *runtime.Pager[container.ListBlobsFlatResponse] {
|
|
env_name := "env-name"
|
|
blobDetails := make([]*container.BlobItem, 5000)
|
|
for i := range blobDetails {
|
|
blobDetails[i] = &container.BlobItem{}
|
|
blobDetails[i].Name = &env_name
|
|
}
|
|
|
|
returnMarker := "next-token"
|
|
|
|
return runtime.NewPager(runtime.PagingHandler[container.ListBlobsFlatResponse]{
|
|
More: func(resp container.ListBlobsFlatResponse) bool {
|
|
return *resp.Marker != returnMarker
|
|
},
|
|
Fetcher: func(context.Context, *container.ListBlobsFlatResponse) (container.ListBlobsFlatResponse, error) {
|
|
prevMarker := p.marker
|
|
p.marker = returnMarker
|
|
return container.ListBlobsFlatResponse{
|
|
ListBlobsFlatSegmentResponse: container.ListBlobsFlatSegmentResponse{
|
|
Segment: &container.BlobFlatListSegment{
|
|
BlobItems: blobDetails,
|
|
},
|
|
Marker: &prevMarker,
|
|
NextMarker: &returnMarker,
|
|
},
|
|
}, nil
|
|
},
|
|
})
|
|
}
|
|
|
|
func TestBackendPagination(t *testing.T) {
|
|
ctx := context.Background()
|
|
client := &mockClient{}
|
|
result, err := getPaginatedResults(ctx, client, "env")
|
|
if err != nil {
|
|
t.Fatalf("error getting paginated results %q", err)
|
|
}
|
|
|
|
// default is always on the list + 10k generated blobs from the mocked ListBlobs
|
|
if len(result) != 10001 {
|
|
t.Fatalf("expected len 10001, got %d instead", len(result))
|
|
}
|
|
}
|
|
|
|
func TestStorageNames(t *testing.T) {
|
|
err := checkAccountAndContainerNames("goodaccountname1000", "good-container-name-2")
|
|
if err != nil {
|
|
t.Fatalf("encountered an error on good storage name: %v", err)
|
|
}
|
|
err = checkAccountAndContainerNames("bad-accountname1000", "good-container-name-2")
|
|
if err == nil {
|
|
t.Fatalf("encountered no error on a bad storage name: account names cannot have hyphens")
|
|
}
|
|
err = checkAccountAndContainerNames("", "good-container-name-2")
|
|
if err == nil {
|
|
t.Fatalf("encountered no error on a bad storage name: account names must have 24 characters or fewer")
|
|
}
|
|
err = checkAccountAndContainerNames("aa", "good-container-name-2")
|
|
if err == nil {
|
|
t.Fatalf("encountered no error on a bad storage name: account names must have 3 characters or more")
|
|
}
|
|
err = checkAccountAndContainerNames("goodaccountname1000", "-bad-container-2")
|
|
if err == nil {
|
|
t.Fatalf("encountered no error on a bad container name: container cannot start with a hyphen")
|
|
}
|
|
err = checkAccountAndContainerNames("goodaccountname1000", "bad-container-2-")
|
|
if err == nil {
|
|
t.Fatalf("encountered no error on a bad container name: container cannot end with a hyphen")
|
|
}
|
|
err = checkAccountAndContainerNames("goodaccountname1000", "bad--container-2")
|
|
if err == nil {
|
|
t.Fatalf("encountered no error on a bad container name: container cannot have consecutive hyphens")
|
|
}
|
|
err = checkAccountAndContainerNames("goodaccountname1000", "myveryeducatedmotherjustservedusnachoswaitwhathappenedtothe9pies")
|
|
if err == nil {
|
|
t.Fatalf("encountered no error on a bad container name: containers must have 63 characters or fewer")
|
|
}
|
|
err = checkAccountAndContainerNames("goodaccountname1000", "x")
|
|
if err == nil {
|
|
t.Fatalf("encountered no error on a bad container name: containers must have 3 characters or more")
|
|
}
|
|
}
|
|
|
|
// TestAccBackendAccessKeyBasic tests if the backend functions when using basic access key.
|
|
func TestAccBackendAccessKeyBasic(t *testing.T) {
|
|
testAccAzureBackend(t)
|
|
rs := acctest.RandString(4)
|
|
res := testResourceNames(rs, "testState")
|
|
|
|
authMethod, err := auth.GetAuthMethod(t.Context(), testAuthConfig())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
authCred, err := authMethod.Construct(t.Context(), testAuthConfig())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
resourceGroupClient, _, err := createTestResources(t, &res, authCred)
|
|
|
|
t.Cleanup(func() {
|
|
destroyTestResources(t, resourceGroupClient, res)
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// The call to backend.TestBackendStates tests workspace creation, list and deletion.
|
|
b1 := backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), backend.TestWrapConfig(map[string]interface{}{
|
|
"storage_account_name": res.storageAccountName,
|
|
"container_name": res.storageContainerName,
|
|
"key": res.storageKeyName,
|
|
"access_key": res.storageAccountAccessKey,
|
|
"use_cli": false,
|
|
})).(*Backend)
|
|
|
|
backend.TestBackendStates(t, b1)
|
|
|
|
b2 := backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), backend.TestWrapConfig(map[string]interface{}{
|
|
"storage_account_name": res.storageAccountName,
|
|
"container_name": res.storageContainerName,
|
|
"key": res.storageKeyName,
|
|
"access_key": res.storageAccountAccessKey,
|
|
"use_cli": false,
|
|
})).(*Backend)
|
|
|
|
// TestBackendStateForceUnlock runs the both the TestBackendStateLocks test and the --force-unlock tests
|
|
backend.TestBackendStateForceUnlock(t, b1, b2)
|
|
|
|
backend.TestBackendStateLocksInWS(t, b1, b2, "foo")
|
|
backend.TestBackendStateForceUnlockInWS(t, b1, b2, "foo")
|
|
}
|
|
|
|
// TestAccBackendSASToken tests if the backend functions when using a SAS token.
|
|
func TestAccBackendSASToken(t *testing.T) {
|
|
testAccAzureBackend(t)
|
|
rs := acctest.RandString(4)
|
|
res := testResourceNames(rs, "testState")
|
|
|
|
authMethod, err := auth.GetAuthMethod(t.Context(), testAuthConfig())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
authCred, err := authMethod.Construct(t.Context(), testAuthConfig())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
resourceGroupClient, _, err := createTestResources(t, &res, authCred)
|
|
|
|
t.Cleanup(func() {
|
|
destroyTestResources(t, resourceGroupClient, res)
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
keycred, err := azblob.NewSharedKeyCredential(res.storageAccountName, res.storageAccountAccessKey)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
sasToken, err := getSASToken(keycred)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
b1 := backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), backend.TestWrapConfig(map[string]interface{}{
|
|
"storage_account_name": res.storageAccountName,
|
|
"container_name": res.storageContainerName,
|
|
"key": res.storageKeyName,
|
|
"sas_token": sasToken,
|
|
"use_cli": false,
|
|
})).(*Backend)
|
|
|
|
backend.TestBackendStates(t, b1)
|
|
|
|
b2 := backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), backend.TestWrapConfig(map[string]interface{}{
|
|
"storage_account_name": res.storageAccountName,
|
|
"container_name": res.storageContainerName,
|
|
"key": res.storageKeyName,
|
|
"sas_token": sasToken,
|
|
"use_cli": false,
|
|
})).(*Backend)
|
|
|
|
backend.TestBackendStateForceUnlock(t, b1, b2)
|
|
|
|
backend.TestBackendStateLocksInWS(t, b1, b2, "foo")
|
|
backend.TestBackendStateForceUnlockInWS(t, b1, b2, "foo")
|
|
}
|
|
|
|
// TestAccBackendServicePrincipalClientSecret tests if the backend functions when using a client ID and secret.
|
|
func TestAccBackendServicePrincipalClientSecret(t *testing.T) {
|
|
testAccAzureBackend(t)
|
|
rs := acctest.RandString(4)
|
|
res := testResourceNames(rs, "testState")
|
|
|
|
client_id := os.Getenv("TF_AZURE_TEST_CLIENT_ID")
|
|
client_secret := os.Getenv("TF_AZURE_TEST_CLIENT_SECRET")
|
|
if client_id == "" || client_secret == "" {
|
|
t.Skip(`
|
|
A client ID or client secret was not provided.
|
|
Please set TF_AZURE_TEST_CLIENT_ID and TF_AZURE_TEST_CLIENT_SECRET, either manually or using the terraform plan in the meta-test folder.`)
|
|
}
|
|
if res.tenantID == "" {
|
|
t.Fatal(errors.New("A tenant ID must be provided through ARM_TENANT_ID in order to run this test."))
|
|
}
|
|
|
|
authMethod, err := auth.GetAuthMethod(t.Context(), testAuthConfig())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
authCred, err := authMethod.Construct(t.Context(), testAuthConfig())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
resourceGroupClient, _, err := createTestResources(t, &res, authCred)
|
|
|
|
t.Cleanup(func() {
|
|
destroyTestResources(t, resourceGroupClient, res)
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
b1 := backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), backend.TestWrapConfig(map[string]interface{}{
|
|
"storage_account_name": res.storageAccountName,
|
|
"container_name": res.storageContainerName,
|
|
"key": res.storageKeyName,
|
|
"resource_group_name": res.resourceGroup,
|
|
"client_id": client_id,
|
|
"client_secret": client_secret,
|
|
"use_cli": false,
|
|
})).(*Backend)
|
|
|
|
backend.TestBackendStates(t, b1)
|
|
|
|
b2 := backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), backend.TestWrapConfig(map[string]interface{}{
|
|
"storage_account_name": res.storageAccountName,
|
|
"container_name": res.storageContainerName,
|
|
"key": res.storageKeyName,
|
|
"resource_group_name": res.resourceGroup,
|
|
"client_id": client_id,
|
|
"client_secret": client_secret,
|
|
"use_cli": false,
|
|
})).(*Backend)
|
|
|
|
// TestBackendStateForceUnlock runs the both the TestBackendStateLocks test and the --force-unlock tests
|
|
backend.TestBackendStateForceUnlock(t, b1, b2)
|
|
|
|
backend.TestBackendStateLocksInWS(t, b1, b2, "foo")
|
|
backend.TestBackendStateForceUnlockInWS(t, b1, b2, "foo")
|
|
}
|
|
|
|
// TestAccBackendServicePrincipalClientCertificate tests if the backend functions when using a PFX certificate file.
|
|
func TestAccBackendServicePrincipalClientCertificate(t *testing.T) {
|
|
testAccAzureBackend(t)
|
|
rs := acctest.RandString(4)
|
|
res := testResourceNames(rs, "testState")
|
|
client_id := os.Getenv("TF_AZURE_TEST_CLIENT_ID")
|
|
cert_path := os.Getenv("TF_AZURE_TEST_CERT_PATH")
|
|
// cert_password may be empty
|
|
cert_password := os.Getenv("TF_AZURE_TEST_CERT_PASSWORD")
|
|
if client_id == "" || cert_path == "" {
|
|
t.Skip("A certificate must be provided through TF_AZURE_TEST_CERT_PATH, and a client_id must be provided through TF_AZURE_TEST_CLIENT_ID")
|
|
}
|
|
// Make sure we can open and read the file
|
|
cert_file, err := os.Open(cert_path)
|
|
if err != nil {
|
|
t.Fatalf("error opening cert file: %s", err.Error())
|
|
}
|
|
_, err = io.ReadAll(cert_file)
|
|
if err != nil {
|
|
t.Fatalf("error reading cert file: %s", err.Error())
|
|
}
|
|
cert_file.Close()
|
|
|
|
authMethod, err := auth.GetAuthMethod(t.Context(), testAuthConfig())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
authCred, err := authMethod.Construct(t.Context(), testAuthConfig())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
resourceGroupClient, _, err := createTestResources(t, &res, authCred)
|
|
|
|
t.Cleanup(func() {
|
|
destroyTestResources(t, resourceGroupClient, res)
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
b1 := backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), backend.TestWrapConfig(map[string]interface{}{
|
|
"storage_account_name": res.storageAccountName,
|
|
"container_name": res.storageContainerName,
|
|
"key": res.storageKeyName,
|
|
"resource_group_name": res.resourceGroup,
|
|
"client_id": client_id,
|
|
"client_certificate_path": cert_path,
|
|
"client_certificate_password": cert_password,
|
|
"use_cli": false,
|
|
})).(*Backend)
|
|
|
|
backend.TestBackendStates(t, b1)
|
|
}
|
|
|
|
// TestAccBackendManagedServiceIdentity tests if the backend functions when using a managed service identity, like on an Azure VM.
|
|
// Note: this test does NOT create its own resource group, storage account, or storage container. You must set up that infrastructure
|
|
// manually, as well as the underlying managed service identity which this test depends upon.
|
|
func TestAccBackendManagedServiceIdentity(t *testing.T) {
|
|
testAccAzureBackend(t)
|
|
|
|
storageAccountName := os.Getenv("TF_AZURE_TEST_STORAGE_ACCOUNT_NAME")
|
|
resourceGroupName := os.Getenv("TF_AZURE_TEST_RESOURCE_GROUP_NAME")
|
|
containerName := os.Getenv("TF_AZURE_TEST_CONTAINER_NAME")
|
|
|
|
if storageAccountName == "" || resourceGroupName == "" || containerName == "" {
|
|
t.Skip("For MSI tests, all infrastructure must be set up ahead of time and passed through environment variables.")
|
|
}
|
|
|
|
b := backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), backend.TestWrapConfig(map[string]interface{}{
|
|
"storage_account_name": storageAccountName,
|
|
"container_name": containerName,
|
|
"key": "testState",
|
|
"resource_group_name": resourceGroupName,
|
|
"use_msi": true,
|
|
"use_cli": false,
|
|
})).(*Backend)
|
|
|
|
backend.TestBackendStates(t, b)
|
|
|
|
// Manually delete all blobs in the container
|
|
client := httpclient.New(t.Context())
|
|
|
|
authCred, err := azidentity.NewManagedIdentityCredential(
|
|
&azidentity.ManagedIdentityCredentialOptions{ClientOptions: azcore.ClientOptions{
|
|
Telemetry: policy.TelemetryOptions{
|
|
Disabled: true,
|
|
},
|
|
Transport: client,
|
|
Cloud: cloud.AzurePublic,
|
|
}},
|
|
)
|
|
if err != nil {
|
|
t.Logf("Skipping deleting blobs in container %s due to error obtaining credentials: %v", containerName, err)
|
|
return
|
|
}
|
|
|
|
deleteBlobsManually(t, authCred, storageAccountName, resourceGroupName, containerName)
|
|
}
|
|
|
|
// TestAccBackendAKSWorkloadIdentity tests if the backend functions when using workload identity, on Azure AKS (Kubernetes).
|
|
// Note: this test does NOT create its own resource group, storage account, or storage container. You must set up that infrastructure
|
|
// manually, as well as the kubernetes cluster, workload identity, and managed identity which this test depends upon.
|
|
func TestAccBackendAKSWorkloadIdentity(t *testing.T) {
|
|
testAccAzureBackend(t)
|
|
|
|
storageAccountName := os.Getenv("TF_AZURE_TEST_STORAGE_ACCOUNT_NAME")
|
|
resourceGroupName := os.Getenv("TF_AZURE_TEST_RESOURCE_GROUP_NAME")
|
|
containerName := os.Getenv("TF_AZURE_TEST_CONTAINER_NAME")
|
|
|
|
if storageAccountName == "" || resourceGroupName == "" || containerName == "" {
|
|
t.Skip("For MSI tests, all infrastructure must be set up ahead of time and passed through environment variables.")
|
|
}
|
|
|
|
b := backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), backend.TestWrapConfig(map[string]interface{}{
|
|
"storage_account_name": storageAccountName,
|
|
"container_name": containerName,
|
|
"key": "testState",
|
|
"resource_group_name": resourceGroupName,
|
|
"use_aks_workload_identity": true,
|
|
"use_cli": false,
|
|
})).(*Backend)
|
|
|
|
backend.TestBackendStates(t, b)
|
|
client := httpclient.New(t.Context())
|
|
|
|
authCred, err := azidentity.NewWorkloadIdentityCredential(
|
|
&azidentity.WorkloadIdentityCredentialOptions{ClientOptions: azcore.ClientOptions{
|
|
Telemetry: policy.TelemetryOptions{
|
|
Disabled: true,
|
|
},
|
|
Transport: client,
|
|
Cloud: cloud.AzurePublic,
|
|
}},
|
|
)
|
|
if err != nil {
|
|
t.Logf("Skipping deleting blobs in container %s due to error obtaining credentials: %v", containerName, err)
|
|
return
|
|
}
|
|
|
|
// Manually delete all blobs in the container
|
|
deleteBlobsManually(t, authCred, storageAccountName, resourceGroupName, containerName)
|
|
}
|