Skip to content
Open
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
43 changes: 43 additions & 0 deletions internal/services/r2_bucket_event_notification/data_source_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package r2_bucket_event_notification_test

import (
"os"
"testing"

"github.com/cloudflare/terraform-provider-cloudflare/internal/acctest"
"github.com/cloudflare/terraform-provider-cloudflare/internal/utils"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/knownvalue"
"github.com/hashicorp/terraform-plugin-testing/statecheck"
"github.com/hashicorp/terraform-plugin-testing/tfjsonpath"
)

func TestAccCloudflareR2BucketEventNotificationDataSource_Basic(t *testing.T) {
rnd := utils.GenerateRandomResourceName()
accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID")
dataSourceName := "data.cloudflare_r2_bucket_event_notification." + rnd

resource.Test(t, resource.TestCase{
PreCheck: func() { acctest.TestAccPreCheck(t) },
ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccR2BucketEventNotificationDataSourceConfig(rnd, accountID),
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue(dataSourceName, tfjsonpath.New("account_id"), knownvalue.StringExact(accountID)),
statecheck.ExpectKnownValue(dataSourceName, tfjsonpath.New("bucket_name"), knownvalue.StringExact(rnd)),
statecheck.ExpectKnownValue(dataSourceName, tfjsonpath.New("queue_id"), knownvalue.NotNull()),
// statecheck.ExpectKnownValue(dataSourceName, tfjsonpath.New("queue_name"), knownvalue.StringExact(fmt.Sprintf("%s-queue", rnd))),
statecheck.ExpectKnownValue(dataSourceName, tfjsonpath.New("rules"), knownvalue.ListSizeExact(1)),
statecheck.ExpectKnownValue(dataSourceName, tfjsonpath.New("rules").AtSliceIndex(0).AtMapKey("actions"), knownvalue.ListSizeExact(2)),
statecheck.ExpectKnownValue(dataSourceName, tfjsonpath.New("rules").AtSliceIndex(0).AtMapKey("description"), knownvalue.StringExact("Data source test event notifications")),
statecheck.ExpectKnownValue(dataSourceName, tfjsonpath.New("rules").AtSliceIndex(0).AtMapKey("prefix"), knownvalue.StringExact("test/")),
},
},
},
})
}

func testAccR2BucketEventNotificationDataSourceConfig(rnd, accountID string) string {
return acctest.LoadTestCase("datasource_basic.tf", rnd, accountID)
}
97 changes: 97 additions & 0 deletions internal/services/r2_bucket_event_notification/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package r2_bucket_event_notification
import (
"github.com/cloudflare/terraform-provider-cloudflare/internal/apijson"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
)

