Repository: cloudflare/cloudflare-go (go)

Issue:
Add API method to get the connections of a tunnel
### Current cloudflare-go version

0.43.0

### Description

This allows us to get the connections of a particular tunnel. Essentially, its a Go wrapper for the following REST endpoint
`GET accounts/:account_identifier/cfd_tunnel/:tunnel_id/connections`


### Use cases

This endpoint is not documented in here https://api.cloudflare.com/#cloudflare-tunnel-properties but I found this endpoint getting used in [cloudflared](https://github.com/cloudflare/cloudflared/blob/d2cb80333652cb24f7d128e81485a195a300f2ad/cfapi/tunnel.go#L170). This endpoint gives more data than the `Connections` property returned by `cloudflare.Tunnel()` and hence is very useful. Especially, it provides the agent architecture. Moreover, it provides the connections as a sub-array of each agent which can be very useful.

### Potential cloudflare-go usage

```go
func (api *API) TunnelConnections(ctx context.Context, params TunnelConnectionParams) ([]Connection, error) {
	if params.AccountID == "" {
		return []Connection{}, ErrMissingAccountID
	}

	if params.ID == "" {
		return []Connection{}, errors.New("missing tunnel ID")
	}

	uri := fmt.Sprintf("/accounts/%s/cfd_tunnel/%s/connections", params.AccountID, params.ID)

	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
	if err != nil {
		return []Connection{}, err
	}

	var argoDetailsResponse TunnelConnectionResponse
	err = json.Unmarshal(res, &argoDetailsResponse)
	if err != nil {
		return []Connection{}, errors.Wrap(err, errUnmarshalError)
	}
	return argoDetailsResponse.Result, nil
}
```


### References
Providing a PR for this #971 

Pull request:
Add Tunnel API method to get connections for a tunnel
## Description

Closes #970 

## Has your change been tested?

Not yet

## 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.
- [x] 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.
- [ ] This change is using publicly documented (api.cloudflare.com or developers.cloudflare.com) and stable APIs.

[1]: https://help.github.com/articles/closing-issues-using-keywords/


Patch:
diff --git a/tunnel.go b/tunnel.go
index c9f7a7cab..be55dff84 100644
--- a/tunnel.go
+++ b/tunnel.go
@@ -26,6 +26,16 @@ type Tunnel struct {
 	ConnInactiveAt *time.Time         `json:"conns_inactive_at,omitempty"`
 }
 
+// Connection is the struct definition of a connection.
+type Connection struct {
+	ID          string             `json:"id,omitempty"`
+	Features    []string           `json:"features,omitempty"`
+	Version     string             `json:"version,omitempty"`
+	Arch        string             `json:"arch,omitempty"`
+	Connections []TunnelConnection `json:"conns,omitempty"`
+	RunAt       *time.Time         `json:"run_at,omitempty"`
+}
+
 // TunnelConnection represents the connections associated with a tunnel.
 type TunnelConnection struct {
 	ColoName           string `json:"colo_name"`
@@ -51,6 +61,13 @@ type TunnelDetailResponse struct {
 	Response
 }
 
+// TunnelConnectionResponse is used for representing the API response payload for
+// connections of a single tunnel.
+type TunnelConnectionResponse struct {
+	Result []Connection `json:"result"`
+	Response
+}
+
 // TunnelConfigurationStringifiedConfigResult is the raw result from the API with
 // the `Config` value as a string. You probably don't want to use this anywhere
 // other than in the HTTP response unmarshaling. Use `TunnelConfigurationResult`
@@ -379,6 +396,33 @@ func (api *API) GetTunnelConfiguration(ctx context.Context, rc *ResourceContaine
 	return tunnelDetails, nil
 }
 
+// TunnelConnections gets all connections on a tunnel.
+//
+// API reference: https://api.cloudflare.com/#cloudflare-tunnel-list-cloudflare-tunnel-connections
+func (api *API) TunnelConnections(ctx context.Context, rc *ResourceContainer, tunnelID string) ([]Connection, error) {
+	if rc.Identifier == "" {
+		return []Connection{}, ErrMissingAccountID
+	}
+
+	if tunnelID == "" {
+		return []Connection{}, ErrMissingTunnelID
+	}
+
+	uri := fmt.Sprintf("/accounts/%s/cfd_tunnel/%s/connections", rc.Identifier, tunnelID)
+
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return []Connection{}, err
+	}
+
+	var argoDetailsResponse TunnelConnectionResponse
+	err = json.Unmarshal(res, &argoDetailsResponse)
+	if err != nil {
+		return []Connection{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+	return argoDetailsResponse.Result, nil
+}
+
 // DeleteTunnel removes a single Argo tunnel.
 //
 // API reference: https://api.cloudflare.com/#cloudflare-tunnel-delete-cloudflare-tunnel


Test patch:
diff --git a/tunnel_test.go b/tunnel_test.go
index 9fa6089cc..cee1ec6d7 100644
--- a/tunnel_test.go
+++ b/tunnel_test.go
@@ -308,6 +308,76 @@ func TestGetTunnelConfiguration(t *testing.T) {
 	}
 }
 
+func TestTunnelConnections(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := 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.Fprintf(w, `{
+			"success":true,
+			"errors":[],
+			"messages":[],
+			"result":[{
+				"id":"dc6472cc-f1ae-44a0-b795-6b8a0ce29f90",
+				"features": [
+					"allow_remote_config",
+					"serialized_headers",
+					"ha-origin"
+				],
+				"version": "2022.2.0",
+            	"arch": "linux_amd64",
+				"run_at":"2009-11-10T23:00:00Z",
+				"conns": [
+					{
+						"colo_name": "DFW",
+						"id": "f174e90a-fafe-4643-bbbc-4a0ed4fc8415",
+						"is_pending_reconnect": false,
+						"client_id": "dc6472cc-f1ae-44a0-b795-6b8a0ce29f90",
+						"client_version": "2022.2.0",
+						"opened_at": "2021-01-25T18:22:34.317854Z",
+						"origin_ip": "85.12.78.6"
+					}
+				]
+			}]
+		}
+		`)
+	}
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/cfd_tunnel/f174e90a-fafe-4643-bbbc-4a0ed4fc8415/connections", handler)
+
+	runAt, _ := time.Parse(time.RFC3339, "2009-11-10T23:00:00Z")
+	want := []Connection{
+		{
+			ID: "dc6472cc-f1ae-44a0-b795-6b8a0ce29f90",
+			Features: []string{
+				"allow_remote_config",
+				"serialized_headers",
+				"ha-origin",
+			},
+			Version: "2022.2.0",
+			Arch:    "linux_amd64",
+			RunAt:   &runAt,
+			Connections: []TunnelConnection{{
+				ColoName:           "DFW",
+				ID:                 "f174e90a-fafe-4643-bbbc-4a0ed4fc8415",
+				IsPendingReconnect: false,
+				ClientID:           "dc6472cc-f1ae-44a0-b795-6b8a0ce29f90",
+				ClientVersion:      "2022.2.0",
+				OpenedAt:           "2021-01-25T18:22:34.317854Z",
+				OriginIP:           "85.12.78.6",
+			}},
+		},
+	}
+
+	actual, err := client.TunnelConnections(context.Background(), AccountIdentifier(testAccountID), "f174e90a-fafe-4643-bbbc-4a0ed4fc8415")
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+}
+
 func TestDeleteTunnel(t *testing.T) {
 	setup()
 	defer teardown()