Skip to content

Commit 292a7f0

Browse files
committed
TUN-3243: Refactor tunnel subcommands to allow commands to compose better
1 parent 679f363 commit 292a7f0

File tree

4 files changed

+379
-279
lines changed

4 files changed

+379
-279
lines changed
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
package tunnel
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io/ioutil"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
11+
"github.com/cloudflare/cloudflared/certutil"
12+
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
13+
"github.com/cloudflare/cloudflared/logger"
14+
"github.com/cloudflare/cloudflared/origin"
15+
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
16+
"github.com/cloudflare/cloudflared/tunnelstore"
17+
"github.com/google/uuid"
18+
"github.com/pkg/errors"
19+
"github.com/urfave/cli/v2"
20+
)
21+
22+
// subcommandContext carries structs shared between subcommands, to reduce number of arguments needed to pass between subcommands,
23+
// and make sure they are only initialized once
24+
type subcommandContext struct {
25+
c *cli.Context
26+
logger logger.Service
27+
28+
// These fields should be accessed using their respective Getter
29+
tunnelstoreClient tunnelstore.Client
30+
userCredential *userCredential
31+
}
32+
33+
func newSubcommandContext(c *cli.Context) (*subcommandContext, error) {
34+
logger, err := createLogger(c, false)
35+
if err != nil {
36+
return nil, errors.Wrap(err, "error setting up logger")
37+
}
38+
return &subcommandContext{
39+
c: c,
40+
logger: logger,
41+
}, nil
42+
}
43+
44+
type userCredential struct {
45+
cert *certutil.OriginCert
46+
certPath string
47+
}
48+
49+
func (sc *subcommandContext) client() (tunnelstore.Client, error) {
50+
if sc.tunnelstoreClient != nil {
51+
return sc.tunnelstoreClient, nil
52+
}
53+
credential, err := sc.credential()
54+
if err != nil {
55+
return nil, err
56+
}
57+
client, err := tunnelstore.NewRESTClient(sc.c.String("api-url"), credential.cert.AccountID, credential.cert.ZoneID, credential.cert.ServiceKey, sc.logger)
58+
if err != nil {
59+
return nil, err
60+
}
61+
sc.tunnelstoreClient = client
62+
return client, nil
63+
}
64+
65+
func (sc *subcommandContext) credential() (*userCredential, error) {
66+
if sc.userCredential == nil {
67+
originCertPath, err := findOriginCert(sc.c, sc.logger)
68+
if err != nil {
69+
return nil, errors.Wrap(err, "Error locating origin cert")
70+
}
71+
blocks, err := readOriginCert(originCertPath, sc.logger)
72+
if err != nil {
73+
return nil, errors.Wrapf(err, "Can't read origin cert from %s", originCertPath)
74+
}
75+
76+
cert, err := certutil.DecodeOriginCert(blocks)
77+
if err != nil {
78+
return nil, errors.Wrap(err, "Error decoding origin cert")
79+
}
80+
81+
if cert.AccountID == "" {
82+
return nil, errors.Errorf(`Origin certificate needs to be refreshed before creating new tunnels.\nDelete %s and run "cloudflared login" to obtain a new cert.`, originCertPath)
83+
}
84+
85+
sc.userCredential = &userCredential{
86+
cert: cert,
87+
certPath: originCertPath,
88+
}
89+
}
90+
return sc.userCredential, nil
91+
}
92+
93+
func (sc *subcommandContext) readTunnelCredentials(tunnelID uuid.UUID) (*pogs.TunnelAuth, error) {
94+
filePath, err := sc.tunnelCredentialsPath(tunnelID)
95+
if err != nil {
96+
return nil, err
97+
}
98+
body, err := ioutil.ReadFile(filePath)
99+
if err != nil {
100+
return nil, errors.Wrapf(err, "couldn't read tunnel credentials from %v", filePath)
101+
}
102+
103+
var auth pogs.TunnelAuth
104+
if err = json.Unmarshal(body, &auth); err != nil {
105+
return nil, err
106+
}
107+
return &auth, nil
108+
}
109+
110+
func (sc *subcommandContext) tunnelCredentialsPath(tunnelID uuid.UUID) (string, error) {
111+
if filePath := sc.c.String("credentials-file"); filePath != "" {
112+
if validFilePath(filePath) {
113+
return filePath, nil
114+
}
115+
}
116+
117+
// Fallback to look for tunnel credentials in the origin cert directory
118+
if originCertPath, err := findOriginCert(sc.c, sc.logger); err == nil {
119+
originCertDir := filepath.Dir(originCertPath)
120+
if filePath, err := tunnelFilePath(tunnelID, originCertDir); err == nil {
121+
if validFilePath(filePath) {
122+
return filePath, nil
123+
}
124+
}
125+
}
126+
127+
// Last resort look under default config directories
128+
for _, configDir := range config.DefaultConfigDirs {
129+
if filePath, err := tunnelFilePath(tunnelID, configDir); err == nil {
130+
if validFilePath(filePath) {
131+
return filePath, nil
132+
}
133+
}
134+
}
135+
return "", fmt.Errorf("Tunnel credentials file not found")
136+
}
137+
138+
func (sc *subcommandContext) create(name string) (*tunnelstore.Tunnel, error) {
139+
client, err := sc.client()
140+
if err != nil {
141+
return nil, err
142+
}
143+
144+
tunnelSecret, err := generateTunnelSecret()
145+
if err != nil {
146+
return nil, err
147+
}
148+
149+
tunnel, err := client.CreateTunnel(name, tunnelSecret)
150+
if err != nil {
151+
return nil, err
152+
}
153+
154+
credential, err := sc.credential()
155+
if err != nil {
156+
return nil, err
157+
}
158+
if writeFileErr := writeTunnelCredentials(tunnel.ID, credential.cert.AccountID, credential.certPath, tunnelSecret, sc.logger); err != nil {
159+
var errorLines []string
160+
errorLines = append(errorLines, fmt.Sprintf("Your tunnel '%v' was created with ID %v. However, cloudflared couldn't write to the tunnel credentials file at %v.json.", tunnel.Name, tunnel.ID, tunnel.ID))
161+
errorLines = append(errorLines, fmt.Sprintf("The file-writing error is: %v", writeFileErr))
162+
if deleteErr := client.DeleteTunnel(tunnel.ID); deleteErr != nil {
163+
errorLines = append(errorLines, fmt.Sprintf("Cloudflared tried to delete the tunnel for you, but encountered an error. You should use `cloudflared tunnel delete %v` to delete the tunnel yourself, because the tunnel can't be run without the tunnelfile.", tunnel.ID))
164+
errorLines = append(errorLines, fmt.Sprintf("The delete tunnel error is: %v", deleteErr))
165+
} else {
166+
errorLines = append(errorLines, fmt.Sprintf("The tunnel was deleted, because the tunnel can't be run without the tunnelfile"))
167+
}
168+
errorMsg := strings.Join(errorLines, "\n")
169+
return nil, errors.New(errorMsg)
170+
}
171+
172+
if outputFormat := sc.c.String(outputFormatFlag.Name); outputFormat != "" {
173+
return nil, renderOutput(outputFormat, &tunnel)
174+
}
175+
176+
sc.logger.Infof("Created tunnel %s with id %s", tunnel.Name, tunnel.ID)
177+
return tunnel, nil
178+
}
179+
180+
func (sc *subcommandContext) list(filter *tunnelstore.Filter) ([]*tunnelstore.Tunnel, error) {
181+
client, err := sc.client()
182+
if err != nil {
183+
return nil, err
184+
}
185+
return client.ListTunnels(filter)
186+
}
187+
188+
func (sc *subcommandContext) delete(tunnelIDs []uuid.UUID) error {
189+
forceFlagSet := sc.c.Bool("force")
190+
191+
client, err := sc.client()
192+
if err != nil {
193+
return err
194+
}
195+
196+
for _, id := range tunnelIDs {
197+
tunnel, err := client.GetTunnel(id)
198+
if err != nil {
199+
return errors.Wrapf(err, "Can't get tunnel information. Please check tunnel id: %s", tunnel.ID)
200+
}
201+
202+
// Check if tunnel DeletedAt field has already been set
203+
if !tunnel.DeletedAt.IsZero() {
204+
return fmt.Errorf("Tunnel %s has already been deleted", tunnel.ID)
205+
}
206+
// Check if tunnel has existing connections and if force flag is set, cleanup connections
207+
if len(tunnel.Connections) > 0 {
208+
if !forceFlagSet {
209+
return fmt.Errorf("You can not delete tunnel %s because it has active connections. To see connections run the 'list' command. If you believe the tunnel is not active, you can use a -f / --force flag with this command.", id)
210+
}
211+
212+
if err := client.CleanupConnections(tunnel.ID); err != nil {
213+
return errors.Wrapf(err, "Error cleaning up connections for tunnel %s", tunnel.ID)
214+
}
215+
}
216+
217+
if err := client.DeleteTunnel(tunnel.ID); err != nil {
218+
return errors.Wrapf(err, "Error deleting tunnel %s", tunnel.ID)
219+
}
220+
221+
tunnelCredentialsPath, err := sc.tunnelCredentialsPath(tunnel.ID)
222+
if err != nil {
223+
sc.logger.Infof("Cannot locate tunnel credentials to delete, error: %v. Please delete the file manually", err)
224+
return nil
225+
}
226+
227+
if err = os.Remove(tunnelCredentialsPath); err != nil {
228+
sc.logger.Infof("Cannot delete tunnel credentials, error: %v. Please delete the file manually", err)
229+
}
230+
}
231+
return nil
232+
}
233+
234+
func (sc *subcommandContext) run(tunnelID uuid.UUID) error {
235+
credentials, err := sc.readTunnelCredentials(tunnelID)
236+
if err != nil {
237+
return err
238+
}
239+
return StartServer(sc.c, version, shutdownC, graceShutdownC, &origin.NamedTunnelConfig{Auth: *credentials, ID: tunnelID})
240+
}
241+
242+
func (sc *subcommandContext) cleanupConnections(tunnelIDs []uuid.UUID) error {
243+
client, err := sc.client()
244+
if err != nil {
245+
return err
246+
}
247+
for _, tunnelID := range tunnelIDs {
248+
sc.logger.Infof("Cleanup connection for tunnel %s", tunnelID)
249+
if err := client.CleanupConnections(tunnelID); err != nil {
250+
sc.logger.Errorf("Error cleaning up connections for tunnel %v, error :%v", tunnelID, err)
251+
}
252+
}
253+
return nil
254+
}
255+
256+
func (sc *subcommandContext) route(tunnelID uuid.UUID, r tunnelstore.Route) error {
257+
client, err := sc.client()
258+
if err != nil {
259+
return err
260+
}
261+
262+
if err := client.RouteTunnel(tunnelID, r); err != nil {
263+
return err
264+
}
265+
266+
return nil
267+
}
268+
269+
func (sc *subcommandContext) tunnelActive(name string) (*tunnelstore.Tunnel, bool, error) {
270+
filter := tunnelstore.NewFilter()
271+
filter.NoDeleted()
272+
filter.ByName(name)
273+
tunnels, err := sc.list(filter)
274+
if err != nil {
275+
return nil, false, err
276+
}
277+
if len(tunnels) == 0 {
278+
return nil, false, nil
279+
}
280+
// There should only be 1 active tunnel for a given name
281+
return tunnels[0], true, nil
282+
}

0 commit comments

Comments
 (0)