-
Notifications
You must be signed in to change notification settings - Fork 75
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add MinIO Bucket Retention Resource (#595)
- Loading branch information
Showing
7 changed files
with
561 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
--- | ||
# generated by https://github.com/hashicorp/terraform-plugin-docs | ||
page_title: "minio_s3_bucket_retention Resource - terraform-provider-minio" | ||
subcategory: "" | ||
description: |- | ||
Manages object lock retention settings for a MinIO bucket. Object locking enforces Write-Once Read-Many (WORM) immutability to protect versioned objects from deletion. | ||
--- | ||
|
||
# minio_s3_bucket_retention (Resource) | ||
|
||
Manages object lock retention settings for a MinIO bucket. Object locking enforces Write-Once Read-Many (WORM) immutability to protect versioned objects from deletion. This resource provides compliance with SEC17a-4(f), FINRA 4511(C), and CFTC 1.31(c)-(d) requirements. | ||
|
||
-> **Note** Object locking can only be enabled during bucket creation and requires versioning. You cannot enable object locking on an existing bucket. | ||
|
||
## Example Usage | ||
|
||
### Basic Retention Configuration | ||
|
||
```terraform | ||
# First, create a bucket with object locking enabled | ||
resource "minio_s3_bucket" "example" { | ||
bucket = "my-bucket" | ||
force_destroy = true | ||
object_locking = true | ||
} | ||
# Configure retention with COMPLIANCE mode | ||
resource "minio_s3_bucket_retention" "example" { | ||
bucket = minio_s3_bucket.example.bucket | ||
mode = "COMPLIANCE" | ||
unit = "DAYS" | ||
validity_period = 30 | ||
} | ||
``` | ||
|
||
### Governance Mode Configuration | ||
|
||
```terraform | ||
resource "minio_s3_bucket_retention" "governance_example" { | ||
bucket = minio_s3_bucket.example.bucket | ||
mode = "GOVERNANCE" | ||
unit = "YEARS" | ||
validity_period = 1 | ||
} | ||
``` | ||
|
||
## Interaction with Lifecycle Rules | ||
|
||
If a bucket has lifecycle rules configured, the retention settings will take precedence. Objects cannot be deleted by lifecycle rules until their retention period expires. The provider will issue a warning if lifecycle rules are detected during retention configuration. | ||
|
||
## Schema | ||
|
||
### Required | ||
|
||
- `bucket` (String) Name of the bucket to configure object locking. The bucket must be created with object locking enabled. | ||
- `mode` (String) Retention mode for the bucket. Valid values are: | ||
- `GOVERNANCE`: Prevents object modification by non-privileged users. Users with s3:BypassGovernanceRetention permission can modify objects. | ||
- `COMPLIANCE`: Prevents any object modification by all users, including the root user, until retention period expires. | ||
- `unit` (String) Time unit for the validity period. Valid values are `DAYS` or `YEARS`. | ||
- `validity_period` (Number) Duration for which objects should be retained under WORM lock, in the specified unit. Must be a positive integer. | ||
|
||
### Read-Only | ||
|
||
- `id` (String) The ID of this resource. | ||
|
||
## Import | ||
|
||
Bucket retention configuration can be imported using the bucket name: | ||
|
||
```shell | ||
$ terraform import minio_s3_bucket_retention.example my-bucket | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
terraform { | ||
required_providers { | ||
minio = { | ||
source = "aminueza/minio" | ||
version = ">= 3.0.0" | ||
} | ||
} | ||
} | ||
|
||
provider "minio" { | ||
minio_server = var.minio_server | ||
minio_region = var.minio_region | ||
minio_user = var.minio_user | ||
minio_password = var.minio_password | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
resource "minio_s3_bucket" "test_bucket" { | ||
bucket = "test-retention-bucket" | ||
force_destroy = true | ||
object_locking = false | ||
} | ||
|
||
# Add basic retention configuration | ||
resource "minio_s3_bucket_retention" "test_retention" { | ||
bucket = minio_s3_bucket.test_bucket.bucket | ||
mode = "GOVERNANCE" | ||
unit = "YEARS" | ||
validity_period = 1 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
variable "minio_region" { | ||
description = "Default MINIO region" | ||
default = "us-east-1" | ||
} | ||
|
||
variable "minio_server" { | ||
description = "Default MINIO host and port" | ||
default = "localhost:9000" | ||
} | ||
|
||
variable "minio_user" { | ||
description = "MINIO user" | ||
default = "minio" | ||
} | ||
|
||
variable "minio_password" { | ||
description = "MINIO password" | ||
default = "minio123" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,247 @@ | ||
package minio | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"strings" | ||
|
||
"github.com/hashicorp/go-cty/cty" | ||
"github.com/hashicorp/terraform-plugin-sdk/v2/diag" | ||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" | ||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" | ||
"github.com/minio/minio-go/v7" | ||
) | ||
|
||
var ValidityUnits = map[minio.ValidityUnit]bool{ | ||
minio.Days: true, | ||
minio.Years: true, | ||
} | ||
|
||
func resourceMinioBucketRetention() *schema.Resource { | ||
return &schema.Resource{ | ||
CreateContext: minioCreateRetention, | ||
ReadContext: minioReadRetention, | ||
UpdateContext: minioUpdateRetention, | ||
DeleteContext: minioDeleteRetention, | ||
Importer: &schema.ResourceImporter{ | ||
StateContext: schema.ImportStatePassthroughContext, | ||
}, | ||
Description: `Manages object lock retention settings for a MinIO bucket. Object locking enforces Write-Once Read-Many (WORM) immutability to protect versioned objects from deletion. | ||
Note: Object locking can only be enabled during bucket creation and requires versioning. You cannot enable object locking on an existing bucket. | ||
This resource provides compliance with SEC17a-4(f), FINRA 4511(C), and CFTC 1.31(c)-(d) requirements.`, | ||
|
||
Schema: map[string]*schema.Schema{ | ||
"bucket": { | ||
Type: schema.TypeString, | ||
Required: true, | ||
ForceNew: true, | ||
ValidateDiagFunc: validation.ToDiagFunc(validation.StringLenBetween(0, 63)), | ||
Description: "Name of the bucket to configure object locking. The bucket must be created with object locking enabled.", | ||
}, | ||
"mode": { | ||
Type: schema.TypeString, | ||
Required: true, | ||
ValidateDiagFunc: validateRetentionMode, | ||
Description: `Retention mode for the bucket. Valid values are: | ||
- GOVERNANCE: Prevents object modification by non-privileged users. Users with s3:BypassGovernanceRetention permission can modify objects. | ||
- COMPLIANCE: Prevents any object modification by all users, including the root user, until retention period expires.`, | ||
}, | ||
"unit": { | ||
Type: schema.TypeString, | ||
Required: true, | ||
ValidateDiagFunc: validateRetentionUnit, | ||
Description: "Time unit for the validity period. Valid values are DAYS or YEARS.", | ||
}, | ||
"validity_period": { | ||
Type: schema.TypeInt, | ||
Required: true, | ||
ValidateDiagFunc: validateValidityPeriod, | ||
Description: "Duration for which objects should be retained under WORM lock, in the specified unit. Must be a positive integer.", | ||
}, | ||
}, | ||
} | ||
} | ||
|
||
func validateRetentionMode(v interface{}, p cty.Path) diag.Diagnostics { | ||
mode := minio.RetentionMode(v.(string)) | ||
if !mode.IsValid() { | ||
return diag.Errorf("retention mode must be either GOVERNANCE or COMPLIANCE, got: %s", mode) | ||
} | ||
return nil | ||
} | ||
|
||
func validateRetentionUnit(v interface{}, p cty.Path) diag.Diagnostics { | ||
unit := minio.ValidityUnit(v.(string)) | ||
if !ValidityUnits[unit] { | ||
return diag.Errorf("validity unit must be either DAYS or YEARS, got: %s", unit) | ||
} | ||
return nil | ||
} | ||
func validateValidityPeriod(v interface{}, p cty.Path) diag.Diagnostics { | ||
value := v.(int) | ||
if value < 1 { | ||
return diag.Errorf("validity period must be positive, got: %d", value) | ||
} | ||
return nil | ||
} | ||
|
||
func validateBucketObjectLock(ctx context.Context, client *minio.Client, bucket string) error { | ||
// Check if bucket exists | ||
exists, err := client.BucketExists(ctx, bucket) | ||
if err != nil { | ||
return fmt.Errorf("error checking bucket existence: %w", err) | ||
} | ||
if !exists { | ||
return fmt.Errorf("bucket %s does not exist", bucket) | ||
} | ||
|
||
// Check if versioning is enabled (required for object locking) | ||
versioning, err := client.GetBucketVersioning(ctx, bucket) | ||
if err != nil { | ||
return fmt.Errorf("error checking bucket versioning: %w", err) | ||
} | ||
|
||
if !versioning.Enabled() { // Use the method, not the field | ||
return fmt.Errorf("bucket %s does not have versioning enabled. Object locking requires versioning", bucket) | ||
} | ||
|
||
// Check if object lock is enabled | ||
objectLock, _, _, _, err := client.GetObjectLockConfig(ctx, bucket) | ||
if err != nil { | ||
if strings.Contains(err.Error(), "Object Lock configuration does not exist") { | ||
return fmt.Errorf("bucket %s does not have object lock enabled. Object lock must be enabled when creating the bucket", bucket) | ||
} | ||
return fmt.Errorf("error checking object lock configuration: %w", err) | ||
} | ||
|
||
if objectLock != "Enabled" { | ||
return fmt.Errorf("bucket %s does not have object lock enabled. Object lock must be enabled when creating the bucket", bucket) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func minioCreateRetention(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { | ||
client := meta.(*S3MinioClient).S3Client | ||
bucket := d.Get("bucket").(string) | ||
var diags diag.Diagnostics | ||
|
||
// Validate bucket object lock status before proceeding | ||
if err := validateBucketObjectLock(ctx, client, bucket); err != nil { | ||
return diag.FromErr(err) | ||
} | ||
|
||
if hasLifecycleRules(ctx, client, bucket) { | ||
diags = append(diags, diag.Diagnostic{ | ||
Severity: diag.Warning, | ||
Summary: "Bucket has lifecycle rules configured", | ||
Detail: "This bucket has lifecycle management rules. Note that object expiration respects retention " + | ||
"settings. Objects cannot be deleted by lifecycle rules until their retention period expires.", | ||
}) | ||
} | ||
|
||
mode := minio.RetentionMode(d.Get("mode").(string)) | ||
unit := minio.ValidityUnit(d.Get("unit").(string)) | ||
validity := uint(d.Get("validity_period").(int)) | ||
|
||
err := client.SetBucketObjectLockConfig(ctx, bucket, &mode, &validity, &unit) | ||
if err != nil { | ||
return diag.FromErr(fmt.Errorf("error setting bucket object lock config: %w", err)) | ||
} | ||
|
||
d.SetId(bucket) | ||
|
||
readDiags := minioReadRetention(ctx, d, meta) | ||
diags = append(diags, readDiags...) | ||
return diags | ||
} | ||
|
||
func minioReadRetention(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { | ||
client := meta.(*S3MinioClient).S3Client | ||
|
||
// First check if bucket still exists | ||
exists, err := client.BucketExists(ctx, d.Id()) | ||
if err != nil { | ||
return diag.FromErr(fmt.Errorf("error checking bucket existence: %w", err)) | ||
} | ||
if !exists { | ||
d.SetId("") | ||
return nil | ||
} | ||
|
||
mode, validity, unit, err := client.GetBucketObjectLockConfig(ctx, d.Id()) | ||
if err != nil { | ||
// Check if the error indicates the retention config is gone | ||
if strings.Contains(err.Error(), "Object Lock configuration does not exist") { | ||
d.SetId("") | ||
return nil | ||
} | ||
return diag.FromErr(fmt.Errorf("error reading bucket retention config: %w", err)) | ||
} | ||
|
||
// If any of the required fields are nil, the retention config is effectively gone | ||
if mode == nil || validity == nil || unit == nil { | ||
d.SetId("") | ||
return nil | ||
} | ||
|
||
if err := d.Set("bucket", d.Id()); err != nil { | ||
return diag.FromErr(err) | ||
} | ||
|
||
if err := d.Set("mode", mode.String()); err != nil { | ||
return diag.FromErr(err) | ||
} | ||
|
||
if err := d.Set("validity_period", *validity); err != nil { | ||
return diag.FromErr(err) | ||
} | ||
|
||
if err := d.Set("unit", unit.String()); err != nil { | ||
return diag.FromErr(err) | ||
} | ||
|
||
return nil | ||
} | ||
func minioUpdateRetention(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { | ||
client := meta.(*S3MinioClient).S3Client | ||
bucket := d.Id() | ||
|
||
// Validate bucket object lock status before proceeding | ||
if err := validateBucketObjectLock(ctx, client, bucket); err != nil { | ||
return diag.FromErr(err) | ||
} | ||
|
||
if d.HasChanges("mode", "unit", "validity_period") { | ||
mode := minio.RetentionMode(d.Get("mode").(string)) | ||
unit := minio.ValidityUnit(d.Get("unit").(string)) | ||
validity := uint(d.Get("validity_period").(int)) | ||
|
||
err := client.SetBucketObjectLockConfig(ctx, bucket, &mode, &validity, &unit) | ||
if err != nil { | ||
return diag.FromErr(fmt.Errorf("error updating bucket object lock config: %w", err)) | ||
} | ||
} | ||
|
||
return minioReadRetention(ctx, d, meta) | ||
} | ||
|
||
func minioDeleteRetention(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { | ||
client := meta.(*S3MinioClient).S3Client | ||
|
||
// To clear object lock config, we pass nil for all optional parameters | ||
err := client.SetBucketObjectLockConfig(ctx, d.Id(), nil, nil, nil) | ||
if err != nil { | ||
return diag.FromErr(fmt.Errorf("error clearing bucket object lock config: %v", err)) | ||
} | ||
|
||
d.SetId("") | ||
return nil | ||
} | ||
|
||
func hasLifecycleRules(ctx context.Context, client *minio.Client, bucket string) bool { | ||
_, err := client.GetBucketLifecycle(ctx, bucket) | ||
return err == nil | ||
} |
Oops, something went wrong.