From 059f4d9898bde183ac1863afd02e1786974507dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Garcia?= Date: Fri, 20 Feb 2026 15:40:25 +0000 Subject: [PATCH] TUN-10247: Update tail command to use /management/logs endpoint * TUN-10247: Update tail command to use /management/logs endpoint The /management endpoint will be deprecated in favor of new /management/resource endpoints. Because of that, we'll need cloudflared to use the new endpoint. Closes TUN-10247 --- cfapi/client.go | 2 +- cfapi/tunnel.go | 34 +++++++++++++++------- cfapi/tunnel_test.go | 58 ++++++++++++++++++++++++++++++++----- cmd/cloudflared/tail/cmd.go | 14 +++++---- 4 files changed, 83 insertions(+), 25 deletions(-) diff --git a/cfapi/client.go b/cfapi/client.go index 4b7e3508..1d08cc50 100644 --- a/cfapi/client.go +++ b/cfapi/client.go @@ -8,7 +8,7 @@ type TunnelClient interface { CreateTunnel(name string, tunnelSecret []byte) (*TunnelWithToken, error) GetTunnel(tunnelID uuid.UUID) (*Tunnel, error) GetTunnelToken(tunnelID uuid.UUID) (string, error) - GetManagementToken(tunnelID uuid.UUID) (string, error) + GetManagementToken(tunnelID uuid.UUID, resource ManagementResource) (string, error) DeleteTunnel(tunnelID uuid.UUID, cascade bool) error ListTunnels(filter *TunnelFilter) ([]*Tunnel, error) ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error) diff --git a/cfapi/tunnel.go b/cfapi/tunnel.go index dc80c6a1..539b2051 100644 --- a/cfapi/tunnel.go +++ b/cfapi/tunnel.go @@ -15,6 +15,21 @@ import ( var ErrTunnelNameConflict = errors.New("tunnel with name already exists") +type ManagementResource int + +const ( + Logs ManagementResource = iota +) + +func (r ManagementResource) String() string { + switch r { + case Logs: + return "logs" + default: + return "" + } +} + type Tunnel struct { ID uuid.UUID `json:"id"` Name string `json:"name"` @@ -50,10 +65,6 @@ type newTunnel struct { TunnelSecret []byte `json:"tunnel_secret"` } -type managementRequest struct { - Resources []string `json:"resources"` -} - type CleanupParams struct { queryParams url.Values } @@ -137,15 +148,16 @@ func (r *RESTClient) GetTunnelToken(tunnelID uuid.UUID) (token string, err error return "", r.statusCodeToError("get tunnel token", resp) } -func (r *RESTClient) GetManagementToken(tunnelID uuid.UUID) (token string, err error) { +// managementEndpointPath returns the path segment for a management resource endpoint +func managementEndpointPath(tunnelID uuid.UUID, res ManagementResource) string { + return fmt.Sprintf("%v/management/%s", tunnelID, res.String()) +} + +func (r *RESTClient) GetManagementToken(tunnelID uuid.UUID, res ManagementResource) (token string, err error) { endpoint := r.baseEndpoints.accountLevel - endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/management", tunnelID)) + endpoint.Path = path.Join(endpoint.Path, managementEndpointPath(tunnelID, res)) - body := &managementRequest{ - Resources: []string{"logs"}, - } - - resp, err := r.sendRequest("POST", endpoint, body) + resp, err := r.sendRequest("POST", endpoint, nil) if err != nil { return "", errors.Wrap(err, "REST request failed") } diff --git a/cfapi/tunnel_test.go b/cfapi/tunnel_test.go index 2c012825..da2c7f9f 100644 --- a/cfapi/tunnel_test.go +++ b/cfapi/tunnel_test.go @@ -2,7 +2,6 @@ package cfapi import ( "bytes" - "fmt" "net" "reflect" "strings" @@ -11,6 +10,7 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var loc, _ = time.LoadLocation("UTC") @@ -52,7 +52,6 @@ func Test_unmarshalTunnel(t *testing.T) { } func TestUnmarshalTunnelOk(t *testing.T) { - jsonBody := `{"success": true, "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}` expected := Tunnel{ ID: uuid.Nil, @@ -61,12 +60,11 @@ func TestUnmarshalTunnelOk(t *testing.T) { Connections: []Connection{}, } actual, err := unmarshalTunnel(bytes.NewReader([]byte(jsonBody))) - assert.NoError(t, err) - assert.Equal(t, &expected, actual) + require.NoError(t, err) + require.Equal(t, &expected, actual) } func TestUnmarshalTunnelErr(t *testing.T) { - tests := []string{ `abc`, `{"success": true, "result": abc}`, @@ -76,7 +74,53 @@ func TestUnmarshalTunnelErr(t *testing.T) { for i, test := range tests { _, err := unmarshalTunnel(bytes.NewReader([]byte(test))) - assert.Error(t, err, fmt.Sprintf("Test #%v failed", i)) + assert.Error(t, err, "Test #%v failed", i) + } +} + +func TestManagementResource_String(t *testing.T) { + tests := []struct { + name string + resource ManagementResource + want string + }{ + { + name: "Logs", + resource: Logs, + want: "logs", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, tt.resource.String()) + }) + } +} + +func TestManagementResource_String_Unknown(t *testing.T) { + unknown := ManagementResource(999) + assert.Equal(t, "", unknown.String()) +} + +func TestManagementEndpointPath(t *testing.T) { + tunnelID := uuid.MustParse("b34cc7ce-925b-46ee-bc23-4cb5c18d8292") + + tests := []struct { + name string + resource ManagementResource + want string + }{ + { + name: "Logs resource", + resource: Logs, + want: "b34cc7ce-925b-46ee-bc23-4cb5c18d8292/management/logs", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := managementEndpointPath(tunnelID, tt.resource) + assert.Equal(t, tt.want, got) + }) } } @@ -97,6 +141,6 @@ func TestUnmarshalConnections(t *testing.T) { }}, } actual, err := parseConnectionsDetails(bytes.NewReader([]byte(jsonBody))) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, []*ActiveClient{&expected}, actual) } diff --git a/cmd/cloudflared/tail/cmd.go b/cmd/cloudflared/tail/cmd.go index 9b2ee6a5..4cfd744e 100644 --- a/cmd/cloudflared/tail/cmd.go +++ b/cmd/cloudflared/tail/cmd.go @@ -18,6 +18,8 @@ import ( "github.com/urfave/cli/v2" "nhooyr.io/websocket" + "github.com/cloudflare/cloudflared/cfapi" + "github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil" cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags" "github.com/cloudflare/cloudflared/credentials" @@ -52,7 +54,7 @@ func buildTailManagementTokenSubcommand() *cli.Command { func managementTokenCommand(c *cli.Context) error { log := createLogger(c) - token, err := getManagementToken(c, log) + token, err := getManagementToken(c, log, cfapi.Logs) if err != nil { return err } @@ -231,7 +233,7 @@ func parseFilters(c *cli.Context) (*management.StreamingFilters, error) { } // getManagementToken will make a call to the Cloudflare API to acquire a management token for the requested tunnel. -func getManagementToken(c *cli.Context, log *zerolog.Logger) (string, error) { +func getManagementToken(c *cli.Context, log *zerolog.Logger, res cfapi.ManagementResource) (string, error) { userCreds, err := credentials.Read(c.String(cfdflags.OriginCert), log) if err != nil { return "", err @@ -258,7 +260,7 @@ func getManagementToken(c *cli.Context, log *zerolog.Logger) (string, error) { return "", errors.New("unable to parse provided tunnel id as a valid UUID") } - token, err := client.GetManagementToken(tunnelID) + token, err := client.GetManagementToken(tunnelID, res) if err != nil { return "", err } @@ -267,12 +269,12 @@ func getManagementToken(c *cli.Context, log *zerolog.Logger) (string, error) { } // buildURL will build the management url to contain the required query parameters to authenticate the request. -func buildURL(c *cli.Context, log *zerolog.Logger) (url.URL, error) { +func buildURL(c *cli.Context, log *zerolog.Logger, res cfapi.ManagementResource) (url.URL, error) { var err error token := c.String("token") if token == "" { - token, err = getManagementToken(c, log) + token, err = getManagementToken(c, log, res) if err != nil { return url.URL{}, fmt.Errorf("unable to acquire management token for requested tunnel id: %w", err) } @@ -345,7 +347,7 @@ func Run(c *cli.Context) error { return nil } - u, err := buildURL(c, log) + u, err := buildURL(c, log, cfapi.Logs) if err != nil { log.Err(err).Msg("unable to construct management request URL") return nil