Skip to content

Commit

Permalink
migrate api_shield_operation to framework
Browse files Browse the repository at this point in the history
  • Loading branch information
Brendan Ball committed Jan 16, 2025
1 parent 8a39731 commit 23e3a5e
Show file tree
Hide file tree
Showing 11 changed files with 432 additions and 177 deletions.
12 changes: 12 additions & 0 deletions .changelog/4893.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
```release-note:note
`cloudflare_api_shield_operation` has been migrated to the `terraform-plugin-framework`.
```

```release-note:bug
`cloudflare_api_shield_operation` fixed a bug when using variable names other than `var1 ... varN` in endpoint definitions causing these resources to be recreated when nothing has changed.
If this affects you, after upgrading to this version, the resource has to be recreated once more to fix the state, after which the bug is fixed.
```

```release-note:internal
resource/cloudflare_api_shield_operation: migrate from SDKv2 to `terraform-plugin-framework`
```
2 changes: 2 additions & 0 deletions internal/framework/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/cloudflare/terraform-provider-cloudflare/internal/consts"
"github.com/cloudflare/terraform-provider-cloudflare/internal/framework/muxclient"
"github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/access_mutual_tls_hostname_settings"
apishieldoperation "github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/api_shield_operation"
"github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/api_token_permissions_groups"
"github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/cloud_connector_rules"
"github.com/cloudflare/terraform-provider-cloudflare/internal/framework/service/content_scanning"
Expand Down Expand Up @@ -399,6 +400,7 @@ func (p *CloudflareProvider) Resources(ctx context.Context) []func() resource.Re
leaked_credential_check_rule.NewResource,
content_scanning.NewResource,
content_scanning_expression.NewResource,
apishieldoperation.NewResource,
}
}

Expand Down
106 changes: 106 additions & 0 deletions internal/framework/service/api_shield_operation/endpoint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package apishieldoperation

import (
"context"
"fmt"
"regexp"

"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)

var varMatch = regexp.MustCompile(`{([\w\d]+)}`)

// EndpointType implemented based on https://developer.hashicorp.com/terraform/plugin/framework/handling-data/types/custom

// Ensure the implementation satisfies the expected interfaces
var _ basetypes.StringTypable = EndpointType{}
var _ basetypes.StringValuableWithSemanticEquals = EndpointValue{}

type EndpointType struct {
basetypes.StringType
}

func (t EndpointType) Equal(o attr.Type) bool {
other, ok := o.(EndpointType)

if !ok {
return false
}

return t.StringType.Equal(other.StringType)
}

func (t EndpointType) String() string {
return "EndpointType"
}

func (t EndpointType) ValueFromString(ctx context.Context, in basetypes.StringValue) (basetypes.StringValuable, diag.Diagnostics) {
value := EndpointValue{
StringValue: in,
}

return value, nil
}

func (t EndpointType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) {
attrValue, err := t.StringType.ValueFromTerraform(ctx, in)

if err != nil {
return nil, err
}

stringValue, ok := attrValue.(basetypes.StringValue)

if !ok {
return nil, fmt.Errorf("unexpected value type of %T", attrValue)
}

stringValuable, diags := t.ValueFromString(ctx, stringValue)

if diags.HasError() {
return nil, fmt.Errorf("unexpected error converting StringValue to StringValuable: %v", diags)
}

return stringValuable, nil
}

func (t EndpointType) ValueType(ctx context.Context) attr.Value {
return EndpointValue{}
}

var _ basetypes.StringValuable = EndpointValue{}

type EndpointValue struct {
basetypes.StringValue
}

func (v EndpointValue) Equal(o attr.Value) bool {
other, ok := o.(EndpointValue)

if !ok {
return false
}

return v.StringValue.Equal(other.StringValue)
}

func (v EndpointValue) Type(ctx context.Context) attr.Type {
return EndpointType{}
}

func NewEndpointValue(value string) EndpointValue {
if value == "" {
return EndpointValue{types.StringNull()}
}
return EndpointValue{types.StringValue(value)}
}

func (v EndpointValue) StringSemanticEquals(ctx context.Context, o basetypes.StringValuable) (bool, diag.Diagnostics) {
oStrVal, diag := o.ToStringValue(ctx)
result := varMatch.ReplaceAllString(v.StringValue.ValueString(), "{var}") == varMatch.ReplaceAllString(oStrVal.ValueString(), "{var}")
return result, diag
}
17 changes: 17 additions & 0 deletions internal/framework/service/api_shield_operation/endpoint_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package apishieldoperation

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
)

func TestEndpointValueEquality(t *testing.T) {
e1 := NewEndpointValue("/foo/{fooId}/bar/{barId}/baz")
e2 := NewEndpointValue("/foo/{var1}/bar/{var2}/baz")

ok, diag := e1.StringSemanticEquals(context.Background(), e2)
assert.Nil(t, diag)
assert.True(t, ok)
}
11 changes: 11 additions & 0 deletions internal/framework/service/api_shield_operation/model.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package apishieldoperation

