blob: a67434710c7f4d4cb154d548ef94d90811b2717f [file] [log] [blame] [edit]
// Copyright (c) HashiCorp, Inc.
// 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
}