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
This commit is contained in:
Gonçalo Garcia 2026-02-20 15:40:25 +00:00
parent a0bcbf6a44
commit 059f4d9898
4 changed files with 83 additions and 25 deletions

View File

@ -8,7 +8,7 @@ type TunnelClient interface {
CreateTunnel(name string, tunnelSecret []byte) (*TunnelWithToken, error) CreateTunnel(name string, tunnelSecret []byte) (*TunnelWithToken, error)
GetTunnel(tunnelID uuid.UUID) (*Tunnel, error) GetTunnel(tunnelID uuid.UUID) (*Tunnel, error)
GetTunnelToken(tunnelID uuid.UUID) (string, 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 DeleteTunnel(tunnelID uuid.UUID, cascade bool) error
ListTunnels(filter *TunnelFilter) ([]*Tunnel, error) ListTunnels(filter *TunnelFilter) ([]*Tunnel, error)
ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error) ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error)

View File

@ -15,6 +15,21 @@ import (
var ErrTunnelNameConflict = errors.New("tunnel with name already exists") 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 { type Tunnel struct {
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
Name string `json:"name"` Name string `json:"name"`
@ -50,10 +65,6 @@ type newTunnel struct {
TunnelSecret []byte `json:"tunnel_secret"` TunnelSecret []byte `json:"tunnel_secret"`
} }
type managementRequest struct {
Resources []string `json:"resources"`
}
type CleanupParams struct { type CleanupParams struct {
queryParams url.Values 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) 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 := 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{ resp, err := r.sendRequest("POST", endpoint, nil)
Resources: []string{"logs"},
}
resp, err := r.sendRequest("POST", endpoint, body)
if err != nil { if err != nil {
return "", errors.Wrap(err, "REST request failed") return "", errors.Wrap(err, "REST request failed")
} }

View File

@ -2,7 +2,6 @@ package cfapi
import ( import (
"bytes" "bytes"
"fmt"
"net" "net"
"reflect" "reflect"
"strings" "strings"
@ -11,6 +10,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
var loc, _ = time.LoadLocation("UTC") var loc, _ = time.LoadLocation("UTC")
@ -52,7 +52,6 @@ func Test_unmarshalTunnel(t *testing.T) {
} }
func TestUnmarshalTunnelOk(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":[]}}` jsonBody := `{"success": true, "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}`
expected := Tunnel{ expected := Tunnel{
ID: uuid.Nil, ID: uuid.Nil,
@ -61,12 +60,11 @@ func TestUnmarshalTunnelOk(t *testing.T) {
Connections: []Connection{}, Connections: []Connection{},
} }
actual, err := unmarshalTunnel(bytes.NewReader([]byte(jsonBody))) actual, err := unmarshalTunnel(bytes.NewReader([]byte(jsonBody)))
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, &expected, actual) require.Equal(t, &expected, actual)
} }
func TestUnmarshalTunnelErr(t *testing.T) { func TestUnmarshalTunnelErr(t *testing.T) {
tests := []string{ tests := []string{
`abc`, `abc`,
`{"success": true, "result": abc}`, `{"success": true, "result": abc}`,
@ -76,7 +74,53 @@ func TestUnmarshalTunnelErr(t *testing.T) {
for i, test := range tests { for i, test := range tests {
_, err := unmarshalTunnel(bytes.NewReader([]byte(test))) _, 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))) actual, err := parseConnectionsDetails(bytes.NewReader([]byte(jsonBody)))
assert.NoError(t, err) require.NoError(t, err)
assert.Equal(t, []*ActiveClient{&expected}, actual) assert.Equal(t, []*ActiveClient{&expected}, actual)
} }

View File

@ -18,6 +18,8 @@ import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"nhooyr.io/websocket" "nhooyr.io/websocket"
"github.com/cloudflare/cloudflared/cfapi"
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil" "github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags" cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
"github.com/cloudflare/cloudflared/credentials" "github.com/cloudflare/cloudflared/credentials"
@ -52,7 +54,7 @@ func buildTailManagementTokenSubcommand() *cli.Command {
func managementTokenCommand(c *cli.Context) error { func managementTokenCommand(c *cli.Context) error {
log := createLogger(c) log := createLogger(c)
token, err := getManagementToken(c, log) token, err := getManagementToken(c, log, cfapi.Logs)
if err != nil { if err != nil {
return err 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. // 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) userCreds, err := credentials.Read(c.String(cfdflags.OriginCert), log)
if err != nil { if err != nil {
return "", err 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") 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 { if err != nil {
return "", err 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. // 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 var err error
token := c.String("token") token := c.String("token")
if token == "" { if token == "" {
token, err = getManagementToken(c, log) token, err = getManagementToken(c, log, res)
if err != nil { if err != nil {
return url.URL{}, fmt.Errorf("unable to acquire management token for requested tunnel id: %w", err) 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 return nil
} }
u, err := buildURL(c, log) u, err := buildURL(c, log, cfapi.Logs)
if err != nil { if err != nil {
log.Err(err).Msg("unable to construct management request URL") log.Err(err).Msg("unable to construct management request URL")
return nil return nil