Skip to content

Commit 1a96889

Browse files
committed
TUN-3286: Use either ID or name in Named Tunnel subcommands.
1 parent 60de05b commit 1a96889

File tree

5 files changed

+160
-31
lines changed

5 files changed

+160
-31
lines changed

cmd/cloudflared/tunnel/subcommand_context.go

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,17 +138,17 @@ func (sc *subcommandContext) tunnelCredentialsPath(tunnelID uuid.UUID) (string,
138138
func (sc *subcommandContext) create(name string) (*tunnelstore.Tunnel, error) {
139139
client, err := sc.client()
140140
if err != nil {
141-
return nil, err
141+
return nil, errors.Wrap(err, "couldn't create client to talk to Argo Tunnel backend")
142142
}
143143

144144
tunnelSecret, err := generateTunnelSecret()
145145
if err != nil {
146-
return nil, err
146+
return nil, errors.Wrap(err, "couldn't generate the secret for your new tunnel")
147147
}
148148

149149
tunnel, err := client.CreateTunnel(name, tunnelSecret)
150150
if err != nil {
151-
return nil, err
151+
return nil, errors.Wrap(err, "Create Tunnel API call failed")
152152
}
153153

154154
credential, err := sc.credential()
@@ -280,3 +280,61 @@ func (sc *subcommandContext) tunnelActive(name string) (*tunnelstore.Tunnel, boo
280280
// There should only be 1 active tunnel for a given name
281281
return tunnels[0], true, nil
282282
}
283+
284+
// findID parses the input. If it's a UUID, return the UUID.
285+
// Otherwise, assume it's a name, and look up the ID of that tunnel.
286+
func (sc *subcommandContext) findID(input string) (uuid.UUID, error) {
287+
if u, err := uuid.Parse(input); err == nil {
288+
return u, nil
289+
}
290+
291+
if tunnel, found, err := sc.tunnelActive(input); err != nil {
292+
return uuid.Nil, err
293+
} else if found {
294+
return tunnel.ID, nil
295+
}
296+
297+
return uuid.Nil, fmt.Errorf("%s is neither the ID nor the name of any of your tunnels", input)
298+
}
299+
300+
// findIDs is just like mapping `findID` over a slice, but it only uses
301+
// one Tunnelstore API call.
302+
func (sc *subcommandContext) findIDs(inputs []string) ([]uuid.UUID, error) {
303+
304+
// First, look up all tunnels the user has
305+
filter := tunnelstore.NewFilter()
306+
filter.NoDeleted()
307+
tunnels, err := sc.list(filter)
308+
if err != nil {
309+
return nil, err
310+
}
311+
// Do the pure list-processing in its own function, so that it can be
312+
// unit tested easily.
313+
return findIDs(tunnels, inputs)
314+
}
315+
316+
func findIDs(tunnels []*tunnelstore.Tunnel, inputs []string) ([]uuid.UUID, error) {
317+
// Put them into a dictionary for faster lookups
318+
nameToID := make(map[string]uuid.UUID, len(tunnels))
319+
for _, tunnel := range tunnels {
320+
nameToID[tunnel.Name] = tunnel.ID
321+
}
322+
323+
// For each input, try to find the tunnel ID.
324+
tunnelIDs := make([]uuid.UUID, len(inputs))
325+
var badInputs []string
326+
for i, input := range inputs {
327+
if id, err := uuid.Parse(input); err == nil {
328+
tunnelIDs[i] = id
329+
} else if id, ok := nameToID[input]; ok {
330+
tunnelIDs[i] = id
331+
} else {
332+
badInputs = append(badInputs, input)
333+
}
334+
}
335+
if len(badInputs) > 0 {
336+
msg := "Please specify either the ID or name of a tunnel. The following inputs were neither: %s"
337+
return nil, fmt.Errorf(msg, strings.Join(badInputs, ", "))
338+
}
339+
return tunnelIDs, nil
340+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package tunnel
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
7+
"github.com/cloudflare/cloudflared/tunnelstore"
8+
"github.com/google/uuid"
9+
)
10+
11+
func Test_findIDs(t *testing.T) {
12+
type args struct {
13+
tunnels []*tunnelstore.Tunnel
14+
inputs []string
15+
}
16+
tests := []struct {
17+
name string
18+
args args
19+
want []uuid.UUID
20+
wantErr bool
21+
}{
22+
{
23+
name: "input not found",
24+
args: args{
25+
inputs: []string{"asdf"},
26+
},
27+
wantErr: true,
28+
},
29+
{
30+
name: "only UUID",
31+
args: args{
32+
inputs: []string{"a8398a0b-876d-48ed-b609-3fcfd67a4950"},
33+
},
34+
want: []uuid.UUID{uuid.MustParse("a8398a0b-876d-48ed-b609-3fcfd67a4950")},
35+
},
36+
{
37+
name: "only name",
38+
args: args{
39+
tunnels: []*tunnelstore.Tunnel{
40+
{
41+
ID: uuid.MustParse("a8398a0b-876d-48ed-b609-3fcfd67a4950"),
42+
Name: "tunnel1",
43+
},
44+
},
45+
inputs: []string{"tunnel1"},
46+
},
47+
want: []uuid.UUID{uuid.MustParse("a8398a0b-876d-48ed-b609-3fcfd67a4950")},
48+
},
49+
{
50+
name: "both UUID and name",
51+
args: args{
52+
tunnels: []*tunnelstore.Tunnel{
53+
{
54+
ID: uuid.MustParse("a8398a0b-876d-48ed-b609-3fcfd67a4950"),
55+
Name: "tunnel1",
56+
},
57+
{
58+
ID: uuid.MustParse("bf028b68-744f-466e-97f8-c46161d80aa5"),
59+
Name: "tunnel2",
60+
},
61+
},
62+
inputs: []string{"tunnel1", "bf028b68-744f-466e-97f8-c46161d80aa5"},
63+
},
64+
want: []uuid.UUID{
65+
uuid.MustParse("a8398a0b-876d-48ed-b609-3fcfd67a4950"),
66+
uuid.MustParse("bf028b68-744f-466e-97f8-c46161d80aa5"),
67+
},
68+
},
69+
}
70+
for _, tt := range tests {
71+
t.Run(tt.name, func(t *testing.T) {
72+
got, err := findIDs(tt.args.tunnels, tt.args.inputs)
73+
if (err != nil) != tt.wantErr {
74+
t.Errorf("findIDs() error = %v, wantErr %v", err, tt.wantErr)
75+
return
76+
}
77+
if !reflect.DeepEqual(got, tt.want) {
78+
t.Errorf("findIDs() = %v, want %v", got, tt.want)
79+
}
80+
})
81+
}
82+
}

cmd/cloudflared/tunnel/subcommands.go

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -272,30 +272,17 @@ func deleteCommand(c *cli.Context) error {
272272
}
273273

274274
if c.NArg() < 1 {
275-
return cliutil.UsageError(`"cloudflared tunnel delete" requires at least argument, the ID of the tunnel to delete.`)
275+
return cliutil.UsageError(`"cloudflared tunnel delete" requires at least 1 argument, the ID or name of the tunnel to delete.`)
276276
}
277277

278-
tunnelIDs, err := tunnelIDsFromArgs(c)
278+
tunnelIDs, err := sc.findIDs(c.Args().Slice())
279279
if err != nil {
280280
return err
281281
}
282282

283283
return sc.delete(tunnelIDs)
284284
}
285285

286-
func tunnelIDsFromArgs(c *cli.Context) ([]uuid.UUID, error) {
287-
tunnelIDs := make([]uuid.UUID, 0, c.NArg())
288-
for i := 0; i < c.NArg(); i++ {
289-
tunnelID, err := uuid.Parse(c.Args().Get(i))
290-
if err != nil {
291-
return nil, err
292-
}
293-
tunnelIDs = append(tunnelIDs, tunnelID)
294-
}
295-
return tunnelIDs, nil
296-
297-
}
298-
299286
func renderOutput(format string, v interface{}) error {
300287
switch format {
301288
case "json":
@@ -327,7 +314,7 @@ func runCommand(c *cli.Context) error {
327314
}
328315

329316
if c.NArg() != 1 {
330-
return cliutil.UsageError(`"cloudflared tunnel run" requires exactly 1 argument, the ID of the tunnel to run.`)
317+
return cliutil.UsageError(`"cloudflared tunnel run" requires exactly 1 argument, the ID or name of the tunnel to run.`)
331318
}
332319
tunnelID, err := uuid.Parse(c.Args().First())
333320
if err != nil {
@@ -357,7 +344,7 @@ func cleanupCommand(c *cli.Context) error {
357344
return err
358345
}
359346

360-
tunnelIDs, err := tunnelIDsFromArgs(c)
347+
tunnelIDs, err := sc.findIDs(c.Args().Slice())
361348
if err != nil {
362349
return err
363350
}
@@ -417,15 +404,15 @@ func lbRouteFromArg(c *cli.Context, tunnelID uuid.UUID) (tunnelstore.Route, erro
417404

418405
func routeCommand(c *cli.Context) error {
419406
if c.NArg() < 2 {
420-
return cliutil.UsageError(`"cloudflared tunnel route" requires the first argument to be the route type(dns or lb), followed by the ID of the tunnel`)
407+
return cliutil.UsageError(`"cloudflared tunnel route" requires the first argument to be the route type(dns or lb), followed by the ID or name of the tunnel`)
421408
}
422-
const tunnelIDIndex = 1
423-
tunnelID, err := uuid.Parse(c.Args().Get(tunnelIDIndex))
409+
sc, err := newSubcommandContext(c)
424410
if err != nil {
425-
return errors.Wrap(err, "error parsing tunnel ID")
411+
return err
426412
}
427413

428-
sc, err := newSubcommandContext(c)
414+
const tunnelIDIndex = 1
415+
tunnelID, err := sc.findID(c.Args().Get(tunnelIDIndex))
429416
if err != nil {
430417
return err
431418
}

h2mux/header_test.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -372,18 +372,18 @@ func randomASCIIPrintableChar(rand *rand.Rand) int {
372372
// between 1 and `maxLength`.
373373
func randomASCIIText(rand *rand.Rand, minLength int, maxLength int) string {
374374
length := minLength + rand.Intn(maxLength)
375-
result := ""
375+
var result strings.Builder
376376
for i := 0; i < length; i++ {
377377
c := randomASCIIPrintableChar(rand)
378378

379379
// 1/4 chance of using percent encoding when not necessary
380380
if c == '%' || rand.Intn(4) == 0 {
381-
result += fmt.Sprintf("%%%02X", c)
381+
result.WriteString(fmt.Sprintf("%%%02X", c))
382382
} else {
383-
result += string(c)
383+
result.WriteByte(byte(c))
384384
}
385385
}
386-
return result
386+
return result.String()
387387
}
388388

389389
// Calls `randomASCIIText` and ensures the result is a valid URL path,
@@ -663,7 +663,7 @@ func BenchmarkH1ResponseToH2ResponseHeaders(b *testing.B) {
663663

664664
h1resp := &http.Response{
665665
StatusCode: 200,
666-
Header: h1,
666+
Header: h1,
667667
}
668668

669669
b.ReportAllocs()
@@ -672,4 +672,3 @@ func BenchmarkH1ResponseToH2ResponseHeaders(b *testing.B) {
672672
_ = H1ResponseToH2ResponseHeaders(h1resp)
673673
}
674674
}
675-

tunnelstore/client.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,9 @@ func (r *RESTClient) CreateTunnel(name string, tunnelSecret []byte) (*Tunnel, er
174174
if name == "" {
175175
return nil, errors.New("tunnel name required")
176176
}
177+
if _, err := uuid.Parse(name); err == nil {
178+
return nil, errors.New("you cannot use UUIDs as tunnel names")
179+
}
177180
body := &newTunnel{
178181
Name: name,
179182
TunnelSecret: tunnelSecret,

0 commit comments

Comments
 (0)