TUN-3295: Show route command results

This commit is contained in:
Igor Postelnik 2020-09-17 15:19:47 -05:00
parent 20623255dd
commit 85d0afd3b0
5 changed files with 179 additions and 24 deletions

View File

@ -244,10 +244,10 @@ func adhocNamedTunnel(c *cli.Context, name string) error {
} }
if r, ok := routeFromFlag(c); ok { if r, ok := routeFromFlag(c); ok {
if err := sc.route(tunnel.ID, r); err != nil { if res, err := sc.route(tunnel.ID, r); err != nil {
sc.logger.Errorf("failed to create route, please create it manually. err: %v.", err) sc.logger.Errorf("failed to create route, please create it manually. err: %v.", err)
} else { } else {
sc.logger.Infof(r.SuccessSummary()) sc.logger.Infof(res.SuccessSummary())
} }
} }

View File

@ -270,17 +270,13 @@ func (sc *subcommandContext) cleanupConnections(tunnelIDs []uuid.UUID) error {
return nil return nil
} }
func (sc *subcommandContext) route(tunnelID uuid.UUID, r tunnelstore.Route) error { func (sc *subcommandContext) route(tunnelID uuid.UUID, r tunnelstore.Route) (tunnelstore.RouteResult, error) {
client, err := sc.client() client, err := sc.client()
if err != nil { if err != nil {
return err return nil, err
} }
if err := client.RouteTunnel(tunnelID, r); err != nil { return client.RouteTunnel(tunnelID, r)
return err
}
return nil
} }
func (sc *subcommandContext) tunnelActive(name string) (*tunnelstore.Tunnel, bool, error) { func (sc *subcommandContext) tunnelActive(name string) (*tunnelstore.Tunnel, bool, error) {

View File

@ -438,7 +438,7 @@ func routeCommand(c *cli.Context) error {
const tunnelIDIndex = 1 const tunnelIDIndex = 1
routeType := c.Args().First() routeType := c.Args().First()
var r tunnelstore.Route var route tunnelstore.Route
var tunnelID uuid.UUID var tunnelID uuid.UUID
switch routeType { switch routeType {
case "dns": case "dns":
@ -446,7 +446,7 @@ func routeCommand(c *cli.Context) error {
if err != nil { if err != nil {
return err return err
} }
r, err = dnsRouteFromArg(c) route, err = dnsRouteFromArg(c)
if err != nil { if err != nil {
return err return err
} }
@ -455,7 +455,7 @@ func routeCommand(c *cli.Context) error {
if err != nil { if err != nil {
return err return err
} }
r, err = lbRouteFromArg(c) route, err = lbRouteFromArg(c)
if err != nil { if err != nil {
return err return err
} }
@ -463,10 +463,12 @@ func routeCommand(c *cli.Context) error {
return cliutil.UsageError("%s is not a recognized route type. Supported route types are dns and lb", routeType) return cliutil.UsageError("%s is not a recognized route type. Supported route types are dns and lb", routeType)
} }
if err := sc.route(tunnelID, r); err != nil { res, err := sc.route(tunnelID, route)
if err != nil {
return err return err
} }
sc.logger.Infof(r.SuccessSummary())
sc.logger.Infof(res.SuccessSummary())
return nil return nil
} }

View File

@ -43,10 +43,22 @@ type Connection struct {
IsPendingReconnect bool `json:"is_pending_reconnect"` IsPendingReconnect bool `json:"is_pending_reconnect"`
} }
type Change = string
const (
ChangeNew = "new"
ChangeUpdated = "updated"
ChangeUnchanged = "unchanged"
)
// Route represents a record type that can route to a tunnel // Route represents a record type that can route to a tunnel
type Route interface { type Route interface {
json.Marshaler json.Marshaler
RecordType() string RecordType() string
UnmarshalResult(body io.Reader) (RouteResult, error)
}
type RouteResult interface {
// SuccessSummary explains what will route to this tunnel when it's provisioned successfully // SuccessSummary explains what will route to this tunnel when it's provisioned successfully
SuccessSummary() string SuccessSummary() string
} }
@ -55,6 +67,11 @@ type DNSRoute struct {
userHostname string userHostname string
} }
type DNSRouteResult struct {
route *DNSRoute
CName Change `json:"cname"`
}
func NewDNSRoute(userHostname string) Route { func NewDNSRoute(userHostname string) Route {
return &DNSRoute{ return &DNSRoute{
userHostname: userHostname, userHostname: userHostname,
@ -72,12 +89,30 @@ func (dr *DNSRoute) MarshalJSON() ([]byte, error) {
return json.Marshal(&s) return json.Marshal(&s)
} }
func (dr *DNSRoute) UnmarshalResult(body io.Reader) (RouteResult, error) {
var result DNSRouteResult
if err := json.NewDecoder(body).Decode(&result); err != nil {
return nil, err
}
result.route = dr
return &result, nil
}
func (dr *DNSRoute) RecordType() string { func (dr *DNSRoute) RecordType() string {
return "dns" return "dns"
} }
func (dr *DNSRoute) SuccessSummary() string { func (res *DNSRouteResult) SuccessSummary() string {
return fmt.Sprintf("%s will route to your tunnel", dr.userHostname) var msgFmt string
switch res.CName {
case ChangeNew:
msgFmt = "Added CNAME %s which will route to this tunnel"
case ChangeUpdated: // this is not currently returned by tunnelsore
msgFmt = "%s updated to route to your tunnel"
case ChangeUnchanged:
msgFmt = "%s is already configured to route to your tunnel"
}
return fmt.Sprintf(msgFmt, res.route.userHostname)
} }
type LBRoute struct { type LBRoute struct {
@ -85,6 +120,12 @@ type LBRoute struct {
lbPool string lbPool string
} }
type LBRouteResult struct {
route *LBRoute
LoadBalancer Change `json:"load_balancer"`
Pool Change `json:"pool"`
}
func NewLBRoute(lbName, lbPool string) Route { func NewLBRoute(lbName, lbPool string) Route {
return &LBRoute{ return &LBRoute{
lbName: lbName, lbName: lbName,
@ -109,8 +150,42 @@ func (lr *LBRoute) RecordType() string {
return "lb" return "lb"
} }
func (lr *LBRoute) SuccessSummary() string { func (lr *LBRoute) UnmarshalResult(body io.Reader) (RouteResult, error) {
return fmt.Sprintf("Load balancer %s will route to this tunnel through pool %s", lr.lbName, lr.lbPool) var result LBRouteResult
if err := json.NewDecoder(body).Decode(&result); err != nil {
return nil, err
}
result.route = lr
return &result, nil
}
func (res *LBRouteResult) SuccessSummary() string {
var msg string
switch res.LoadBalancer + "," + res.Pool {
case "new,new":
msg = "Created load balancer %s and added a new pool %s with this tunnel as an origin"
case "new,updated":
msg = "Created load balancer %s with an existing pool %s which was updated to use this tunnel as an origin"
case "new,unchanged":
msg = "Created load balancer %s with an existing pool %s which already has this tunnel as an origin"
case "updated,new":
msg = "Added new pool %[2]s with this tunnel as an origin to load balancer %[1]s"
case "updated,updated":
msg = "Updated pool %[2]s to use this tunnel as an origin and added it to load balancer %[1]s"
case "updated,unchanged":
msg = "Added pool %[2]s, which already has this tunnel as an origin, to load balancer %[1]s"
case "unchanged,updated":
msg = "Added this tunnel as an origin in pool %[2]s which is already used by load balancer %[1]s"
case "unchanged,unchanged":
msg = "Load balancer %s already uses pool %s which has this tunnel as an origin"
case "unchanged,new":
// this state is not possible
fallthrough
default:
msg = "Something went wrong: failed to modify load balancer %s with pool %s; please check traffic manager configuration in the dashboard"
}
return fmt.Sprintf(msg, res.route.lbName, res.route.lbPool)
} }
type Client interface { type Client interface {
@ -119,7 +194,7 @@ type Client interface {
DeleteTunnel(tunnelID uuid.UUID) error DeleteTunnel(tunnelID uuid.UUID) error
ListTunnels(filter *Filter) ([]*Tunnel, error) ListTunnels(filter *Filter) ([]*Tunnel, error)
CleanupConnections(tunnelID uuid.UUID) error CleanupConnections(tunnelID uuid.UUID) error
RouteTunnel(tunnelID uuid.UUID, route Route) error RouteTunnel(tunnelID uuid.UUID, route Route) (RouteResult, error)
} }
type RESTClient struct { type RESTClient struct {
@ -260,16 +335,20 @@ func (r *RESTClient) CleanupConnections(tunnelID uuid.UUID) error {
return r.statusCodeToError("cleanup connections", resp) return r.statusCodeToError("cleanup connections", resp)
} }
func (r *RESTClient) RouteTunnel(tunnelID uuid.UUID, route Route) error { func (r *RESTClient) RouteTunnel(tunnelID uuid.UUID, route Route) (RouteResult, error) {
endpoint := r.baseEndpoints.zoneLevel endpoint := r.baseEndpoints.zoneLevel
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/routes", tunnelID)) endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/routes", tunnelID))
resp, err := r.sendRequest("PUT", endpoint, route) resp, err := r.sendRequest("PUT", endpoint, route)
if err != nil { if err != nil {
return errors.Wrap(err, "REST request failed") return nil, errors.Wrap(err, "REST request failed")
} }
defer resp.Body.Close() defer resp.Body.Close()
return r.statusCodeToError("add route", resp) if resp.StatusCode == http.StatusOK {
return route.UnmarshalResult(resp.Body)
}
return nil, r.statusCodeToError("add route", resp)
} }
func (r *RESTClient) sendRequest(method string, url url.URL, body interface{}) (*http.Response, error) { func (r *RESTClient) sendRequest(method string, url url.URL, body interface{}) (*http.Response, error) {
@ -304,10 +383,10 @@ func unmarshalTunnel(reader io.Reader) (*Tunnel, error) {
func (r *RESTClient) statusCodeToError(op string, resp *http.Response) error { func (r *RESTClient) statusCodeToError(op string, resp *http.Response) error {
if resp.Header.Get("Content-Type") == "application/json" { if resp.Header.Get("Content-Type") == "application/json" {
var errorsResp struct{ var errorsResp struct {
Error string `json:"error"` Error string `json:"error"`
} }
if json.NewDecoder(resp.Body).Decode(&errorsResp) == nil && errorsResp.Error != ""{ if json.NewDecoder(resp.Body).Decode(&errorsResp) == nil && errorsResp.Error != "" {
return errors.Errorf("Failed to %s: %s", op, errorsResp.Error) return errors.Errorf("Failed to %s: %s", op, errorsResp.Error)
} }
} }

View File

@ -0,0 +1,78 @@
package tunnelstore
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestDNSRouteUnmarshalResult(t *testing.T) {
route := &DNSRoute{
userHostname: "example.com",
}
result, err := route.UnmarshalResult(strings.NewReader(`{"cname": "new"}`))
assert.NoError(t, err)
assert.Equal(t, &DNSRouteResult{
route: route,
CName: ChangeNew,
}, result)
_, err = route.UnmarshalResult(strings.NewReader(`abc`))
assert.NotNil(t, err)
}
func TestLBRouteUnmarshalResult(t *testing.T) {
route := &LBRoute{
lbName: "lb.example.com",
lbPool: "pool",
}
result, err := route.UnmarshalResult(strings.NewReader(`{"pool": "unchanged", "load_balancer": "updated"}`))
assert.NoError(t, err)
assert.Equal(t, &LBRouteResult{
route: route,
LoadBalancer: ChangeUpdated,
Pool: ChangeUnchanged,
}, result)
_, err = route.UnmarshalResult(strings.NewReader(`abc`))
assert.NotNil(t, err)
}
func TestLBRouteResultSuccessSummary(t *testing.T) {
route := &LBRoute{
lbName: "lb.example.com",
lbPool: "POOL",
}
tests := []struct {
lb Change
pool Change
expected string
}{
{ChangeNew, ChangeNew, "Created load balancer lb.example.com and added a new pool POOL with this tunnel as an origin" },
{ChangeNew, ChangeUpdated, "Created load balancer lb.example.com with an existing pool POOL which was updated to use this tunnel as an origin" },
{ChangeNew, ChangeUnchanged, "Created load balancer lb.example.com with an existing pool POOL which already has this tunnel as an origin" },
{ChangeUpdated, ChangeNew, "Added new pool POOL with this tunnel as an origin to load balancer lb.example.com" },
{ChangeUpdated, ChangeUpdated, "Updated pool POOL to use this tunnel as an origin and added it to load balancer lb.example.com" },
{ChangeUpdated, ChangeUnchanged, "Added pool POOL, which already has this tunnel as an origin, to load balancer lb.example.com" },
{ChangeUnchanged, ChangeNew, "Something went wrong: failed to modify load balancer lb.example.com with pool POOL; please check traffic manager configuration in the dashboard" },
{ChangeUnchanged, ChangeUpdated, "Added this tunnel as an origin in pool POOL which is already used by load balancer lb.example.com" },
{ChangeUnchanged, ChangeUnchanged, "Load balancer lb.example.com already uses pool POOL which has this tunnel as an origin" },
{"", "", "Something went wrong: failed to modify load balancer lb.example.com with pool POOL; please check traffic manager configuration in the dashboard" },
{"a", "b", "Something went wrong: failed to modify load balancer lb.example.com with pool POOL; please check traffic manager configuration in the dashboard" },
}
for i, tt := range tests {
res := &LBRouteResult{
route: route,
LoadBalancer: tt.lb,
Pool: tt.pool,
}
actual := res.SuccessSummary()
assert.Equal(t, tt.expected, actual, "case %d", i+1)
}
}