Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add MinIO Bucket Retention Resource #595

Merged
merged 5 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions docs/resources/s3_bucket_retention.md
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
```
16 changes: 16 additions & 0 deletions examples/resources/minio_s3_bucket_retention/main.tf
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
}

13 changes: 13 additions & 0 deletions examples/resources/minio_s3_bucket_retention/resource.tf
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
}
19 changes: 19 additions & 0 deletions examples/resources/minio_s3_bucket_retention/variables.tf
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"
}
1 change: 1 addition & 0 deletions minio/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ func newProvider(envvarPrefixed ...string) *schema.Provider {
"minio_s3_bucket_policy": resourceMinioBucketPolicy(),
"minio_s3_bucket_versioning": resourceMinioBucketVersioning(),
"minio_s3_bucket_replication": resourceMinioBucketReplication(),
"minio_s3_bucket_retention": resourceMinioBucketRetention(),
"minio_s3_bucket_notification": resourceMinioBucketNotification(),
"minio_s3_bucket_server_side_encryption": resourceMinioBucketServerSideEncryption(),
"minio_s3_object": resourceMinioObject(),
Expand Down
247 changes: 247 additions & 0 deletions minio/resource_minio_s3_bucket_retention.go
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
}
Loading
Loading