Skip to content

Commit

Permalink
APISHI-2354 resource/cloudflare_api_shield_operation: add API Shield …
Browse files Browse the repository at this point in the history
…Operation resource
  • Loading branch information
djhworld committed Sep 14, 2023
1 parent 3befeb9 commit 93f97ae
Show file tree
Hide file tree
Showing 7 changed files with 305 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .changelog/2760.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:new-resource
resource/cloudflare_api_shield_operation: add API Shield Operation resource
```
37 changes: 37 additions & 0 deletions docs/resources/api_shield_operation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
page_title: "cloudflare_api_shield_operation Resource - Cloudflare"
subcategory: ""
description: |-
Provides a resource to manage an operation in API Shield Endpoint Management.
---

# cloudflare_api_shield_operation (Resource)

Provides a resource to manage an operation in API Shield Endpoint Management.

## Example Usage

```terraform
# Operation to manage in API Shield Endpoint Management
resource "cloudflare_api_shield_operation" "example" {
zone_id = "0da42c8d2132a9ddaf714f9e7c920711"
method = "GET"
host = "api.cloudflare.com"
endpoint = "/client/v4/zones"
}
```
<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `endpoint` (String) The endpoint which can contain path parameter templates in curly braces, each will be replaced from left to right with `{varN}`, starting with `{var1}`. This will then be [Cloudflare-normalized](https://developers.cloudflare.com/rules/normalization/how-it-works/). **Modifying this attribute will force creation of a new resource.**
- `host` (String) RFC3986-compliant host. **Modifying this attribute will force creation of a new resource.**
- `method` (String) The HTTP method used to access the endpoint. **Modifying this attribute will force creation of a new resource.**
- `zone_id` (String) The zone identifier to target for the resource. **Modifying this attribute will force creation of a new resource.**

### Read-Only

- `id` (String) The ID of this resource.


Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Operation to manage in API Shield Endpoint Management
resource "cloudflare_api_shield_operation" "example" {
zone_id = "0da42c8d2132a9ddaf714f9e7c920711"
method = "GET"
host = "api.cloudflare.com"
endpoint = "/client/v4/zones"
}
1 change: 1 addition & 0 deletions internal/sdkv2provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ func New(version string) func() *schema.Provider {
"cloudflare_account": resourceCloudflareAccount(),
"cloudflare_address_map": resourceCloudflareAddressMap(),
"cloudflare_api_shield": resourceCloudflareAPIShield(),
"cloudflare_api_shield_operation": resourceCloudflareAPIShieldOperation(),
"cloudflare_api_token": resourceCloudflareApiToken(),
"cloudflare_argo": resourceCloudflareArgo(),
"cloudflare_authenticated_origin_pulls_certificate": resourceCloudflareAuthenticatedOriginPullsCertificate(),
Expand Down
108 changes: 108 additions & 0 deletions internal/sdkv2provider/resource_cloudflare_api_shield_operation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package sdkv2provider

import (
"context"
"fmt"
"github.com/pkg/errors"

"github.com/MakeNowJust/heredoc/v2"
"github.com/cloudflare/cloudflare-go"
"github.com/cloudflare/terraform-provider-cloudflare/internal/consts"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

func resourceCloudflareAPIShieldOperation() *schema.Resource {
return &schema.Resource{
Schema: resourceCloudflareAPIShieldOperationSchema(),
CreateContext: resourceCloudflareAPIShieldOperationCreate,
ReadContext: resourceCloudflareAPIShieldOperationRead,
DeleteContext: resourceCloudflareAPIShieldOperationDelete,
Importer: &schema.ResourceImporter{
StateContext: nil,
},
Description: heredoc.Doc(`
Provides a resource to manage an operation in API Shield Endpoint Management.
`),
}
}

func resourceCloudflareAPIShieldOperationCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*cloudflare.API)
zoneID := d.Get(consts.ZoneIDSchemaKey).(string)

ops, err := client.CreateAPIShieldOperations(
ctx,
cloudflare.ZoneIdentifier(zoneID),
cloudflare.CreateAPIShieldOperationsParams{
Operations: []cloudflare.APIShieldBasicOperation{
{
Method: d.Get("method").(string),
Host: d.Get("host").(string),
Endpoint: d.Get("endpoint").(string),
},
},
},
)

if err != nil {
return diag.FromErr(errors.Wrap(err, "failed to create API Shield Operation"))
}

if length := len(ops); length != 1 {
return diag.FromErr(fmt.Errorf("expected output to have 1 entry but got: %d", length))
}

d.SetId(ops[0].ID)
return resourceCloudflareAPIShieldOperationRead(ctx, d, meta)
}

func resourceCloudflareAPIShieldOperationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*cloudflare.API)
zoneID := d.Get(consts.ZoneIDSchemaKey).(string)

op, err := client.GetAPIShieldOperation(
ctx,
cloudflare.ZoneIdentifier(zoneID),
cloudflare.GetAPIShieldOperationParams{
OperationID: d.Id(),
},
)

if err != nil {
return diag.FromErr(fmt.Errorf("failed to fetch API Shield Operation: %w", err))
}

if err := d.Set("method", op.Method); err != nil {
return diag.FromErr(err)
}

if err := d.Set("host", op.Host); err != nil {
return diag.FromErr(err)
}

if err := d.Set("endpoint", op.Endpoint); err != nil {
return diag.FromErr(err)
}

d.SetId(op.ID)
return nil
}

func resourceCloudflareAPIShieldOperationDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*cloudflare.API)
zoneID := d.Get(consts.ZoneIDSchemaKey).(string)

err := client.DeleteAPIShieldOperation(
ctx,
cloudflare.ZoneIdentifier(zoneID),
cloudflare.DeleteAPIShieldOperationParams{
OperationID: d.Id(),
},
)
if err != nil {
return diag.FromErr(fmt.Errorf("failed to fetch API Shield Operation: %w", err))
}

return nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package sdkv2provider

import (
"context"
"errors"
"fmt"
"github.com/cloudflare/cloudflare-go"
"github.com/cloudflare/terraform-provider-cloudflare/internal/consts"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/terraform"
"os"
"testing"
)

func TestAccAPIShieldOperation_Create(t *testing.T) {
// Temporarily unset CLOUDFLARE_API_TOKEN if it is set as the API token
// endpoint does not yet support the API tokens without an explicit scope.
if os.Getenv("CLOUDFLARE_API_TOKEN") != "" {
t.Setenv("CLOUDFLARE_API_TOKEN", "")
}

rnd := generateRandomResourceName()
resourceID := "cloudflare_api_shield_operation." + rnd
zoneID := os.Getenv("CLOUDFLARE_ZONE_ID")

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: providerFactories,
CheckDestroy: testAccCheckAPIShieldOperationDelete,
Steps: []resource.TestStep{
{
Config: testAccCloudflareAPIShieldOperation(rnd, zoneID, cloudflare.APIShieldBasicOperation{Method: "GET", Host: "api.cloudflare.com", Endpoint: "/client/v4/zones"}),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceID, consts.ZoneIDSchemaKey, zoneID),
resource.TestCheckResourceAttr(resourceID, "method", "GET"),
resource.TestCheckResourceAttr(resourceID, "host", "api.cloudflare.com"),
resource.TestCheckResourceAttr(resourceID, "endpoint", "/client/v4/zones"),
),
},
},
})
}

func TestAccAPIShieldOperation_ForceNew(t *testing.T) {
// Temporarily unset CLOUDFLARE_API_TOKEN if it is set as the API token
// endpoint does not yet support the API tokens without an explicit scope.
if os.Getenv("CLOUDFLARE_API_TOKEN") != "" {
t.Setenv("CLOUDFLARE_API_TOKEN", "")
}

rnd := generateRandomResourceName()
resourceID := "cloudflare_api_shield_operation." + rnd
zoneID := os.Getenv("CLOUDFLARE_ZONE_ID")

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProviderFactories: providerFactories,
CheckDestroy: testAccCheckAPIShieldOperationDelete,
Steps: []resource.TestStep{
{
Config: testAccCloudflareAPIShieldOperation(rnd, zoneID, cloudflare.APIShieldBasicOperation{Method: "GET", Host: "api.cloudflare.com", Endpoint: "/client/v4/zones"}),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceID, consts.ZoneIDSchemaKey, zoneID),
resource.TestCheckResourceAttr(resourceID, "method", "GET"),
resource.TestCheckResourceAttr(resourceID, "host", "api.cloudflare.com"),
resource.TestCheckResourceAttr(resourceID, "endpoint", "/client/v4/zones"),
),
},
{
Config: testAccCloudflareAPIShieldOperation(rnd, zoneID, cloudflare.APIShieldBasicOperation{Method: "POST", Host: "api.cloudflare.com", Endpoint: "/client/v4/zones"}),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceID, consts.ZoneIDSchemaKey, zoneID),
resource.TestCheckResourceAttr(resourceID, "method", "POST"), // check that we've 'updated' the value
resource.TestCheckResourceAttr(resourceID, "host", "api.cloudflare.com"),
resource.TestCheckResourceAttr(resourceID, "endpoint", "/client/v4/zones"),
),
},
},
})
}

func testAccCheckAPIShieldOperationDelete(s *terraform.State) error {
client := testAccProvider.Meta().(*cloudflare.API)

for _, rs := range s.RootModule().Resources {
if rs.Type != "cloudflare_api_shield_operation" {
continue
}

_, err := client.GetAPIShieldOperation(
context.Background(),
cloudflare.ZoneIdentifier(rs.Primary.Attributes[consts.ZoneIDSchemaKey]),
cloudflare.GetAPIShieldOperationParams{
OperationID: rs.Primary.Attributes["id"],
},
)
if err == nil {
return fmt.Errorf("operation still exists")
}

var notFoundError *cloudflare.NotFoundError
if !errors.As(err, &notFoundError) {
return fmt.Errorf("expected not found error but got: %w", err)
}
}

return nil
}

func testAccCloudflareAPIShieldOperation(resourceName, zone string, op cloudflare.APIShieldBasicOperation) string {
return fmt.Sprintf(`
resource "cloudflare_api_shield_operation" "%[1]s" {
zone_id = "%[2]s"
method = "%[3]s"
host = "%[4]s"
endpoint = "%[5]s"
}
`, resourceName, zone, op.Method, op.Host, op.Endpoint)
}
30 changes: 30 additions & 0 deletions internal/sdkv2provider/schema_cloudflare_api_shield.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package sdkv2provider

import (
"fmt"
"github.com/MakeNowJust/heredoc/v2"

"github.com/cloudflare/terraform-provider-cloudflare/internal/consts"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
Expand Down Expand Up @@ -38,3 +39,32 @@ func resourceCloudflareAPIShieldSchema() map[string]*schema.Schema {
},
}
}

func resourceCloudflareAPIShieldOperationSchema() map[string]*schema.Schema {
return map[string]*schema.Schema{
consts.ZoneIDSchemaKey: {
Description: consts.ZoneIDSchemaDescription,
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"method": {
Description: "The HTTP method used to access the endpoint",
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"host": {
Description: "RFC3986-compliant host",
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"endpoint": {
Description: heredoc.Doc("The endpoint which can contain path parameter templates in curly braces, each will be replaced from left to right with `{varN}`, starting with `{var1}`. This will then be [Cloudflare-normalized](https://developers.cloudflare.com/rules/normalization/how-it-works/)"),
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
}
}

0 comments on commit 93f97ae

Please sign in to comment.