mirror of https://github.com/hashicorp/terraform
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
476 lines
15 KiB
476 lines
15 KiB
// Copyright IBM Corp. 2014, 2026
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package oci
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/hashicorp/terraform/internal/configs/configschema"
|
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
"github.com/hashicorp/terraform/internal/backend"
|
|
"github.com/oracle/oci-go-sdk/v65/common"
|
|
"github.com/oracle/oci-go-sdk/v65/objectstorage"
|
|
)
|
|
|
|
func TestBackendBasic(t *testing.T) {
|
|
testACC(t)
|
|
|
|
ctx := context.Background()
|
|
|
|
bucketName := fmt.Sprintf("terraform-remote-oci-test-%x", time.Now().Unix())
|
|
keyName := "testState.json"
|
|
namespace := getEnvSettingWithBlankDefault(NamespaceAttrName)
|
|
compartmentId := getEnvSettingWithBlankDefault("compartment_id")
|
|
|
|
b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
|
|
"bucket": bucketName,
|
|
"key": keyName,
|
|
"namespace": namespace,
|
|
})).(*Backend)
|
|
|
|
response := createOCIBucket(ctx, t, b.client.objectStorageClient, bucketName, namespace, compartmentId)
|
|
defer deleteOCIBucket(ctx, t, b.client.objectStorageClient, bucketName, *response.ETag, namespace)
|
|
|
|
backend.TestBackendStates(t, b)
|
|
}
|
|
func TestBackendLocked_ForceUnlock(t *testing.T) {
|
|
testACC(t)
|
|
ctx := context.Background()
|
|
bucketName := fmt.Sprintf("terraform-remote-oci-test-%x", time.Now().Unix())
|
|
keyName := "testState.json"
|
|
namespace := getEnvSettingWithBlankDefault(NamespaceAttrName)
|
|
compartmentId := getEnvSettingWithBlankDefault("compartment_id")
|
|
b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
|
|
"bucket": bucketName,
|
|
"key": keyName,
|
|
"namespace": namespace,
|
|
})).(*Backend)
|
|
b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
|
|
"bucket": bucketName,
|
|
"key": keyName,
|
|
"namespace": namespace,
|
|
})).(*Backend)
|
|
response := createOCIBucket(ctx, t, b1.client.objectStorageClient, bucketName, namespace, compartmentId)
|
|
defer deleteOCIBucket(ctx, t, b1.client.objectStorageClient, bucketName, *response.ETag, namespace)
|
|
// Test state locking and force-unlock
|
|
backend.TestBackendStateLocks(t, b1, b2)
|
|
backend.TestBackendStateLocksInWS(t, b1, b2, "testenv")
|
|
backend.TestBackendStateForceUnlock(t, b1, b2)
|
|
backend.TestBackendStateForceUnlockInWS(t, b1, b2, "testenv")
|
|
}
|
|
func TestBackendBasic_multipart_Upload(t *testing.T) {
|
|
testACC(t)
|
|
|
|
ctx := context.Background()
|
|
DefaultFilePartSize = 100 // 100 Bytes
|
|
bucketName := fmt.Sprintf("terraform-remote-oci-test-%x", time.Now().Unix())
|
|
keyName := "testState.json"
|
|
namespace := getEnvSettingWithBlankDefault(NamespaceAttrName)
|
|
compartmentId := getEnvSettingWithBlankDefault("compartment_id")
|
|
|
|
b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
|
|
"bucket": bucketName,
|
|
"key": keyName,
|
|
"namespace": namespace,
|
|
})).(*Backend)
|
|
|
|
response := createOCIBucket(ctx, t, b.client.objectStorageClient, bucketName, namespace, compartmentId)
|
|
defer deleteOCIBucket(ctx, t, b.client.objectStorageClient, bucketName, *response.ETag, namespace)
|
|
|
|
backend.TestBackendStates(t, b)
|
|
}
|
|
|
|
// Helper functions to create and delete OCI bucket
|
|
func createOCIBucket(ctx context.Context, t *testing.T, client *objectstorage.ObjectStorageClient, bucketName, namespace, compartmentId string) objectstorage.CreateBucketResponse {
|
|
req := objectstorage.CreateBucketRequest{
|
|
NamespaceName: common.String(namespace),
|
|
CreateBucketDetails: objectstorage.CreateBucketDetails{
|
|
Name: common.String(bucketName),
|
|
CompartmentId: common.String(compartmentId),
|
|
Versioning: objectstorage.CreateBucketDetailsVersioningEnabled,
|
|
},
|
|
}
|
|
|
|
response, err := client.CreateBucket(ctx, req)
|
|
if err != nil {
|
|
t.Fatalf("failed to create OCI bucket: %v", err)
|
|
}
|
|
return response
|
|
}
|
|
|
|
func deleteOCIBucket(ctx context.Context, t *testing.T, client *objectstorage.ObjectStorageClient, bucketName, etag, namespace string) {
|
|
request := objectstorage.ListObjectVersionsRequest{
|
|
BucketName: common.String(bucketName),
|
|
NamespaceName: common.String(namespace),
|
|
Prefix: common.String(""),
|
|
RequestMetadata: common.RequestMetadata{
|
|
RetryPolicy: getDefaultRetryPolicy(),
|
|
},
|
|
}
|
|
|
|
response, err := client.ListObjectVersions(context.Background(), request)
|
|
if err != nil {
|
|
t.Fatalf("failed to list(First page) OCI bucket objects: %v", err)
|
|
}
|
|
|
|
request.Page = response.OpcNextPage
|
|
|
|
for request.Page != nil {
|
|
request.RequestMetadata.RetryPolicy = getDefaultRetryPolicy()
|
|
|
|
listResponse, err := client.ListObjectVersions(context.Background(), request)
|
|
if err != nil {
|
|
t.Fatalf("failed to list OCI bucket objects: %v", err)
|
|
}
|
|
response.Items = append(response.Items, listResponse.Items...)
|
|
request.Page = listResponse.OpcNextPage
|
|
}
|
|
|
|
var diagErr tfdiags.Diagnostics
|
|
|
|
for _, objectVersion := range response.Items {
|
|
|
|
deleteObjectVersionRequest := objectstorage.DeleteObjectRequest{
|
|
BucketName: common.String(bucketName),
|
|
NamespaceName: common.String(namespace),
|
|
ObjectName: objectVersion.Name,
|
|
VersionId: objectVersion.VersionId,
|
|
RequestMetadata: common.RequestMetadata{
|
|
RetryPolicy: getDefaultRetryPolicy(),
|
|
},
|
|
}
|
|
|
|
_, err := client.DeleteObject(context.Background(), deleteObjectVersionRequest)
|
|
if err != nil {
|
|
diagErr = diagErr.Append(err)
|
|
}
|
|
}
|
|
if diagErr != nil {
|
|
t.Fatalf("error while deleting object from bucket: %v", diagErr.Err())
|
|
}
|
|
|
|
req := objectstorage.DeleteBucketRequest{
|
|
NamespaceName: common.String(namespace),
|
|
BucketName: common.String(bucketName),
|
|
IfMatch: common.String(etag),
|
|
}
|
|
|
|
_, err = client.DeleteBucket(ctx, req)
|
|
if err != nil {
|
|
t.Fatalf("failed to delete OCI bucket: %v", err)
|
|
}
|
|
}
|
|
|
|
// verify that we are doing ACC tests or the oci backend tests specifically
|
|
func testACC(t *testing.T) {
|
|
skip := os.Getenv("TF_ACC") == "" && os.Getenv("TF_OCI_BACKEND_TEST") == ""
|
|
if skip {
|
|
t.Log("oci backend tests require setting TF_ACC or TF_OCI_BACKEND_TEST")
|
|
t.Skip()
|
|
}
|
|
}
|
|
|
|
func TestOCIBackendConfig_PrepareConfigValidation(t *testing.T) {
|
|
cases := map[string]struct {
|
|
config cty.Value
|
|
expectedDiags tfdiags.Diagnostics
|
|
mock func()
|
|
}{
|
|
"null bucket": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
BucketAttrName: cty.NullVal(cty.String),
|
|
NamespaceAttrName: cty.StringVal("test-namespace"),
|
|
KeyAttrName: cty.StringVal("test-key"),
|
|
}),
|
|
expectedDiags: tfdiags.Diagnostics{
|
|
requiredAttributeErrDiag(cty.GetAttrPath(BucketAttrName)),
|
|
},
|
|
},
|
|
"empty bucket": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
BucketAttrName: cty.StringVal(""),
|
|
NamespaceAttrName: cty.StringVal("test-namespace"),
|
|
KeyAttrName: cty.StringVal("test-key"),
|
|
}),
|
|
expectedDiags: tfdiags.Diagnostics{
|
|
requiredAttributeErrDiag(cty.GetAttrPath(BucketAttrName)),
|
|
},
|
|
},
|
|
"null namespace": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
BucketAttrName: cty.StringVal("test-bucket"),
|
|
NamespaceAttrName: cty.NullVal(cty.String),
|
|
KeyAttrName: cty.StringVal("test-key"),
|
|
}),
|
|
expectedDiags: tfdiags.Diagnostics{
|
|
requiredAttributeErrDiag(cty.GetAttrPath("namespace")),
|
|
},
|
|
},
|
|
"empty namespace": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
BucketAttrName: cty.StringVal("test-bucket"),
|
|
NamespaceAttrName: cty.StringVal(""),
|
|
KeyAttrName: cty.StringVal("test-key"),
|
|
}),
|
|
expectedDiags: tfdiags.Diagnostics{
|
|
requiredAttributeErrDiag(cty.GetAttrPath("namespace")),
|
|
},
|
|
},
|
|
"key with leading slash": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
BucketAttrName: cty.StringVal("test-bucket"),
|
|
NamespaceAttrName: cty.StringVal("test-namespace"),
|
|
KeyAttrName: cty.StringVal("/leading-slash"),
|
|
}),
|
|
expectedDiags: tfdiags.Diagnostics{
|
|
attributeErrDiag(
|
|
"Invalid Value",
|
|
`The value must not start or end with "/" and also not contain consecutive "/"`,
|
|
cty.GetAttrPath(KeyAttrName),
|
|
),
|
|
},
|
|
},
|
|
"key with trailing slash": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
BucketAttrName: cty.StringVal("test-bucket"),
|
|
NamespaceAttrName: cty.StringVal("test-namespace"),
|
|
KeyAttrName: cty.StringVal("trailing-slash/"),
|
|
}),
|
|
expectedDiags: tfdiags.Diagnostics{
|
|
attributeErrDiag(
|
|
"Invalid Value",
|
|
`The value must not start or end with "/" and also not contain consecutive "/"`,
|
|
cty.GetAttrPath(KeyAttrName),
|
|
),
|
|
},
|
|
},
|
|
"key with double slash": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
BucketAttrName: cty.StringVal("test-bucket"),
|
|
NamespaceAttrName: cty.StringVal("test-namespace"),
|
|
KeyAttrName: cty.StringVal("test/with/double//slash"),
|
|
}),
|
|
expectedDiags: tfdiags.Diagnostics{
|
|
attributeErrDiag(
|
|
"Invalid Value",
|
|
`The value must not start or end with "/" and also not contain consecutive "/"`,
|
|
cty.GetAttrPath(KeyAttrName),
|
|
),
|
|
},
|
|
},
|
|
"workspace_key_prefix with leading slash": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
BucketAttrName: cty.StringVal("test-bucket"),
|
|
NamespaceAttrName: cty.StringVal("test-namespace"),
|
|
KeyAttrName: cty.StringVal("test-key"),
|
|
WorkspaceKeyPrefixAttrName: cty.StringVal("/env"),
|
|
}),
|
|
expectedDiags: tfdiags.Diagnostics{
|
|
attributeErrDiag(
|
|
"Invalid Value",
|
|
`The value must not start with "/" and also not contain consecutive "/"`,
|
|
cty.GetAttrPath(WorkspaceKeyPrefixAttrName),
|
|
),
|
|
},
|
|
},
|
|
"encryption key conflict": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
BucketAttrName: cty.StringVal("test-bucket"),
|
|
NamespaceAttrName: cty.StringVal("test-namespace"),
|
|
KeyAttrName: cty.StringVal("test-key"),
|
|
KmsKeyIdAttrName: cty.StringVal("ocid1.key.oc1..example"),
|
|
SseCustomerKeyAttrName: cty.StringVal("base64-encoded-key"),
|
|
SseCustomerKeySHA256AttrName: cty.StringVal("base64-encoded-key md5"),
|
|
}),
|
|
expectedDiags: tfdiags.Diagnostics{
|
|
attributeErrDiag(
|
|
"Invalid Attribute Combination",
|
|
`Only one of kms_key_id, sse_customer_key can be set.`,
|
|
cty.GetAttrPath(KmsKeyIdAttrName),
|
|
),
|
|
},
|
|
},
|
|
"Invalid encryption key combination": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
BucketAttrName: cty.StringVal("test-bucket"),
|
|
NamespaceAttrName: cty.StringVal("test-namespace"),
|
|
KeyAttrName: cty.StringVal("test-key"),
|
|
SseCustomerKeyAttrName: cty.StringVal("base64-encoded-key"),
|
|
}),
|
|
expectedDiags: tfdiags.Diagnostics{
|
|
attributeErrDiag(
|
|
"Invalid Attribute Combination",
|
|
` sse_customer_key and its SHA both required.`,
|
|
cty.GetAttrPath(SseCustomerKeySHA256AttrName)),
|
|
},
|
|
},
|
|
"private_key and private_key_path conflict": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
BucketAttrName: cty.StringVal("test-bucket"),
|
|
NamespaceAttrName: cty.StringVal("test-namespace"),
|
|
KeyAttrName: cty.StringVal("test-key"),
|
|
PrivateKeyAttrName: cty.StringVal("-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"),
|
|
PrivateKeyPathAttrName: cty.StringVal("/path/to/key.pem"),
|
|
}),
|
|
expectedDiags: tfdiags.Diagnostics{
|
|
attributeErrDiag(
|
|
"Invalid Attribute Combination",
|
|
`Only one of private_key, private_key_path can be set.`,
|
|
cty.GetAttrPath(PrivateKeyPathAttrName),
|
|
),
|
|
},
|
|
},
|
|
"invalid auth method": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
BucketAttrName: cty.StringVal("test-bucket"),
|
|
NamespaceAttrName: cty.StringVal("test-namespace"),
|
|
KeyAttrName: cty.StringVal("test-key"),
|
|
AuthAttrName: cty.StringVal("invalid-auth"),
|
|
}),
|
|
expectedDiags: tfdiags.Diagnostics{
|
|
tfdiags.AttributeValue(tfdiags.Error,
|
|
"Invalid authentication method",
|
|
fmt.Sprintf("auth must be one of '%s' or '%s' or '%s' or '%s' or '%s' or '%s'", AuthAPIKeySetting, AuthInstancePrincipalSetting, AuthInstancePrincipalWithCertsSetting, AuthSecurityToken, ResourcePrincipal, AuthOKEWorkloadIdentity), cty.GetAttrPath(AuthAttrName),
|
|
),
|
|
},
|
|
},
|
|
"missing region for InstancePrinciple auth": {
|
|
config: cty.ObjectVal(map[string]cty.Value{
|
|
BucketAttrName: cty.StringVal("test-bucket"),
|
|
NamespaceAttrName: cty.StringVal("test-namespace"),
|
|
KeyAttrName: cty.StringVal("test-key"),
|
|
AuthAttrName: cty.StringVal(AuthInstancePrincipalSetting),
|
|
}),
|
|
expectedDiags: tfdiags.Diagnostics{
|
|
tfdiags.AttributeValue(tfdiags.Error,
|
|
"Missing region attribute required",
|
|
fmt.Sprintf("The attribute %q is required by the backend for %s authentication.\n\n", RegionAttrName, AuthInstancePrincipalSetting), cty.GetAttrPath(RegionAttrName),
|
|
),
|
|
},
|
|
mock: func() {
|
|
os.Setenv("OCI_region", "")
|
|
os.Setenv("region", "")
|
|
},
|
|
},
|
|
}
|
|
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
|
|
b := New()
|
|
if tc.mock != nil {
|
|
tc.mock()
|
|
}
|
|
_, valDiags := b.PrepareConfig(populateSchema(t, b.ConfigSchema(), tc.config))
|
|
|
|
if diff := cmp.Diff(valDiags, tc.expectedDiags, tfdiags.DiagnosticComparer); diff != "" {
|
|
t.Errorf("unexpected diagnostics difference: %s", diff)
|
|
}
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
func populateSchema(t *testing.T, schema *configschema.Block, value cty.Value) cty.Value {
|
|
ty := schema.ImpliedType()
|
|
var path cty.Path
|
|
val, err := unmarshal(value, ty, path)
|
|
if err != nil {
|
|
t.Fatalf("populating schema: %s", err)
|
|
}
|
|
return val
|
|
}
|
|
|
|
func unmarshal(value cty.Value, ty cty.Type, path cty.Path) (cty.Value, error) {
|
|
switch {
|
|
case ty.IsPrimitiveType():
|
|
return value, nil
|
|
// case ty.IsListType():
|
|
// return unmarshalList(value, ty.ElementType(), path)
|
|
case ty.IsSetType():
|
|
return unmarshalSet(value, ty.ElementType(), path)
|
|
case ty.IsMapType():
|
|
return unmarshalMap(value, ty.ElementType(), path)
|
|
// case ty.IsTupleType():
|
|
// return unmarshalTuple(value, ty.TupleElementTypes(), path)
|
|
case ty.IsObjectType():
|
|
return unmarshalObject(value, ty.AttributeTypes(), path)
|
|
default:
|
|
return cty.NilVal, path.NewErrorf("unsupported type %s", ty.FriendlyName())
|
|
}
|
|
}
|
|
|
|
func unmarshalSet(dec cty.Value, ety cty.Type, path cty.Path) (cty.Value, error) {
|
|
if dec.IsNull() {
|
|
return dec, nil
|
|
}
|
|
|
|
length := dec.LengthInt()
|
|
|
|
if length == 0 {
|
|
return cty.SetValEmpty(ety), nil
|
|
}
|
|
|
|
vals := make([]cty.Value, 0, length)
|
|
dec.ForEachElement(func(key, val cty.Value) (stop bool) {
|
|
vals = append(vals, val)
|
|
return
|
|
})
|
|
|
|
return cty.SetVal(vals), nil
|
|
}
|
|
func unmarshalMap(dec cty.Value, ety cty.Type, path cty.Path) (cty.Value, error) {
|
|
if dec.IsNull() {
|
|
return dec, nil
|
|
}
|
|
|
|
length := dec.LengthInt()
|
|
|
|
if length == 0 {
|
|
return cty.MapValEmpty(ety), nil
|
|
}
|
|
|
|
vals := make(map[string]cty.Value, length)
|
|
dec.ForEachElement(func(key, val cty.Value) (stop bool) {
|
|
vals[key.AsString()] = val
|
|
return
|
|
})
|
|
|
|
return cty.MapVal(vals), nil
|
|
}
|
|
|
|
func unmarshalObject(dec cty.Value, atys map[string]cty.Type, path cty.Path) (cty.Value, error) {
|
|
if dec.IsNull() {
|
|
return dec, nil
|
|
}
|
|
valueTy := dec.Type()
|
|
|
|
vals := make(map[string]cty.Value, len(atys))
|
|
path = append(path, nil)
|
|
for key, aty := range atys {
|
|
path[len(path)-1] = cty.IndexStep{
|
|
Key: cty.StringVal(key),
|
|
}
|
|
|
|
if !valueTy.HasAttribute(key) {
|
|
vals[key] = cty.NullVal(aty)
|
|
} else {
|
|
val, err := unmarshal(dec.GetAttr(key), aty, path)
|
|
if err != nil {
|
|
return cty.DynamicVal, err
|
|
}
|
|
vals[key] = val
|
|
}
|
|
}
|
|
|
|
return cty.ObjectVal(vals), nil
|
|
}
|