-
Notifications
You must be signed in to change notification settings - Fork 632
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
migrate api_shield_operation to framework
- Loading branch information
Brendan Ball
committed
Jan 16, 2025
1 parent
8a39731
commit d7c2be0
Showing
11 changed files
with
432 additions
and
177 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,12 @@ | ||
```release-note:note | ||
resource/cloudflare_api_shield_operation: migrated to the `terraform-plugin-framework`. | ||
``` | ||
|
||
```release-note:bug | ||
resource/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` | ||
``` |
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
106 changes: 106 additions & 0 deletions
106
internal/framework/service/api_shield_operation/endpoint.go
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,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
17
internal/framework/service/api_shield_operation/endpoint_test.go
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,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) | ||
} |
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,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
148
internal/framework/service/api_shield_operation/resource.go
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,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], | ||
)...) | ||
} |
Oops, something went wrong.