Skip to content

Commit 6578659

Browse files
committed
TUN-8732: implement port selection algorithm
## Summary Implements the discovery of the metrics server given an array of addresses (known addresses), tunnelID, and connectorID. Closes TUN-8732
1 parent f884b29 commit 6578659

File tree

3 files changed

+222
-0
lines changed

3 files changed

+222
-0
lines changed

diagnostic/diagnostic_utils.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@ package diagnostic
22

33
import (
44
"archive/zip"
5+
"context"
56
"fmt"
67
"io"
8+
"net/url"
79
"os"
810
"path/filepath"
911
"strings"
1012
"time"
13+
14+
"github.com/google/uuid"
15+
"github.com/rs/zerolog"
1116
)
1217

1318
// CreateDiagnosticZipFile create a zip file with the contents from the all
@@ -67,3 +72,69 @@ func CreateDiagnosticZipFile(base string, paths []string) (zipFileName string, e
6772
zipFileName = archive.Name()
6873
return zipFileName, nil
6974
}
75+
76+
type AddressableTunnelState struct {
77+
*TunnelState
78+
URL *url.URL
79+
}
80+
81+
func findMetricsServerPredicate(tunnelID, connectorID uuid.UUID) func(state *TunnelState) bool {
82+
if tunnelID != uuid.Nil && connectorID != uuid.Nil {
83+
return func(state *TunnelState) bool {
84+
return state.ConnectorID == connectorID && state.TunnelID == tunnelID
85+
}
86+
} else if tunnelID == uuid.Nil && connectorID != uuid.Nil {
87+
return func(state *TunnelState) bool {
88+
return state.ConnectorID == connectorID
89+
}
90+
} else if tunnelID != uuid.Nil && connectorID == uuid.Nil {
91+
return func(state *TunnelState) bool {
92+
return state.TunnelID == tunnelID
93+
}
94+
}
95+
96+
return func(*TunnelState) bool {
97+
return true
98+
}
99+
}
100+
101+
// The FindMetricsServer will try to find the metrics server url.
102+
// There are two possible error scenarios:
103+
// 1. No instance is found which will only return ErrMetricsServerNotFound
104+
// 2. Multiple instances are found which will return an array of state and ErrMultipleMetricsServerFound
105+
// In case of success, only the state for the instance is returned.
106+
func FindMetricsServer(
107+
log *zerolog.Logger,
108+
client *httpClient,
109+
addresses []string,
110+
) (*AddressableTunnelState, []*AddressableTunnelState, error) {
111+
instances := make([]*AddressableTunnelState, 0)
112+
113+
for _, address := range addresses {
114+
url, err := url.Parse("http://" + address)
115+
if err != nil {
116+
log.Debug().Err(err).Msgf("error parsing address %s", address)
117+
118+
continue
119+
}
120+
121+
client.SetBaseURL(url)
122+
123+
state, err := client.GetTunnelState(context.Background())
124+
if err == nil {
125+
instances = append(instances, &AddressableTunnelState{state, url})
126+
} else {
127+
log.Debug().Err(err).Msgf("error getting tunnel state from address %s", address)
128+
}
129+
}
130+
131+
if len(instances) == 0 {
132+
return nil, nil, ErrMetricsServerNotFound
133+
}
134+
135+
if len(instances) == 1 {
136+
return instances[0], nil, nil
137+
}
138+
139+
return nil, instances, ErrMultipleMetricsServerFound
140+
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package diagnostic_test
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/url"
7+
"sync"
8+
"testing"
9+
"time"
10+
11+
"github.com/facebookgo/grace/gracenet"
12+
"github.com/google/uuid"
13+
"github.com/rs/zerolog"
14+
"github.com/stretchr/testify/assert"
15+
"github.com/stretchr/testify/require"
16+
17+
"github.com/cloudflare/cloudflared/diagnostic"
18+
"github.com/cloudflare/cloudflared/metrics"
19+
"github.com/cloudflare/cloudflared/tunnelstate"
20+
)
21+
22+
func helperCreateServer(t *testing.T, listeners *gracenet.Net, tunnelID uuid.UUID, connectorID uuid.UUID) func() {
23+
t.Helper()
24+
listener, err := metrics.CreateMetricsListener(listeners, "localhost:0")
25+
require.NoError(t, err)
26+
log := zerolog.Nop()
27+
tracker := tunnelstate.NewConnTracker(&log)
28+
handler := diagnostic.NewDiagnosticHandler(&log, 0, nil, tunnelID, connectorID, tracker, nil, []string{})
29+
router := http.NewServeMux()
30+
router.HandleFunc("/diag/tunnel", handler.TunnelStateHandler)
31+
server := &http.Server{
32+
ReadTimeout: 10 * time.Second,
33+
WriteTimeout: 10 * time.Second,
34+
Handler: router,
35+
}
36+
37+
var wgroup sync.WaitGroup
38+
39+
wgroup.Add(1)
40+
41+
go func() {
42+
defer wgroup.Done()
43+
44+
_ = server.Serve(listener)
45+
}()
46+
47+
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
48+
49+
cleanUp := func() {
50+
_ = server.Shutdown(ctx)
51+
52+
cancel()
53+
wgroup.Wait()
54+
}
55+
56+
return cleanUp
57+
}
58+
59+
func TestFindMetricsServer_WhenSingleServerIsRunning_ReturnState(t *testing.T) {
60+
listeners := gracenet.Net{}
61+
tid1 := uuid.New()
62+
cid1 := uuid.New()
63+
64+
cleanUp := helperCreateServer(t, &listeners, tid1, cid1)
65+
defer cleanUp()
66+
67+
log := zerolog.Nop()
68+
client := diagnostic.NewHTTPClient()
69+
addresses := metrics.GetMetricsKnownAddresses("host")
70+
url1, err := url.Parse("http://localhost:20241")
71+
require.NoError(t, err)
72+
73+
tunnel1 := &diagnostic.AddressableTunnelState{
74+
TunnelState: &diagnostic.TunnelState{
75+
TunnelID: tid1,
76+
ConnectorID: cid1,
77+
Connections: nil,
78+
},
79+
URL: url1,
80+
}
81+
82+
state, tunnels, err := diagnostic.FindMetricsServer(&log, client, addresses[:])
83+
if err != nil {
84+
require.ErrorIs(t, err, diagnostic.ErrMultipleMetricsServerFound)
85+
}
86+
87+
assert.Equal(t, tunnel1, state)
88+
assert.Nil(t, tunnels)
89+
}
90+
91+
func TestFindMetricsServer_WhenMultipleServerAreRunning_ReturnError(t *testing.T) {
92+
listeners := gracenet.Net{}
93+
tid1 := uuid.New()
94+
cid1 := uuid.New()
95+
cid2 := uuid.New()
96+
97+
cleanUp := helperCreateServer(t, &listeners, tid1, cid1)
98+
defer cleanUp()
99+
100+
cleanUp = helperCreateServer(t, &listeners, tid1, cid2)
101+
defer cleanUp()
102+
103+
log := zerolog.Nop()
104+
client := diagnostic.NewHTTPClient()
105+
addresses := metrics.GetMetricsKnownAddresses("host")
106+
url1, err := url.Parse("http://localhost:20241")
107+
require.NoError(t, err)
108+
url2, err := url.Parse("http://localhost:20242")
109+
require.NoError(t, err)
110+
111+
tunnel1 := &diagnostic.AddressableTunnelState{
112+
TunnelState: &diagnostic.TunnelState{
113+
TunnelID: tid1,
114+
ConnectorID: cid1,
115+
Connections: nil,
116+
},
117+
URL: url1,
118+
}
119+
tunnel2 := &diagnostic.AddressableTunnelState{
120+
TunnelState: &diagnostic.TunnelState{
121+
TunnelID: tid1,
122+
ConnectorID: cid2,
123+
Connections: nil,
124+
},
125+
URL: url2,
126+
}
127+
128+
state, tunnels, err := diagnostic.FindMetricsServer(&log, client, addresses[:])
129+
if err != nil {
130+
require.ErrorIs(t, err, diagnostic.ErrMultipleMetricsServerFound)
131+
}
132+
133+
assert.Nil(t, state)
134+
assert.Equal(t, []*diagnostic.AddressableTunnelState{tunnel1, tunnel2}, tunnels)
135+
}
136+
137+
func TestFindMetricsServer_WhenNoInstanceIsRuning_ReturnError(t *testing.T) {
138+
log := zerolog.Nop()
139+
client := diagnostic.NewHTTPClient()
140+
addresses := metrics.GetMetricsKnownAddresses("host")
141+
142+
state, tunnels, err := diagnostic.FindMetricsServer(&log, client, addresses[:])
143+
require.ErrorIs(t, err, diagnostic.ErrMetricsServerNotFound)
144+
145+
assert.Nil(t, state)
146+
assert.Nil(t, tunnels)
147+
}

diagnostic/error.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,8 @@ var (
1919
ErrNoVolumeFound = errors.New("no disk volume information found")
2020
// Error user when the base url of the diagnostic client is not provided.
2121
ErrNoBaseUrl = errors.New("no base url")
22+
// Error used when no metrics server is found listening to the known addresses list (check [metrics.GetMetricsKnownAddresses])
23+
ErrMetricsServerNotFound = errors.New("metrics server not found")
24+
// Error used when multiple metrics server are found listening to the known addresses list (check [metrics.GetMetricsKnownAddresses])
25+
ErrMultipleMetricsServerFound = errors.New("multiple metrics server found")
2226
)

0 commit comments

Comments
 (0)