type R2BucketEventNotificationResultEnvelope struct {
Expand Down Expand Up @@ -34,3 +35,99 @@ type R2BucketEventNotificationRulesModel struct {
Prefix types.String `tfsdk:"prefix" json:"prefix,optional"`
Suffix types.String `tfsdk:"suffix" json:"suffix,optional"`
}

func (enRules1 R2BucketEventNotificationRulesModel) Equal(enRules2 R2BucketEventNotificationRulesModel) bool {
if !stringEqualNullOrEmpty(enRules1.Prefix, enRules2.Prefix) {
return false
}

if !stringEqualNullOrEmpty(enRules1.Suffix, enRules2.Suffix) {
return false
}

if !stringEqualNullOrEmpty(enRules1.Description, enRules2.Description) {
return false
}

return actionsEqual(enRules1.Actions, enRules2.Actions)
}

// String comaprison function that treats empty strings as equal to null
func stringEqualNullOrEmpty(s1, s2 types.String) bool {
if s1.Equal(s2) {
return true
}

if s1.IsNull() && s2.Equal(basetypes.NewStringValue("")) {
return true
}
if s2.IsNull() && s1.Equal(basetypes.NewStringValue("")) {
return true
}

return false
}

// actionsEqual does an order-independent comparison of 2 actions arrays
func actionsEqual(a1, a2 *[]types.String) bool {
if a1 == a2 {
return true
}
if a1 == nil || a2 == nil {
return false
}

// Check lengths
if len(*a1) != len(*a2) {
return false
}

counts1 := make(map[string]int)
for _, action := range *a1 {
counts1[action.ValueString()]++
}

counts2 := make(map[string]int)
for _, action := range *a2 {
counts2[action.ValueString()]++
}

if len(counts1) != len(counts2) {
return false
}

for action, count := range counts1 {
if counts2[action] != count {
return false
}
}

return true
}

// RulesEqual compares two Rules arrays for equality
func RulesEqual(planRules, stateRules *[]*R2BucketEventNotificationRulesModel) bool {
if planRules == stateRules {
return true
}
if planRules == nil || stateRules == nil {
return false
}

// Check lengths
if len(*planRules) != len(*stateRules) {
return false
}

// Compare each rule element by element
for i := range *planRules {
rule1 := (*planRules)[i]
rule2 := (*stateRules)[i]

if !rule1.Equal(*rule2) {
return false
}
}

return true
}
30 changes: 30 additions & 0 deletions internal/services/r2_bucket_event_notification/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,35 @@ func (r *R2BucketEventNotificationResource) Update(ctx context.Context, req reso
return
}

if data.AccountID == state.AccountID &&
data.BucketName == state.BucketName &&
data.Jurisdiction == state.Jurisdiction &&
data.QueueID == state.QueueID &&
RulesEqual(data.Rules, state.Rules) {
// No changes were detected, preserve planned values (including null for prefix/suffix)
data.QueueName = state.QueueName
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
return
}

// To make this compatible with R2's event notification API, the
// existing configuration must be deleted first before applying the plan
_, err := r.client.R2.Buckets.EventNotifications.Delete(
ctx,
data.BucketName.ValueString(),
data.QueueID.ValueString(),
r2.BucketEventNotificationDeleteParams{
AccountID: cloudflare.F(data.AccountID.ValueString()),
},
option.WithHeader(consts.R2JurisdictionHTTPHeaderName, data.Jurisdiction.ValueString()),
option.WithMiddleware(logging.Middleware(ctx)),
)
if err != nil {
resp.Diagnostics.AddError("failed to make http request", err.Error())
return
}

// Start Update
dataBytes, err := data.MarshalJSONForUpdate(*state)
if err != nil {
resp.Diagnostics.AddError("failed to serialize http request", err.Error())
Expand Down Expand Up @@ -142,6 +171,7 @@ func (r *R2BucketEventNotificationResource) Update(ctx context.Context, req reso
resp.Diagnostics.AddError("failed to deserialize http request", err.Error())
return
}

data = &env.Result

resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
Expand Down
160 changes: 160 additions & 0 deletions internal/services/r2_bucket_event_notification/resource_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package r2_bucket_event_notification_test

import (
"os"
"testing"

"github.com/cloudflare/terraform-provider-cloudflare/internal/acctest"
"github.com/cloudflare/terraform-provider-cloudflare/internal/utils"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/knownvalue"
"github.com/hashicorp/terraform-plugin-testing/statecheck"
"github.com/hashicorp/terraform-plugin-testing/tfjsonpath"
)

func TestAccCloudflareR2BucketEventNotification_Basic(t *testing.T) {
rnd := utils.GenerateRandomResourceName()
accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID")
resourceName := "cloudflare_r2_bucket_event_notification." + rnd

resource.Test(t, resource.TestCase{
PreCheck: func() { acctest.TestAccPreCheck(t) },
ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccR2BucketEventNotificationConfig(rnd, accountID),
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("account_id"), knownvalue.StringExact(accountID)),
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("bucket_name"), knownvalue.StringExact(rnd)),
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("queue_id"), knownvalue.NotNull()),
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("jurisdiction"), knownvalue.StringExact("default")),
// statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("queue_name"), knownvalue.StringExact(fmt.Sprintf("%s-queue", rnd))),
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("rules"), knownvalue.ListSizeExact(1)),
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("rules").AtSliceIndex(0).AtMapKey("actions"), knownvalue.ListSizeExact(2)),
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("rules").AtSliceIndex(0).AtMapKey("description"), knownvalue.StringExact("Basic event notification rule")),
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("rules").AtSliceIndex(0).AtMapKey("prefix"), knownvalue.StringExact("documents/")),
},
},
},
})
}

func TestAccCloudflareR2BucketEventNotification_Update(t *testing.T) {
rnd := utils.GenerateRandomResourceName()
accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID")
resourceName := "cloudflare_r2_bucket_event_notification." + rnd

resource.Test(t, resource.TestCase{
PreCheck: func() { acctest.TestAccPreCheck(t) },
ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccR2BucketEventNotificationConfig(rnd, accountID),
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("rules"), knownvalue.ListSizeExact(1)),
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("rules").AtSliceIndex(0).AtMapKey("actions"), knownvalue.ListSizeExact(2)),
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("rules").AtSliceIndex(0).AtMapKey("prefix"), knownvalue.StringExact("documents/")),
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("rules").AtSliceIndex(0).AtMapKey("suffix"), knownvalue.Null()),
},
},
{
Config: testAccR2BucketEventNotificationMultipleRulesConfig(rnd, accountID),
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("rules"), knownvalue.ListSizeExact(2)),
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("rules").AtSliceIndex(0).AtMapKey("actions"), knownvalue.ListSizeExact(4)),
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("rules").AtSliceIndex(0).AtMapKey("prefix"), knownvalue.StringExact("uploads/")),
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("rules").AtSliceIndex(0).AtMapKey("suffix"), knownvalue.StringExact(".jpg")),
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("rules").AtSliceIndex(1).AtMapKey("actions"), knownvalue.ListSizeExact(1)),
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("rules").AtSliceIndex(1).AtMapKey("description"), knownvalue.StringExact("Lifecycle deletion notifications")),
},
},
},
})
}

