// Copyright (c) The OpenTofu Authors // SPDX-License-Identifier: MPL-2.0 // Copyright (c) 2023 HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package oss import ( "fmt" "math/rand" "os" "testing" "time" "strings" "github.com/aliyun/aliyun-oss-go-sdk/oss" "github.com/aliyun/aliyun-tablestore-go-sdk/tablestore" "github.com/opentofu/opentofu/internal/backend" "github.com/opentofu/opentofu/internal/encryption" "github.com/opentofu/opentofu/internal/legacy/hcl2shim" ) // verify that we are doing ACC tests or the OSS tests specifically func testACC(t *testing.T) { t.Helper() skip := os.Getenv("TF_ACC") == "" && os.Getenv("TF_OSS_TEST") == "" if skip { t.Log("oss backend tests require setting TF_ACC or TF_OSS_TEST") t.Skip() } if skip { t.Fatal("oss backend tests require setting ALICLOUD_ACCESS_KEY or ALICLOUD_ACCESS_KEY_ID") } t.Setenv("ALICLOUD_REGION", "cn-beijing") } func TestBackend_impl(t *testing.T) { var _ backend.Backend = new(Backend) } func TestBackendConfig(t *testing.T) { testACC(t) config := map[string]interface{}{ "region": "cn-beijing", "bucket": "terraform-backend-oss-test", "prefix": "mystate", "key": "first.tfstate", "tablestore_endpoint": "https://terraformstate.cn-beijing.ots.aliyuncs.com", "tablestore_table": "TableStore", } b := backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), backend.TestWrapConfig(config)).(*Backend) if !strings.HasPrefix(b.ossClient.Config.Endpoint, "https://oss-cn-beijing") { t.Fatalf("Incorrect region was provided") } if b.bucketName != "terraform-backend-oss-test" { t.Fatalf("Incorrect bucketName was provided") } if b.statePrefix != "mystate" { t.Fatalf("Incorrect state file path was provided") } if b.stateKey != "first.tfstate" { t.Fatalf("Incorrect keyName was provided") } if b.ossClient.Config.AccessKeyID == "" { t.Fatalf("No Access Key Id was provided") } if b.ossClient.Config.AccessKeySecret == "" { t.Fatalf("No Secret Access Key was provided") } } func TestBackendConfigWorkSpace(t *testing.T) { testACC(t) bucketName := fmt.Sprintf("terraform-backend-oss-test-%d", rand.Intn(1000)) config := map[string]interface{}{ "region": "cn-beijing", "bucket": bucketName, "prefix": "mystate", "key": "first.tfstate", "tablestore_endpoint": "https://terraformstate.cn-beijing.ots.aliyuncs.com", "tablestore_table": "TableStore", } b := backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), backend.TestWrapConfig(config)).(*Backend) createOSSBucket(t, b.ossClient, bucketName) defer deleteOSSBucket(t, b.ossClient, bucketName) if _, err := b.Workspaces(t.Context()); err != nil { t.Fatal(err.Error()) } if !strings.HasPrefix(b.ossClient.Config.Endpoint, "https://oss-cn-beijing") { t.Fatalf("Incorrect region was provided") } if b.bucketName != bucketName { t.Fatalf("Incorrect bucketName was provided") } if b.statePrefix != "mystate" { t.Fatalf("Incorrect state file path was provided") } if b.stateKey != "first.tfstate" { t.Fatalf("Incorrect keyName was provided") } if b.ossClient.Config.AccessKeyID == "" { t.Fatalf("No Access Key Id was provided") } if b.ossClient.Config.AccessKeySecret == "" { t.Fatalf("No Secret Access Key was provided") } } func TestBackendConfigProfile(t *testing.T) { testACC(t) config := map[string]interface{}{ "region": "cn-beijing", "bucket": "terraform-backend-oss-test", "prefix": "mystate", "key": "first.tfstate", "tablestore_endpoint": "https://terraformstate.cn-beijing.ots.aliyuncs.com", "tablestore_table": "TableStore", "profile": "default", } b := backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), backend.TestWrapConfig(config)).(*Backend) if !strings.HasPrefix(b.ossClient.Config.Endpoint, "https://oss-cn-beijing") { t.Fatalf("Incorrect region was provided") } if b.bucketName != "terraform-backend-oss-test" { t.Fatalf("Incorrect bucketName was provided") } if b.statePrefix != "mystate" { t.Fatalf("Incorrect state file path was provided") } if b.stateKey != "first.tfstate" { t.Fatalf("Incorrect keyName was provided") } if b.ossClient.Config.AccessKeyID == "" { t.Fatalf("No Access Key Id was provided") } if b.ossClient.Config.AccessKeySecret == "" { t.Fatalf("No Secret Access Key was provided") } } func TestBackendConfig_invalidKey(t *testing.T) { testACC(t) cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{ "region": "cn-beijing", "bucket": "terraform-backend-oss-test", "prefix": "/leading-slash", "name": "/test.tfstate", "tablestore_endpoint": "https://terraformstate.cn-beijing.ots.aliyuncs.com", "tablestore_table": "TableStore", }) _, results := New(encryption.StateEncryptionDisabled()).PrepareConfig(cfg) if !results.HasErrors() { t.Fatal("expected config validation error") } } func TestBackend(t *testing.T) { testACC(t) bucketName := fmt.Sprintf("terraform-remote-oss-test-%x", time.Now().Unix()) statePrefix := "multi/level/path/" b1 := backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), backend.TestWrapConfig(map[string]interface{}{ "bucket": bucketName, "prefix": statePrefix, })).(*Backend) b2 := backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), backend.TestWrapConfig(map[string]interface{}{ "bucket": bucketName, "prefix": statePrefix, })).(*Backend) createOSSBucket(t, b1.ossClient, bucketName) defer deleteOSSBucket(t, b1.ossClient, bucketName) backend.TestBackendStates(t, b1) backend.TestBackendStateLocks(t, b1, b2) backend.TestBackendStateForceUnlock(t, b1, b2) } func createOSSBucket(t *testing.T, ossClient *oss.Client, bucketName string) { // Be clear about what we're doing in case the user needs to clean this up later. if err := ossClient.CreateBucket(bucketName); err != nil { t.Fatal("failed to create test OSS bucket:", err) } } func deleteOSSBucket(t *testing.T, ossClient *oss.Client, bucketName string) { warning := "WARNING: Failed to delete the test OSS bucket. It may have been left in your Alibaba Cloud account and may incur storage charges. (error was %s)" // first we have to get rid of the env objects, or we can't delete the bucket bucket, err := ossClient.Bucket(bucketName) if err != nil { t.Fatal("Error getting bucket:", err) return } objects, err := bucket.ListObjects() if err != nil { t.Logf(warning, err) return } for _, obj := range objects.Objects { if err := bucket.DeleteObject(obj.Key); err != nil { // this will need cleanup no matter what, so just warn and exit t.Logf(warning, err) return } } if err := ossClient.DeleteBucket(bucketName); err != nil { t.Logf(warning, err) } } // create the tablestore table, and wait until we can query it. func createTablestoreTable(t *testing.T, otsClient *tablestore.TableStoreClient, tableName string) { tableMeta := new(tablestore.TableMeta) tableMeta.TableName = tableName tableMeta.AddPrimaryKeyColumn(pkName, tablestore.PrimaryKeyType_STRING) tableOption := new(tablestore.TableOption) tableOption.TimeToAlive = -1 tableOption.MaxVersion = 1 reservedThroughput := new(tablestore.ReservedThroughput) _, err := otsClient.CreateTable(&tablestore.CreateTableRequest{ TableMeta: tableMeta, TableOption: tableOption, ReservedThroughput: reservedThroughput, }) if err != nil { t.Fatal(err) } } func deleteTablestoreTable(t *testing.T, otsClient *tablestore.TableStoreClient, tableName string) { params := &tablestore.DeleteTableRequest{ TableName: tableName, } _, err := otsClient.DeleteTable(params) if err != nil { t.Logf("WARNING: Failed to delete the test TableStore table %q. It has been left in your Alibaba Cloud account and may incur charges. (error was %s)", tableName, err) } } func TestGetHttpProxyUrl(t *testing.T) { tests := []struct { name string rawUrl string httpProxy string httpsProxy string noProxy string expectedProxyURL string }{ { name: "should set proxy using http_proxy environment variable", rawUrl: "http://example.com", httpProxy: "http://foo.bar:3128", httpsProxy: "https://secure.example.com", noProxy: "", expectedProxyURL: "http://foo.bar:3128", }, { name: "should set proxy using http_proxy environment variable", rawUrl: "http://example.com", httpProxy: "http://foo.barr", httpsProxy: "https://secure.example.com", noProxy: "", expectedProxyURL: "http://foo.barr", }, { name: "should set proxy using https_proxy environment variable", rawUrl: "https://secure.example.com", httpProxy: "http://foo.bar", httpsProxy: "https://foo.bar.com:3128", noProxy: "", expectedProxyURL: "https://foo.bar.com:3128", }, { name: "should set proxy using https_proxy environment variable", rawUrl: "https://secure.example.com", httpProxy: "", httpsProxy: "http://foo.baz", noProxy: "", expectedProxyURL: "http://foo.baz", }, { name: "should not set http proxy if NO_PROXY contains the host", rawUrl: "http://example.internal", httpProxy: "http://foo.bar:3128", httpsProxy: "", noProxy: "example.internal", expectedProxyURL: "", }, { name: "should not set HTTP proxy when NO_PROXY matches the domain with suffix", rawUrl: "http://qqu.example.internal", httpProxy: "http://foo.bar:3128", httpsProxy: "", noProxy: ".example.internal", expectedProxyURL: "", }, { name: "should not set https proxy if NO_PROXY contains the host", rawUrl: "https://secure.internal", httpProxy: "", httpsProxy: "https://foo.baz:3128", noProxy: "secure.internal", expectedProxyURL: "", }, { name: "should not set https proxy if NO_PROXY matches the domain with suffix", rawUrl: "https://ss.qcsc.secure.internal", httpProxy: "", httpsProxy: "https://foo.baz:3128", noProxy: ".secure.internal", expectedProxyURL: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Set environment variables t.Setenv("HTTP_PROXY", tt.httpProxy) t.Setenv("HTTPS_PROXY", tt.httpsProxy) t.Setenv("NO_PROXY", tt.noProxy) proxyUrl, err := getHttpProxyUrl(tt.rawUrl) if err != nil { t.Fatalf("unexpected error: %s", err) } if tt.expectedProxyURL == "" { if proxyUrl != nil { t.Fatalf("unexpected proxy URL, want nil, got: %s", proxyUrl) } } else { if tt.expectedProxyURL != proxyUrl.String() { t.Fatalf("unexpected proxy URL, want: %s, got: %s", tt.expectedProxyURL, proxyUrl.String()) } } }) } }