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

resource/cloudflare_api_shield_operation: add API Shield Operation resource #2760

Merged
merged 6 commits into from
Oct 3, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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
jacobbednarz marked this conversation as resolved.
Show resolved Hide resolved
```
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
jacobbednarz marked this conversation as resolved.
Show resolved Hide resolved
resource "cloudflare_api_shield_operation" "example" {
zone_id = "0da42c8d2132a9ddaf714f9e7c920711"
method = "GET"
host = "api.cloudflare.com"
endpoint = "/client/v4/zones"
jacobbednarz marked this conversation as resolved.
Show resolved Hide resolved
}
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,
jacobbednarz marked this conversation as resolved.
Show resolved Hide resolved
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)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package sdkv2provider

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

"github.com/cloudflare/terraform-provider-cloudflare/internal/consts"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/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,
},
}
}