func TestAccCloudflareR2BucketEventNotification_JurisdictionEU(t *testing.T) {
rnd := utils.GenerateRandomResourceName()
accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID")
resourceName := "cloudflare_r2_bucket_event_notification." + rnd

resource.Test(t, resource.TestCase{
PreCheck: func() { acctest.TestAccPreCheck(t) },
ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccR2BucketEventNotificationJurisdictionEUConfig(rnd, accountID),
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("account_id"), knownvalue.StringExact(accountID)),
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("bucket_name"), knownvalue.StringExact(rnd)),
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("jurisdiction"), knownvalue.StringExact("eu")),
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("rules").AtSliceIndex(0).AtMapKey("prefix"), knownvalue.StringExact("gdpr/")),
},
},
},
})
}

func TestAccCloudflareR2BucketEventNotification_JurisdictionFedRAMP(t *testing.T) {
rnd := utils.GenerateRandomResourceName()
accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID")
resourceName := "cloudflare_r2_bucket_event_notification." + rnd

resource.Test(t, resource.TestCase{
PreCheck: func() { acctest.TestAccPreCheck(t) },
ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccR2BucketEventNotificationJurisdictionFedRAMPConfig(rnd, accountID),
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("account_id"), knownvalue.StringExact(accountID)),
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("bucket_name"), knownvalue.StringExact(rnd)),
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("jurisdiction"), knownvalue.StringExact("fedramp")),
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("rules").AtSliceIndex(0).AtMapKey("prefix"), knownvalue.StringExact("gdpr/")),
},
},
},
})
}

func TestAccCloudflareR2BucketEventNotification_AllActions(t *testing.T) {
rnd := utils.GenerateRandomResourceName()
accountID := os.Getenv("CLOUDFLARE_ACCOUNT_ID")
resourceName := "cloudflare_r2_bucket_event_notification." + rnd

resource.Test(t, resource.TestCase{
PreCheck: func() { acctest.TestAccPreCheck(t) },
ProtoV6ProviderFactories: acctest.TestAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccR2BucketEventNotificationAllActionsConfig(rnd, accountID),
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("rules"), knownvalue.ListSizeExact(1)),
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("rules").AtSliceIndex(0).AtMapKey("actions"), knownvalue.ListSizeExact(5)),
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("rules").AtSliceIndex(0).AtMapKey("description"), knownvalue.StringExact("All supported R2 object actions")),
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("rules").AtSliceIndex(0).AtMapKey("prefix"), knownvalue.Null()),
statecheck.ExpectKnownValue(resourceName, tfjsonpath.New("rules").AtSliceIndex(0).AtMapKey("suffix"), knownvalue.Null()),
},
},
},
})
}

func testAccR2BucketEventNotificationConfig(rnd, accountID string) string {
return acctest.LoadTestCase("r2notificationbasic.tf", rnd, accountID)
}

func testAccR2BucketEventNotificationMultipleRulesConfig(rnd, accountID string) string {
return acctest.LoadTestCase("r2notificationmultiple_rules.tf", rnd, accountID)
}

func testAccR2BucketEventNotificationJurisdictionEUConfig(rnd, accountID string) string {
return acctest.LoadTestCase("r2notificationjurisdiction_eu.tf", rnd, accountID)
}

func testAccR2BucketEventNotificationJurisdictionFedRAMPConfig(rnd, accountID string) string {
return acctest.LoadTestCase("r2notificationjurisdiction_fedramp.tf", rnd, accountID)
}

func testAccR2BucketEventNotificationAllActionsConfig(rnd, accountID string) string {
return acctest.LoadTestCase("r2notificationall_actions.tf", rnd, accountID)
}
5 changes: 4 additions & 1 deletion internal/services/r2_bucket_event_notification/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func ResourceSchema(ctx context.Context) schema.Schema {
},
"rules": schema.ListNestedAttribute{
Description: "Array of rules to drive notifications.",
Optional: true,
Required: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"actions": schema.ListAttribute{
Expand Down Expand Up @@ -84,6 +84,9 @@ func ResourceSchema(ctx context.Context) schema.Schema {
},
},
},
Validators: []validator.List{
listvalidator.SizeAtLeast(1),
},
},
"queue_name": schema.StringAttribute{
Description: "Name of the queue.",
Expand Down
Loading