Repository: cloudflare/cloudflare-go
Pull request #3184 (2024-09-19 16:10:40)

Add CRUD endpoints for Infrastructure Access Targets
## Description
We are recently added new endpoints for infrastructure access. We are looking to get these endpoints reflected in the terraform provider documentation. This PR contains the client side changes necessary for that. It writes CRUDL functions for the terraform provider to interact with our new endpoints. 

Our OpenAPI schema is published in `cloudflare/api-schemas` viewable in [this commit](https://github.com/cloudflare/api-schemas/commit/07d60aae32323373b4a35ff1b8c2246ad927ef9e)
Our API docs are accessible in production at https://developers.cloudflare.com/api/operations/infra-targets-list

## Has your change been tested?
I've tested these changes through acceptance tests.

## Screenshots (if appropriate):

## Types of changes

What sort of change does your code introduce/modify?

- [ ] Bug fix (non-breaking change which fixes an issue)
- [X] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)

## Checklist:

- [X] My code follows the code style of this project.
- [ ] My change requires a change to the documentation.
- [ ] I have updated the documentation accordingly.
- [X] I have added tests to cover my changes.
- [X] All new and existing tests passed.
- [x] This change is using publicly documented in [cloudflare/api-schemas](https://github.com/cloudflare/api-schemas) 
      and relies on stable APIs.

[1]: https://help.github.com/articles/closing-issues-using-keywords/



Patch:
diff --git a/.changelog/3184.txt b/.changelog/3184.txt
new file mode 100644
index 000000000..e0c858c35
--- /dev/null
+++ b/.changelog/3184.txt
@@ -0,0 +1,3 @@
+```release-note:enhancement
+infrastructure_targets: initialize CRUD endpoints for infrastructure access endpoints
+```
diff --git a/infrastructure_access_target.go b/infrastructure_access_target.go
new file mode 100644
index 000000000..0c6a72a86
--- /dev/null
+++ b/infrastructure_access_target.go
@@ -0,0 +1,234 @@
+package cloudflare
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net/http"
+
+	"github.com/goccy/go-json"
+)
+
+var ErrMissingTargetId = errors.New("required target id missing")
+
+type InfrastructureAccessTarget struct {
+	Hostname   string                           `json:"hostname"`
+	ID         string                           `json:"id"`
+	IP         InfrastructureAccessTargetIPInfo `json:"ip"`
+	CreatedAt  string                           `json:"created_at"`
+	ModifiedAt string                           `json:"modified_at"`
+}
+
+type InfrastructureAccessTargetIPInfo struct {
+	IPV4 *InfrastructureAccessTargetIPDetails `json:"ipv4,omitempty"`
+	IPV6 *InfrastructureAccessTargetIPDetails `json:"ipv6,omitempty"`
+}
+
+type InfrastructureAccessTargetIPDetails struct {
+	IPAddr           string `json:"ip_addr"`
+	VirtualNetworkId string `json:"virtual_network_id"`
+}
+
+type InfrastructureAccessTargetParams struct {
+	Hostname string                           `json:"hostname"`
+	IP       InfrastructureAccessTargetIPInfo `json:"ip"`
+}
+
+type CreateInfrastructureAccessTargetParams struct {
+	InfrastructureAccessTargetParams
+}
+
+type UpdateInfrastructureAccessTargetParams struct {
+	ID           string                           `json:"-"`
+	ModifyParams InfrastructureAccessTargetParams `json:"modify_params"`
+}
+
+// InfrastructureAccessTargetDetailResponse is the API response, containing a single target.
+type InfrastructureAccessTargetDetailResponse struct {
+	Result InfrastructureAccessTarget `json:"result"`
+	Response
+}
+
+type InfrastructureAccessTargetListDetailResponse struct {
+	Result []InfrastructureAccessTarget `json:"result"`
+	Response
+	ResultInfo `json:"result_info"`
+}
+
+type InfrastructureAccessTargetListParams struct {
+	Hostname         string `url:"hostname,omitempty"`
+	HostnameContains string `url:"hostname_contains,omitempty"`
+	IPV4             string `url:"ip_v4,omitempty"`
+	IPV6             string `url:"ip_v6,omitempty"`
+	CreatedAfter     string `url:"created_after,omitempty"`
+	ModifedAfter     string `url:"modified_after,omitempty"`
+	VirtualNetworkId string `url:"virtual_network_id,omitempty"`
+
+	ResultInfo
+}
+
+// ListInfrastructureAccessTargets returns all infrastructure access targets within an account.
+//
+// Account API reference: https://developers.cloudflare.com/api/operations/infra-targets-list
+func (api *API) ListInfrastructureAccessTargets(ctx context.Context, rc *ResourceContainer, params InfrastructureAccessTargetListParams) ([]InfrastructureAccessTarget, *ResultInfo, error) {
+	if rc.Identifier == "" {
+		return []InfrastructureAccessTarget{}, &ResultInfo{}, ErrMissingAccountID
+	}
+
+	baseURL := fmt.Sprintf("/%s/%s/infrastructure/targets", rc.Level, rc.Identifier)
+
+	autoPaginate := true
+	if params.PerPage >= 1 || params.Page >= 1 {
+		autoPaginate = false
+	}
+
+	if params.PerPage < 1 {
+		params.PerPage = 25
+	}
+
+	if params.Page < 1 {
+		params.Page = 1
+	}
+
+	var targets []InfrastructureAccessTarget
+	var r InfrastructureAccessTargetListDetailResponse
+
+	for {
+		uri := buildURI(baseURL, params)
+
+		res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+		if err != nil {
+			return []InfrastructureAccessTarget{}, &ResultInfo{}, fmt.Errorf("%s: %w", errMakeRequestError, err)
+		}
+
+		err = json.Unmarshal(res, &r)
+		if err != nil {
+			return []InfrastructureAccessTarget{}, &ResultInfo{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+		}
+		targets = append(targets, r.Result...)
+		params.ResultInfo = r.ResultInfo.Next()
+		if params.ResultInfo.Done() || !autoPaginate {
+			break
+		}
+	}
+
+	return targets, &r.ResultInfo, nil
+}
+
+// CreateInfrastructureAccessTarget creates a new infrastructure access target.
+//
+// Account API reference: https://developers.cloudflare.com/api/operations/infra-targets-post
+func (api *API) CreateInfrastructureAccessTarget(ctx context.Context, rc *ResourceContainer, params CreateInfrastructureAccessTargetParams) (InfrastructureAccessTarget, error) {
+	if rc.Identifier == "" {
+		return InfrastructureAccessTarget{}, ErrMissingAccountID
+	}
+
+	uri := fmt.Sprintf("/%s/%s/infrastructure/targets", rc.Level, rc.Identifier)
+
+	res, err := api.makeRequestContext(ctx, http.MethodPost, uri, params)
+	if err != nil {
+		return InfrastructureAccessTarget{}, fmt.Errorf("%s: %w", errMakeRequestError, err)
+	}
+
+	var targetDetailResponse InfrastructureAccessTargetDetailResponse
+	err = json.Unmarshal(res, &targetDetailResponse)
+	if err != nil {
+		return InfrastructureAccessTarget{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+
+	return targetDetailResponse.Result, nil
+}
+
+// UpdateInfrastructureAccessTarget updates an existing infrastructure access target.
+//
+// Account API reference: https://developers.cloudflare.com/api/operations/infra-targets-put
+func (api *API) UpdateInfrastructureAccessTarget(ctx context.Context, rc *ResourceContainer, params UpdateInfrastructureAccessTargetParams) (InfrastructureAccessTarget, error) {
+	if rc.Identifier == "" {
+		return InfrastructureAccessTarget{}, ErrMissingAccountID
+	}
+
+	if params.ID == "" {
+		return InfrastructureAccessTarget{}, ErrMissingTargetId
+	}
+
+	uri := fmt.Sprintf(
+		"/%s/%s/infrastructure/targets/%s",
+		rc.Level,
+		rc.Identifier,
+		params.ID,
+	)
+
+	res, err := api.makeRequestContext(ctx, http.MethodPut, uri, params.ModifyParams)
+	if err != nil {
+		return InfrastructureAccessTarget{}, fmt.Errorf("%s: %w", errMakeRequestError, err)
+	}
+
+	var targetDetailResponse InfrastructureAccessTargetDetailResponse
+	err = json.Unmarshal(res, &targetDetailResponse)
+	if err != nil {
+		return InfrastructureAccessTarget{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+
+	return targetDetailResponse.Result, nil
+}
+
+// GetInfrastructureAccessTarget returns a single infrastructure access target based on target ID
+// ID for either account or zone.
+//
+// Account API reference: https://developers.cloudflare.com/api/operations/infra-targets-get
+func (api *API) GetInfrastructureAccessTarget(ctx context.Context, rc *ResourceContainer, targetID string) (InfrastructureAccessTarget, error) {
+	if rc.Identifier == "" {
+		return InfrastructureAccessTarget{}, ErrMissingAccountID
+	}
+
+	if targetID == "" {
+		return InfrastructureAccessTarget{}, ErrMissingTargetId
+	}
+
+	uri := fmt.Sprintf(
+		"/%s/%s/infrastructure/targets/%s",
+		rc.Level,
+		rc.Identifier,
+		targetID,
+	)
+
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return InfrastructureAccessTarget{}, fmt.Errorf("%s: %w", errMakeRequestError, err)
+	}
+
+	var targetDetailResponse InfrastructureAccessTargetDetailResponse
+	err = json.Unmarshal(res, &targetDetailResponse)
+	if err != nil {
+		return InfrastructureAccessTarget{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+
+	return targetDetailResponse.Result, nil
+}
+
+// DeleteInfrastructureAccessTarget deletes an infrastructure access target.
+//
+// Account API reference: https://developers.cloudflare.com/api/operations/infra-targets-delete
+func (api *API) DeleteInfrastructureAccessTarget(ctx context.Context, rc *ResourceContainer, targetID string) error {
+	if rc.Identifier == "" {
+		return ErrMissingAccountID
+	}
+
+	if targetID == "" {
+		return ErrMissingTargetId
+	}
+
+	uri := fmt.Sprintf(
+		"/%s/%s/infrastructure/targets/%s",
+		rc.Level,
+		rc.Identifier,
+		targetID,
+	)
+
+	_, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil)
+	if err != nil {
+		return fmt.Errorf("%s: %w", errMakeRequestError, err)
+	}
+
+	return nil
+}


Test patch:
diff --git a/infrastructure_access_target_test.go b/infrastructure_access_target_test.go
new file mode 100644
index 000000000..b80bcda76
--- /dev/null
+++ b/infrastructure_access_target_test.go
@@ -0,0 +1,286 @@
+package cloudflare
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+const testInfrastructureAccessTargetId = "019205b5-97d7-7272-b00e-0ea05e61a124"
+
+var (
+	infrastructureAccessTargetCreatedOn, _  = time.Parse(time.RFC3339, "2024-08-25T05:00:22Z")
+	infrastructureAccessTargetModifiedOn, _ = time.Parse(time.RFC3339, "2024-08-25T05:00:22Z")
+	expectedInfrastructureAccessTarget      = InfrastructureAccessTarget{
+		Hostname: "infra-access-target",
+		ID:       testInfrastructureAccessTargetId,
+		IP: InfrastructureAccessTargetIPInfo{
+			IPV4: &InfrastructureAccessTargetIPDetails{
+				IPAddr:           "187.26.29.249",
+				VirtualNetworkId: "c77b744e-acc8-428f-9257-6878c046ed55",
+			},
+			IPV6: &InfrastructureAccessTargetIPDetails{
+				IPAddr:           "2001:0db8:0000:0000:0000:0000:0000:1000",
+				VirtualNetworkId: "c77b744e-acc8-428f-9257-6878c046ed55",
+			},
+		},
+		CreatedAt:  infrastructureAccessTargetCreatedOn.String(),
+		ModifiedAt: infrastructureAccessTargetModifiedOn.String(),
+	}
+	expectedInfrastructureModified = InfrastructureAccessTarget{
+		Hostname: "infra-access-target-modified",
+		ID:       testInfrastructureAccessTargetId,
+		IP: InfrastructureAccessTargetIPInfo{
+			IPV4: &InfrastructureAccessTargetIPDetails{
+				IPAddr:           "198.51.100.2",
+				VirtualNetworkId: "c77b744e-acc8-428f-9257-6878c046ed55",
+			},
+		},
+		CreatedAt:  infrastructureAccessTargetCreatedOn.String(),
+		ModifiedAt: infrastructureAccessTargetModifiedOn.String(),
+	}
+)
+
+func TestInfrastructureAccessTarget_Create(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/infrastructure/targets", func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodPost, r.Method, "Expected method 'POST', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `
+			{
+			  "success": true,
+			  "errors": [],
+			  "messages": [],
+			  "result": {
+			 	"created_at": "2024-08-25 05:00:22 +0000 UTC",
+				"hostname": "infra-access-target",
+				"id": "019205b5-97d7-7272-b00e-0ea05e61a124",
+				"ip": {
+				"ipv4": {
+					"ip_addr": "187.26.29.249",
+					"virtual_network_id": "c77b744e-acc8-428f-9257-6878c046ed55"
+				},
+				"ipv6": {
+					"ip_addr": "2001:0db8:0000:0000:0000:0000:0000:1000",
+					"virtual_network_id": "c77b744e-acc8-428f-9257-6878c046ed55"
+				}
+				},
+				"modified_at": "2024-08-25 05:00:22 +0000 UTC"
+			  }
+			}`)
+	})
+
+	// Make sure missing account ID is thrown
+	_, err := client.CreateInfrastructureAccessTarget(context.Background(), AccountIdentifier(""), CreateInfrastructureAccessTargetParams{})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingAccountID, err)
+	}
+
+	out, err := client.CreateInfrastructureAccessTarget(context.Background(), AccountIdentifier(testAccountID), CreateInfrastructureAccessTargetParams{
+		InfrastructureAccessTargetParams: InfrastructureAccessTargetParams{
+			Hostname: "infra-access-target",
+			IP: InfrastructureAccessTargetIPInfo{
+				IPV4: &InfrastructureAccessTargetIPDetails{
+					IPAddr:           "187.26.29.249",
+					VirtualNetworkId: "c77b744e-acc8-428f-9257-6878c046ed55",
+				},
+				IPV6: &InfrastructureAccessTargetIPDetails{
+					IPAddr:           "2001:0db8:0000:0000:0000:0000:0000:1000",
+					VirtualNetworkId: "c77b744e-acc8-428f-9257-6878c046ed55",
+				},
+			},
+		},
+	})
+	if assert.NoError(t, err) {
+		assert.Equal(t, expectedInfrastructureAccessTarget, out, "create infrastructure_access_target structs not equal")
+	}
+}
+
+func TestInfrastructureTarget_List(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/infrastructure/targets", func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `
+{
+  "success": true,
+  "errors": [],
+  "messages": [],
+  "result": [
+    {
+	  "created_at": "2024-08-25 05:00:22 +0000 UTC",
+	  "hostname": "infra-access-target",
+	  "id": "019205b5-97d7-7272-b00e-0ea05e61a124",
+	  "ip": {
+	  	"ipv4": {
+		  "ip_addr": "187.26.29.249",
+		  "virtual_network_id": "c77b744e-acc8-428f-9257-6878c046ed55"
+		},
+		"ipv6": {
+		  "ip_addr": "2001:0db8:0000:0000:0000:0000:0000:1000",
+		  "virtual_network_id": "c77b744e-acc8-428f-9257-6878c046ed55"
+		}
+	  },
+	  "modified_at": "2024-08-25 05:00:22 +0000 UTC"
+    }
+  ],
+  "result_info": {
+    "page": 1,
+    "per_page": 20,
+    "count": 1,
+    "total_count": 1
+  }
+}`)
+	})
+
+	_, _, err := client.ListInfrastructureAccessTargets(context.Background(), AccountIdentifier(""), InfrastructureAccessTargetListParams{})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingAccountID, err)
+	}
+
+	out, results, err := client.ListInfrastructureAccessTargets(context.Background(), AccountIdentifier(testAccountID), InfrastructureAccessTargetListParams{})
+	if assert.NoError(t, err) {
+		assert.Equal(t, 1, len(out), "expected 1 challenge_widgets")
+		assert.Equal(t, 20, results.PerPage, "expected 20 per page")
+		assert.Equal(t, expectedInfrastructureAccessTarget, out[0], "list challenge_widgets structs not equal")
+	}
+}
+
+func TestInfrastructureTarget_Get(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/infrastructure/targets/"+testInfrastructureAccessTargetId, func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `
+{
+  "success": true,
+  "errors": [],
+  "messages": [],
+  "result": {
+	"created_at": "2024-08-25 05:00:22 +0000 UTC",
+    "hostname": "infra-access-target",
+    "id": "019205b5-97d7-7272-b00e-0ea05e61a124",
+    "ip": {
+      "ipv4": {
+        "ip_addr": "187.26.29.249",
+        "virtual_network_id": "c77b744e-acc8-428f-9257-6878c046ed55"
+      },
+      "ipv6": {
+        "ip_addr": "2001:0db8:0000:0000:0000:0000:0000:1000",
+        "virtual_network_id": "c77b744e-acc8-428f-9257-6878c046ed55"
+      }
+    },
+    "modified_at": "2024-08-25 05:00:22 +0000 UTC"
+  }
+}`)
+	})
+
+	_, err := client.GetInfrastructureAccessTarget(context.Background(), AccountIdentifier(""), "")
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingAccountID, err)
+	}
+
+	_, err = client.GetInfrastructureAccessTarget(context.Background(), AccountIdentifier(testAccountID), "")
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingTargetId, err)
+	}
+
+	out, err := client.GetInfrastructureAccessTarget(context.Background(), AccountIdentifier(testAccountID), testInfrastructureAccessTargetId)
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, expectedInfrastructureAccessTarget, out, "get infrastructure_target not equal to expected")
+	}
+}
+
+func TestInfrastructureTarget_Update(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/infrastructure/targets/"+testInfrastructureAccessTargetId, func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodPut, r.Method, "Expected method 'PUT', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `
+{
+  "success": true,
+  "errors": [],
+  "messages": [],
+  "result": {
+	"created_at": "2024-08-25 05:00:22 +0000 UTC",
+    "hostname": "infra-access-target-modified",
+    "id": "019205b5-97d7-7272-b00e-0ea05e61a124",
+    "ip": {
+      "ipv4": {
+        "ip_addr": "198.51.100.2",
+        "virtual_network_id": "c77b744e-acc8-428f-9257-6878c046ed55"
+      }
+    },
+    "modified_at": "2024-08-25 05:00:22 +0000 UTC"
+  }
+}`)
+	})
+
+	_, err := client.UpdateInfrastructureAccessTarget(context.Background(), AccountIdentifier(""), UpdateInfrastructureAccessTargetParams{})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingAccountID, err)
+	}
+
+	_, err = client.UpdateInfrastructureAccessTarget(context.Background(), AccountIdentifier(testAccountID), UpdateInfrastructureAccessTargetParams{
+		ID:           "",
+		ModifyParams: InfrastructureAccessTargetParams{},
+	})
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingTargetId, err)
+	}
+
+	out, err := client.UpdateInfrastructureAccessTarget(context.Background(), AccountIdentifier(testAccountID), UpdateInfrastructureAccessTargetParams{
+		ID: testInfrastructureAccessTargetId,
+		ModifyParams: InfrastructureAccessTargetParams{
+			// Updates hostname and IPv4 address. Deletes IPv6 address.
+			Hostname: "infra-access-target-modified",
+			IP: InfrastructureAccessTargetIPInfo{
+				IPV4: &InfrastructureAccessTargetIPDetails{
+					IPAddr:           "198.51.100.2",
+					VirtualNetworkId: "c77b744e-acc8-428f-9257-6878c046ed55",
+				},
+			},
+		},
+	})
+	if assert.NoError(t, err) {
+		assert.Equal(t, expectedInfrastructureModified, out, "update challenge_widgets structs not equal")
+	}
+}
+
+func TestInfrastructureTarget_Delete(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/infrastructure/targets/"+testInfrastructureAccessTargetId, func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodDelete, r.Method, "Expected method 'DELETE', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, ``)
+	})
+
+	// Make sure missing account ID is thrown
+	err := client.DeleteInfrastructureAccessTarget(context.Background(), AccountIdentifier(""), "")
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingAccountID, err)
+	}
+
+	err = client.DeleteInfrastructureAccessTarget(context.Background(), AccountIdentifier(testAccountID), "")
+	if assert.Error(t, err) {
+		assert.Equal(t, ErrMissingTargetId, err)
+	}
+
+	err = client.DeleteInfrastructureAccessTarget(context.Background(), AccountIdentifier(testAccountID), testInfrastructureAccessTargetId)
+	assert.NoError(t, err)
+}