| // Copyright IBM Corp. 2014, 2026 | |
| // SPDX-License-Identifier: BUSL-1.1 | |
| package oci | |
| import ( | |
| "fmt" | |
| "path" | |
| "strings" | |
| "github.com/hashicorp/terraform/internal/backend" | |
| "github.com/hashicorp/terraform/internal/backend/backendbase" | |
| "github.com/hashicorp/terraform/internal/configs/configschema" | |
| "github.com/hashicorp/terraform/internal/tfdiags" | |
| "github.com/zclconf/go-cty/cty" | |
| ) | |
| var ( | |
| lockFileSuffix = ".lock" | |
| ) | |
| func New() backend.Backend { | |
| return &Backend{} | |
| } | |
| // New creates a new backend for oci remote state. | |
| func (b *Backend) ConfigSchema() *configschema.Block { | |
| return &configschema.Block{ | |
| Attributes: map[string]*configschema.Attribute{ | |
| KeyAttrName: { | |
| Type: cty.String, | |
| Optional: true, | |
| Description: "The name of the state file stored on the remote backend.", | |
| }, | |
| BucketAttrName: { | |
| Type: cty.String, | |
| Required: true, | |
| Description: "The name of the OCI Object Storage bucket.", | |
| }, | |
| NamespaceAttrName: { | |
| Type: cty.String, | |
| Required: true, | |
| Description: "The namespace of the OCI Object Storage.", | |
| }, | |
| RegionAttrName: { | |
| Type: cty.String, | |
| Optional: true, | |
| Description: "OCI region where the bucket is located.", | |
| }, | |
| TenancyOcidAttrName: { | |
| Type: cty.String, | |
| Optional: true, | |
| Description: "The OCID of the tenancy.", | |
| }, | |
| UserOcidAttrName: { | |
| Type: cty.String, | |
| Optional: true, | |
| Description: "The OCID of the user.", | |
| }, | |
| FingerprintAttrName: { | |
| Type: cty.String, | |
| Optional: true, | |
| Description: "The fingerprint of the user's API key.", | |
| }, | |
| PrivateKeyAttrName: { | |
| Type: cty.String, | |
| Sensitive: true, | |
| Optional: true, | |
| Description: "The private key for API authentication.", | |
| }, | |
| PrivateKeyPathAttrName: { | |
| Type: cty.String, | |
| Optional: true, | |
| Description: "Path to the private key file.", | |
| }, | |
| PrivateKeyPasswordAttrName: { | |
| Type: cty.String, | |
| Sensitive: true, | |
| Optional: true, | |
| Description: "Passphrase for the private key, if required.", | |
| }, | |
| AuthAttrName: { | |
| Type: cty.String, | |
| Optional: true, | |
| Description: "Authentication method (API key, Instance Principal, Resource Principal, etc.).", | |
| }, | |
| ConfigFileProfileAttrName: { | |
| Type: cty.String, | |
| Optional: true, | |
| Description: "Profile name from the OCI config file.", | |
| }, | |
| WorkspaceKeyPrefixAttrName: { | |
| Type: cty.String, | |
| Optional: true, | |
| Description: "The prefix applied to the non-default state path inside the bucket.", | |
| }, | |
| KmsKeyIdAttrName: { | |
| Type: cty.String, | |
| Optional: true, | |
| Description: "The OCID of a master encryption key used to call the Key Management service to generate a data encryption key or to encrypt or decrypt a data encryption key.", | |
| }, | |
| SseCustomerKeyAttrName: { | |
| Type: cty.String, | |
| Optional: true, | |
| Description: "The optional header that specifies the base64-encoded 256-bit encryption key to use to encrypt or decrypt the data.", | |
| Sensitive: true, | |
| }, | |
| SseCustomerKeySHA256AttrName: { | |
| Type: cty.String, | |
| Optional: true, | |
| Description: "The optional header that specifies the base64-encoded SHA256 hash of the encryption key. This value is used to check the integrity of the encryption key.", | |
| }, | |
| SseCustomerAlgorithmAttrName: { | |
| Type: cty.String, | |
| Optional: true, | |
| Description: "The optional header that specifies \"AES256\" as the encryption algorithm.", | |
| }, | |
| }, | |
| } | |
| } | |
| type Backend struct { | |
| configProvider ociAuthConfigProvider | |
| bucket string | |
| key string | |
| namespace string | |
| workspaceKeyPrefix string | |
| kmsKeyID string | |
| SSECustomerKey string | |
| SSECustomerKeySHA256 string | |
| SSECustomerAlgorithm string | |
| client *RemoteClient | |
| } | |
| func (b *Backend) PrepareConfig(obj cty.Value) (cty.Value, tfdiags.Diagnostics) { | |
| var diags tfdiags.Diagnostics | |
| if obj.IsNull() { | |
| diags.Append(tfdiags.AttributeValue(tfdiags.Error, "Invalid Configuration", "Received null configuration for OCI backend.", cty.GetAttrPath("."))) | |
| return obj, diags | |
| } | |
| if bucketVal := obj.GetAttr(BucketAttrName); bucketVal.IsNull() || bucketVal.AsString() == "" { | |
| diags = diags.Append(requiredAttributeErrDiag(cty.GetAttrPath(BucketAttrName))) | |
| } else { | |
| validateStringBucketName(bucketVal.AsString(), cty.GetAttrPath(BucketAttrName), &diags) | |
| } | |
| if namespaceVal := obj.GetAttr(NamespaceAttrName); namespaceVal.IsNull() || namespaceVal.AsString() == "" { | |
| diags = diags.Append(requiredAttributeErrDiag(cty.GetAttrPath(NamespaceAttrName))) | |
| } | |
| if keyVal, ok := getBackendAttrWithDefault(obj, KeyAttrName, defaultKeyValue); ok { | |
| validateStringObjectPath(keyVal.AsString(), cty.GetAttrPath(KeyAttrName), &diags) | |
| } | |
| if workspaceKeyPrefixVal, ok := getBackendAttrWithDefault(obj, WorkspaceKeyPrefixAttrName, defaultWorkspaceEnvPrefix); ok { | |
| validateStringWorkspacePrefix(workspaceKeyPrefixVal.AsString(), cty.GetAttrPath(WorkspaceKeyPrefixAttrName), &diags) | |
| } | |
| authVal, ok := getBackendAttr(obj, AuthAttrName) | |
| if ok && len(authVal.AsString()) > 0 { | |
| switch strings.ToLower(authVal.AsString()) { | |
| case strings.ToLower(AuthAPIKeySetting): | |
| //Nothing to do | |
| return obj, diags | |
| case strings.ToLower(AuthInstancePrincipalSetting), strings.ToLower(AuthInstancePrincipalWithCertsSetting), strings.ToLower(ResourcePrincipal), strings.ToLower(AuthSecurityToken): | |
| region, _ := getBackendAttr(obj, RegionAttrName) | |
| if region.AsString() == "" { | |
| diags = diags.Append(tfdiags.AttributeValue(tfdiags.Error, | |
| "Missing region attribute required", | |
| fmt.Sprintf("The attribute %q is required by the backend for %s authentication.\n\n", RegionAttrName, authVal.AsString()), cty.GetAttrPath(RegionAttrName), | |
| )) | |
| } | |
| if strings.ToLower(authVal.AsString()) == strings.ToLower(AuthSecurityToken) { | |
| profileVal, _ := getBackendAttr(obj, ConfigFileProfileAttrName) | |
| if profileVal.IsNull() || profileVal.AsString() == "" { | |
| diags = diags.Append(tfdiags.AttributeValue(tfdiags.Error, | |
| "Missing config_file_profile attribute required", | |
| fmt.Sprintf("The attribute %q is required by the backend for %s authentication.\n\n", ConfigFileProfileAttrName, authVal.AsString()), cty.GetAttrPath(ConfigFileProfileAttrName), | |
| )) | |
| } | |
| } | |
| default: | |
| diags = diags.Append(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), | |
| )) | |
| } | |
| } | |
| customerKey, _ := getBackendAttr(obj, SseCustomerKeyAttrName) | |
| customerKeySHA, _ := getBackendAttr(obj, SseCustomerKeySHA256AttrName) | |
| kmsKeyId, _ := getBackendAttr(obj, KmsKeyIdAttrName) | |
| privateKey, _ := getBackendAttr(obj, PrivateKeyAttrName) | |
| privateKeyPath, _ := getBackendAttr(obj, PrivateKeyPathAttrName) | |
| if (!customerKey.IsNull() && len(customerKey.AsString()) > 0) && (customerKeySHA.IsNull() || len(customerKeySHA.AsString()) == 0) { | |
| diags = diags.Append(attributeErrDiag( | |
| "Invalid Attribute Combination", | |
| ` sse_customer_key and its SHA both required.`, | |
| cty.GetAttrPath(SseCustomerKeySHA256AttrName))) | |
| } | |
| if !customerKey.IsNull() && len(customerKey.AsString()) > 0 && !kmsKeyId.IsNull() && len(kmsKeyId.AsString()) > 0 { | |
| diags = diags.Append(attributeErrDiag( | |
| "Invalid Attribute Combination", | |
| `Only one of kms_key_id, sse_customer_key can be set.`, | |
| cty.GetAttrPath(KmsKeyIdAttrName), | |
| )) | |
| } | |
| if !privateKey.IsNull() && len(privateKey.AsString()) > 0 && !privateKeyPath.IsNull() && len(privateKeyPath.AsString()) > 0 { | |
| diags = diags.Append(attributeErrDiag( | |
| "Invalid Attribute Combination", | |
| `Only one of private_key, private_key_path can be set.`, | |
| cty.GetAttrPath(PrivateKeyPathAttrName), | |
| )) | |
| } | |
| return obj, diags | |
| } | |
| func (b *Backend) Configure(obj cty.Value) tfdiags.Diagnostics { | |
| var diags tfdiags.Diagnostics | |
| if bucketVal, ok := getBackendAttr(obj, BucketAttrName); ok { | |
| b.bucket = bucketVal.AsString() | |
| } | |
| if namespaceVal, ok := getBackendAttr(obj, NamespaceAttrName); ok { | |
| b.namespace = namespaceVal.AsString() | |
| } | |
| if keyVal, ok := getBackendAttrWithDefault(obj, KeyAttrName, defaultKeyValue); ok { | |
| b.key = keyVal.AsString() | |
| } | |
| if workspaceKeyPrefixVal, ok := getBackendAttrWithDefault(obj, WorkspaceKeyPrefixAttrName, defaultWorkspaceEnvPrefix); ok { | |
| b.workspaceKeyPrefix = workspaceKeyPrefixVal.AsString() | |
| } | |
| if kmsKeyIdVal, ok := getBackendAttr(obj, KmsKeyIdAttrName); ok { | |
| b.kmsKeyID = kmsKeyIdVal.AsString() | |
| } | |
| if customerKeyVal, ok := getBackendAttr(obj, SseCustomerKeyAttrName); ok { | |
| b.SSECustomerKey = customerKeyVal.AsString() | |
| } | |
| if customerKeySHA256Val, ok := getBackendAttr(obj, SseCustomerKeySHA256AttrName); ok { | |
| b.SSECustomerKeySHA256 = customerKeySHA256Val.AsString() | |
| } | |
| if customerAlgorithmVal, ok := getBackendAttrWithDefault(obj, SseCustomerAlgorithmAttrName, DefaultAlgorithm); ok { | |
| b.SSECustomerAlgorithm = customerAlgorithmVal.AsString() | |
| } | |
| b.configProvider = newOciAuthConfigProvider(obj) | |
| err := b.configureRemoteClient() | |
| if err != nil { | |
| diags = append(diags, backendbase.ErrorAsDiagnostics(err)[0]) | |
| } | |
| return diags | |
| } | |
| func (b *Backend) path(name string) string { | |
| if name == backend.DefaultStateName { | |
| return b.key | |
| } | |
| return path.Join(b.workspaceKeyPrefix, name, b.key) | |
| } | |
| // getLockFilePath returns the path to the lock file for the given Terraform state. | |
| // For `default.tfstate`, the lock file is stored at `default.tfstate.tflock`. | |
| func (b *Backend) getLockFilePath(name string) string { | |
| return b.path(name) + lockFileSuffix | |
| } |