import "github.com/hashicorp/terraform-plugin-framework/types"

type APIShieldOperationModel struct {
ID types.String `tfsdk:"id"`
ZoneID types.String `tfsdk:"zone_id"`
Method types.String `tfsdk:"method"`
Host types.String `tfsdk:"host"`
Endpoint EndpointValue `tfsdk:"endpoint"`
}
148 changes: 148 additions & 0 deletions internal/framework/service/api_shield_operation/resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package apishieldoperation

import (
"context"
"fmt"
"strings"

"github.com/cloudflare/cloudflare-go"
"github.com/cloudflare/terraform-provider-cloudflare/internal/framework/flatteners"
"github.com/cloudflare/terraform-provider-cloudflare/internal/framework/muxclient"

"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
)

// Ensure provider defined types fully satisfy framework interfaces.
var _ resource.Resource = &APIShieldOperationResource{}
var _ resource.ResourceWithImportState = &APIShieldOperationResource{}

func NewResource() resource.Resource {
return &APIShieldOperationResource{}
}

type APIShieldOperationResource struct {
client *muxclient.Client
}

func (r *APIShieldOperationResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_api_shield_operation"
}

func (r *APIShieldOperationResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}

r.client = req.ProviderData.(*muxclient.Client)
}

func (r *APIShieldOperationResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var model APIShieldOperationModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...)
if resp.Diagnostics.HasError() {
return
}

ops, err := r.client.V1.CreateAPIShieldOperations(
ctx,
cloudflare.ZoneIdentifier(model.ZoneID.ValueString()),
cloudflare.CreateAPIShieldOperationsParams{
Operations: []cloudflare.APIShieldBasicOperation{
{
Method: model.Method.ValueString(),
Host: model.Host.ValueString(),
Endpoint: model.Endpoint.ValueString(),
},
},
},
)

if err != nil {
resp.Diagnostics.AddError("Error creating API Shield Operation", err.Error())
return
}

if len(ops) != 1 {
resp.Diagnostics.AddError("Error creating API Shield Operation", fmt.Sprintf("expected 1 operation in response but got %d", len(ops)))
return
}

op := ops[0]
// The API normalizes the response, so we must not override it on create.
// See https://github.com/hashicorp/terraform/blob/main/docs/resource-instance-change-lifecycle.md
// Normalization: the remote API has returned some data in a different form than was recorded in the Previous Run State, but the meaning is unchanged.
// In this case, the provider should return the exact value from the Previous Run State,
// thereby preserving the value as it was written by the user in the configuration and thus avoiding unwanted cascading changes to elsewhere in the configuration.
model.ID = flatteners.String(op.ID)
resp.Diagnostics.Append(resp.State.Set(ctx, model)...)
}

func (r *APIShieldOperationResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var model APIShieldOperationModel
resp.Diagnostics.Append(req.State.Get(ctx, &model)...)
if resp.Diagnostics.HasError() {
return
}

op, err := r.client.V1.GetAPIShieldOperation(
ctx,
cloudflare.ZoneIdentifier(model.ZoneID.ValueString()),
cloudflare.GetAPIShieldOperationParams{
OperationID: model.ID.ValueString(),
})

if err != nil {
resp.Diagnostics.AddError("Error reading API Shield Operation", err.Error())
return
}

model.ID = flatteners.String(op.ID)
model.Method = flatteners.String(op.Method)
model.Host = flatteners.String(op.Host)
model.Endpoint = NewEndpointValue(op.Endpoint)

resp.Diagnostics.Append(resp.State.Set(ctx, &model)...)
}

func (r *APIShieldOperationResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
resp.Diagnostics.AddError(
"Update API shield Operation is not supported",
"Update should never have been called because the resource is configured to be replaced instead",
)
}

func (r *APIShieldOperationResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var model APIShieldOperationModel
resp.Diagnostics.Append(req.State.Get(ctx, &model)...)
if resp.Diagnostics.HasError() {
return
}

err := r.client.V1.DeleteAPIShieldOperation(
ctx,
cloudflare.ZoneIdentifier(model.ZoneID.ValueString()),
cloudflare.DeleteAPIShieldOperationParams{
OperationID: model.ID.ValueString(),
},
)

if err != nil {
resp.Diagnostics.AddError("Error deleting API Shield Operation", err.Error())
return
}
}

func (r *APIShieldOperationResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
idparts := strings.Split(req.ID, "/")
if len(idparts) != 2 {
resp.Diagnostics.AddError("error importing api_shield_operation", `invalid ID specified. Please specify the ID as "<zone_id>/<operation_id>"`)
return
}
resp.Diagnostics.Append(resp.State.SetAttribute(
ctx, path.Root("zone_id"), idparts[0],
)...)
resp.Diagnostics.Append(resp.State.SetAttribute(
ctx, path.Root("id"), idparts[1],
)...)
}
Loading

0 comments on commit 23e3a5e

Please sign in to comment.