diff --git a/.changelog/3184.txt b/.changelog/3184.txt new file mode 100644 index 00000000000..e0c858c3517 --- /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 00000000000..0c6a72a8666 --- /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 +} diff --git a/infrastructure_access_target_test.go b/infrastructure_access_target_test.go new file mode 100644 index 00000000000..b80bcda76b0 --- /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) +}