From 424f1fa47445ca3aa680c13e77b325428c005e8b Mon Sep 17 00:00:00 2001 From: Matthew Kocher Date: Tue, 7 Sep 2021 22:57:20 +0000 Subject: [PATCH] Implements `cf route` command `cf route` can be used to see details about a route and mapped destinations. Handles http & tcp routes. Shows http1/http2/tcp protocol for destinations. Will not error if used on older cloud controllers but destination protocols will be blank. Co-authored-by: Matthew Kocher Co-authored-by: Merric de Launey --- actor/v7action/route.go | 11 + actor/v7action/route_test.go | 75 +++++ command/common/command_list_v7.go | 1 + command/common/internal/help_all_display.go | 4 +- command/v7/actor.go | 1 + command/v7/route_command.go | 138 +++++++++ command/v7/route_command_test.go | 275 ++++++++++++++++++ command/v7/v7fakes/fake_actor.go | 83 ++++++ integration/v7/isolated/route_command_test.go | 233 +++++++++++++++ resources/route_resource.go | 6 +- 10 files changed, 824 insertions(+), 3 deletions(-) create mode 100644 command/v7/route_command.go create mode 100644 command/v7/route_command_test.go create mode 100644 integration/v7/isolated/route_command_test.go diff --git a/actor/v7action/route.go b/actor/v7action/route.go index 67c7c374f55..14bde2bf456 100644 --- a/actor/v7action/route.go +++ b/actor/v7action/route.go @@ -204,6 +204,17 @@ func (actor Actor) GetRoutesByOrg(orgGUID string, labelSelector string) ([]resou return routes, allWarnings, nil } +func (actor Actor) GetApplicationMapForRoute(route resources.Route) (map[string]resources.Application, Warnings, error) { + var v7Warning Warnings + apps, v7Warning, err := actor.GetApplicationsByGUIDs(extract.UniqueList("Destinations.App.GUID", route)) + + appMap := make(map[string]resources.Application) + for _, a := range apps { + appMap[a.GUID] = a + } + return appMap, v7Warning, err +} + func (actor Actor) GetRouteSummaries(routes []resources.Route) ([]RouteSummary, Warnings, error) { var ( spaces []resources.Space diff --git a/actor/v7action/route_test.go b/actor/v7action/route_test.go index ec402858c0e..01eafeca46a 100644 --- a/actor/v7action/route_test.go +++ b/actor/v7action/route_test.go @@ -175,6 +175,81 @@ var _ = Describe("Route Actions", func() { }) }) + Describe("GetApplicationMapForRoute", func() { + var ( + appsByAppGuid map[string]resources.Application + app1 resources.Application + app2 resources.Application + route resources.Route + warnings Warnings + err error + ) + + BeforeEach(func() { + app1 = resources.Application{ + GUID: "app-guid-1", + Name: "app-name-1", + } + app2 = resources.Application{ + GUID: "app-guid-2", + Name: "app-name-2", + } + route = resources.Route{ + Destinations: []resources.RouteDestination{ + { + App: resources.RouteDestinationApp{ + GUID: "app-guid-1", + }, + }, + { + App: resources.RouteDestinationApp{ + GUID: "app-guid-2", + }, + }, + }, + SpaceGUID: "fake-space-1-guid", + URL: "fake-url-1/fake-path-1:1", + Host: "fake-host-1", + Path: "fake-path-1", + Port: 1, + } + }) + + JustBeforeEach(func() { + appsByAppGuid, warnings, err = actor.GetApplicationMapForRoute(route) + }) + + When("CC successfully returns the response", func() { + BeforeEach(func() { + fakeCloudControllerClient.GetApplicationsReturns( + []resources.Application{app1, app2}, + ccv3.Warnings{"get-apps-warning"}, + nil, + ) + }) + It("returns a mapping from apps guids to apps", func() { + Expect(appsByAppGuid).To(Equal(map[string]resources.Application{"app-guid-1": app1, "app-guid-2": app2})) + Expect(warnings).To(ConsistOf("get-apps-warning")) + Expect(err).ToNot(HaveOccurred()) + }) + }) + + When("CC errors", func() { + var cc_err = errors.New("failed to get route") + BeforeEach(func() { + fakeCloudControllerClient.GetApplicationsReturns( + []resources.Application{}, + ccv3.Warnings{"get-apps-warning"}, + cc_err, + ) + }) + It("returns an error", func() { + Expect(warnings).To(ConsistOf("get-apps-warning")) + Expect(err).To(Equal(cc_err)) + }) + }) + }) + Describe("GetRoutesBySpace", func() { var ( routes []resources.Route diff --git a/command/common/command_list_v7.go b/command/common/command_list_v7.go index 20c6c40dc94..6cb264ed745 100644 --- a/command/common/command_list_v7.go +++ b/command/common/command_list_v7.go @@ -126,6 +126,7 @@ type commandList struct { Restart v7.RestartCommand `command:"restart" alias:"rs" description:"Stop all instances of the app, then start them again."` RestartAppInstance v7.RestartAppInstanceCommand `command:"restart-app-instance" description:"Terminate, then instantiate an app instance"` RouterGroups v7.RouterGroupsCommand `command:"router-groups" description:"List router groups"` + Route v7.RouteCommand `command:"route" alias:"ro" description:"Display route details and mapped destinations"` Routes v7.RoutesCommand `command:"routes" alias:"r" description:"List all routes in the current space or the current organization"` RunTask v7.RunTaskCommand `command:"run-task" alias:"rt" description:"Run a one-off task on an app"` RunningEnvironmentVariableGroup v7.RunningEnvironmentVariableGroupCommand `command:"running-environment-variable-group" alias:"revg" description:"Retrieve the contents of the running environment variable group"` diff --git a/command/common/internal/help_all_display.go b/command/common/internal/help_all_display.go index c39f434dee0..3dac8169748 100644 --- a/command/common/internal/help_all_display.go +++ b/command/common/internal/help_all_display.go @@ -64,7 +64,9 @@ var HelpCategoryList = []HelpCategory{ { CategoryName: "ROUTES:", CommandList: [][]string{ - {"routes", "create-route", "check-route", "map-route", "unmap-route", "delete-route", "delete-orphaned-routes"}, + {"routes", "route"}, + {"create-route", "check-route", "map-route", "unmap-route", "delete-route"}, + {"delete-orphaned-routes"}, }, }, { diff --git a/command/v7/actor.go b/command/v7/actor.go index 3c6e1ba3560..52fc12ece70 100644 --- a/command/v7/actor.go +++ b/command/v7/actor.go @@ -88,6 +88,7 @@ type Actor interface { GetAppFeature(appGUID string, featureName string) (resources.ApplicationFeature, v7action.Warnings, error) GetAppSummariesForSpace(spaceGUID string, labels string) ([]v7action.ApplicationSummary, v7action.Warnings, error) GetApplicationByNameAndSpace(appName string, spaceGUID string) (resources.Application, v7action.Warnings, error) + GetApplicationMapForRoute(route resources.Route) (map[string]resources.Application, v7action.Warnings, error) GetApplicationDroplets(appName string, spaceGUID string) ([]resources.Droplet, v7action.Warnings, error) GetApplicationLabels(appName string, spaceGUID string) (map[string]types.NullString, v7action.Warnings, error) GetApplicationPackages(appName string, spaceGUID string) ([]resources.Package, v7action.Warnings, error) diff --git a/command/v7/route_command.go b/command/v7/route_command.go new file mode 100644 index 00000000000..5c89398e8ff --- /dev/null +++ b/command/v7/route_command.go @@ -0,0 +1,138 @@ +package v7 + +import ( + "code.cloudfoundry.org/cli/command/flag" + "code.cloudfoundry.org/cli/resources" + + "strconv" +) + +type RouteCommand struct { + BaseCommand + + RequiredArgs flag.Domain `positional-args:"yes"` + Hostname string `long:"hostname" short:"n" description:"Hostname used to identify the HTTP route"` + Path flag.V7RoutePath `long:"path" description:"Path used to identify the HTTP route"` + Port int `long:"port" description:"Port used to identify the TCP route"` + relatedCommands interface{} `related_commands:"create-route, delete-route, routes"` +} + +func (cmd RouteCommand) Usage() string { + return ` +Display an HTTP route: + CF_NAME route DOMAIN [--hostname HOSTNAME] [--path PATH] + +Display a TCP route: + CF_NAME route DOMAIN --port PORT` +} + +func (cmd RouteCommand) Examples() string { + return ` +CF_NAME route example.com # example.com +CF_NAME route example.com -n myhost --path foo # myhost.example.com/foo +CF_NAME route example.com --path foo # example.com/foo +CF_NAME route example.com --port 5000 # example.com:5000` +} + +func (cmd RouteCommand) Execute(args []string) error { + err := cmd.SharedActor.CheckTarget(true, false) + if err != nil { + return err + } + + user, err := cmd.Config.CurrentUser() + if err != nil { + return err + } + + domain, warnings, err := cmd.Actor.GetDomainByName(cmd.RequiredArgs.Domain) + + cmd.UI.DisplayWarnings(warnings) + if err != nil { + return err + } + + hostName := "" + if cmd.Hostname != "" { + hostName = cmd.Hostname + "." + } + + displayPort := "" + if cmd.Port != 0 { + displayPort = ":" + strconv.Itoa(cmd.Port) + + } + + cmd.UI.DisplayTextWithFlavor(" Showing route {{.HostName}}{{.DomainName}}{{.Port}}{{.PathName}} in org {{.OrgName}} / space {{.SpaceName}} as {{.Username}}...", map[string]interface{}{ + "HostName": hostName, + "DomainName": cmd.RequiredArgs.Domain, + "PathName": cmd.Path.Path, + "Port": displayPort, + "OrgName": cmd.Config.TargetedOrganization().Name, + "SpaceName": cmd.Config.TargetedSpace().Name, + "Username": user.Name, + }) + cmd.UI.DisplayNewline() + + route, warnings, err := cmd.Actor.GetRouteByAttributes(domain, cmd.Hostname, cmd.Path.Path, cmd.Port) + cmd.UI.DisplayWarnings(warnings) + if err != nil { + return err + } + + port := "" + if route.Port != 0 { + port = strconv.Itoa(route.Port) + } + + appMap, warnings, err := cmd.Actor.GetApplicationMapForRoute(route) + cmd.UI.DisplayWarnings(warnings) + if err != nil { + return err + } + + table := [][]string{ + {cmd.UI.TranslateText("domain:"), domain.Name}, + {cmd.UI.TranslateText("host:"), route.Host}, + {cmd.UI.TranslateText("port:"), port}, + {cmd.UI.TranslateText("path:"), route.Path}, + {cmd.UI.TranslateText("protocol:"), route.Protocol}, + } + + cmd.UI.DisplayKeyValueTable("", table, 3) + cmd.UI.DisplayNewline() + + cmd.UI.DisplayText("Destinations:") + cmd.displayDestinations(route, appMap) + + return nil +} + +func (cmd RouteCommand) displayDestinations(route resources.Route, appMap map[string]resources.Application) { + destinations := route.Destinations + if len(destinations) > 0 { + var keyValueTable = [][]string{ + { + cmd.UI.TranslateText("app"), + cmd.UI.TranslateText("process"), + cmd.UI.TranslateText("port"), + cmd.UI.TranslateText("protocol"), + }, + } + + for _, destination := range destinations { + port := "" + if destination.Port != 0 { + port = strconv.Itoa(destination.Port) + } + keyValueTable = append(keyValueTable, []string{ + appMap[destination.App.GUID].Name, + destination.App.Process.Type, + port, + destination.Protocol, + }) + } + + cmd.UI.DisplayKeyValueTable("\t", keyValueTable, 3) + } +} diff --git a/command/v7/route_command_test.go b/command/v7/route_command_test.go new file mode 100644 index 00000000000..2d616beafb4 --- /dev/null +++ b/command/v7/route_command_test.go @@ -0,0 +1,275 @@ +package v7_test + +import ( + "code.cloudfoundry.org/cli/actor/actionerror" + "code.cloudfoundry.org/cli/actor/v7action" + "code.cloudfoundry.org/cli/cf/errors" + "code.cloudfoundry.org/cli/command/commandfakes" + "code.cloudfoundry.org/cli/command/flag" + v7 "code.cloudfoundry.org/cli/command/v7" + "code.cloudfoundry.org/cli/command/v7/v7fakes" + "code.cloudfoundry.org/cli/resources" + "code.cloudfoundry.org/cli/util/configv3" + "code.cloudfoundry.org/cli/util/ui" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gbytes" +) + +var _ = Describe("route Command", func() { + var ( + cmd v7.RouteCommand + testUI *ui.UI + fakeConfig *commandfakes.FakeConfig + fakeSharedActor *commandfakes.FakeSharedActor + fakeActor *v7fakes.FakeActor + binaryName string + executeErr error + domainName string + ) + + BeforeEach(func() { + testUI = ui.NewTestUI(nil, NewBuffer(), NewBuffer()) + fakeConfig = new(commandfakes.FakeConfig) + fakeSharedActor = new(commandfakes.FakeSharedActor) + fakeActor = new(v7fakes.FakeActor) + + binaryName = "faceman" + fakeConfig.BinaryNameReturns(binaryName) + + domainName = "some-domain.com" + + cmd = v7.RouteCommand{ + BaseCommand: v7.BaseCommand{ + UI: testUI, + Config: fakeConfig, + SharedActor: fakeSharedActor, + Actor: fakeActor, + }, + RequiredArgs: flag.Domain{Domain: domainName}, + } + + fakeConfig.TargetedSpaceReturns(configv3.Space{Name: "some-space", GUID: "some-space-guid"}) + fakeConfig.TargetedOrganizationReturns(configv3.Organization{Name: "some-org"}) + fakeConfig.CurrentUserReturns(configv3.User{Name: "some-user"}, nil) + + fakeActor.GetDomainByNameReturns( + resources.Domain{Name: domainName, GUID: "domain-guid"}, + v7action.Warnings{"get-domain-warnings"}, + nil, + ) + + fakeActor.GetRouteByAttributesReturns( + resources.Route{GUID: "route-guid"}, + v7action.Warnings{"get-route-warnings"}, + nil, + ) + fakeActor.GetApplicationMapForRouteReturns( + map[string]resources.Application{"app-guid": {GUID: "app-guid", Name: "app-name"}}, + v7action.Warnings{"get-route-warnings"}, + nil, + ) + + }) + + JustBeforeEach(func() { + executeErr = cmd.Execute(nil) + }) + + It("checks the target", func() { + Expect(fakeSharedActor.CheckTargetCallCount()).To(Equal(1)) + checkTargetedOrg, checkTargetedSpace := fakeSharedActor.CheckTargetArgsForCall(0) + Expect(checkTargetedOrg).To(BeTrue()) + Expect(checkTargetedSpace).To(BeFalse()) + }) + + When("checking target fails", func() { + BeforeEach(func() { + fakeSharedActor.CheckTargetReturns(actionerror.NotLoggedInError{BinaryName: binaryName}) + }) + + It("returns an error", func() { + Expect(executeErr).To(MatchError(actionerror.NotLoggedInError{BinaryName: binaryName})) + }) + }) + + It("checks if the user is logged in", func() { + Expect(fakeConfig.CurrentUserCallCount()).To(Equal(1)) + }) + + When("the user is not logged in", func() { + BeforeEach(func() { + fakeConfig.CurrentUserReturns(configv3.User{}, errors.New("no current user")) + }) + + It("returns an error", func() { + Expect(executeErr).To(MatchError("no current user")) + }) + }) + + When("getting the domain errors", func() { + BeforeEach(func() { + fakeActor.GetDomainByNameReturns(resources.Domain{}, v7action.Warnings{"get-domain-warnings"}, errors.New("get-domain-error")) + }) + + It("returns the error and displays warnings", func() { + Expect(testUI.Err).To(Say("get-domain-warnings")) + Expect(executeErr).To(MatchError(errors.New("get-domain-error"))) + Expect(fakeActor.UnmapRouteCallCount()).To(Equal(0)) + }) + }) + + It("gets the domain and displays warnings", func() { + Expect(testUI.Err).To(Say("get-domain-warnings")) + + Expect(fakeActor.GetDomainByNameCallCount()).To(Equal(1)) + Expect(fakeActor.GetDomainByNameArgsForCall(0)).To(Equal(domainName)) + }) + + Describe("getting the routes", func() { + It("calls GetRouteByAttributes and displaying warnings", func() { + Expect(testUI.Err).To(Say("route-warning")) + + Expect(fakeActor.GetRouteByAttributesCallCount()).To(Equal(1)) + domain, host, path, port := fakeActor.GetRouteByAttributesArgsForCall(0) + Expect(domain.Name).To(Equal(domainName)) + Expect(domain.GUID).To(Equal("domain-guid")) + Expect(host).To(Equal(cmd.Hostname)) + Expect(path).To(Equal(cmd.Path.Path)) + Expect(port).To(Equal(0)) + }) + + When("getting the route errors", func() { + BeforeEach(func() { + fakeActor.GetRouteByAttributesReturns( + resources.Route{}, + v7action.Warnings{"get-route-warnings"}, + errors.New("get-route-error"), + ) + }) + + It("returns the error and displays warnings", func() { + Expect(testUI.Err).To(Say("get-route-warnings")) + Expect(executeErr).To(MatchError(errors.New("get-route-error"))) + }) + }) + }) + + Describe("getting the apps", func() { + It("calls GetApplicationMapForRoute and displaying warnings", func() { + Expect(testUI.Err).To(Say("route-warning")) + + Expect(fakeActor.GetApplicationMapForRouteCallCount()).To(Equal(1)) + route := fakeActor.GetApplicationMapForRouteArgsForCall(0) + Expect(route.GUID).To(Equal("route-guid")) + }) + + When("getting the Application mapping errors", func() { + BeforeEach(func() { + fakeActor.GetApplicationMapForRouteReturns( + map[string]resources.Application{}, + v7action.Warnings{"get-app-map-warnings"}, + errors.New("get-app-map-error"), + ) + }) + + It("returns the error and displays warnings", func() { + Expect(testUI.Err).To(Say("get-app-map-warnings")) + Expect(executeErr).To(MatchError(errors.New("get-app-map-error"))) + }) + }) + }) + + When("passing hostname and path flags", func() { + BeforeEach(func() { + cmd.Path.Path = "/some-path" + cmd.Hostname = "some-host" + + destAppA := resources.RouteDestinationApp{GUID: "abc", Process: struct{ Type string }{"web"}} + destinationA := resources.RouteDestination{App: destAppA, Port: 8080, Protocol: "http1"} + + destAppB := resources.RouteDestinationApp{GUID: "123", Process: struct{ Type string }{"web"}} + destinationB := resources.RouteDestination{App: destAppB, Port: 1337, Protocol: "http2"} + + destinations := []resources.RouteDestination{destinationA, destinationB} + route := resources.Route{GUID: "route-guid", Host: cmd.Hostname, Path: cmd.Path.Path, Protocol: "http", Destinations: destinations} + + fakeActor.GetRouteByAttributesReturns( + route, + v7action.Warnings{"get-route-warnings"}, + nil, + ) + + appA := resources.Application{GUID: "abc", Name: "app-name"} + appB := resources.Application{GUID: "123", Name: "other-app-name"} + + fakeActor.GetApplicationMapForRouteReturns( + map[string]resources.Application{"abc": appA, "123": appB}, + v7action.Warnings{"get-apps-error"}, + nil, + ) + }) + + It("displays the summary", func() { + Expect(executeErr).NotTo(HaveOccurred()) + + Expect(testUI.Out).To(Say(`Showing route %s\.%s/some-path in org some-org / space some-space as some-user\.\.\.`, cmd.Hostname, domainName)) + Expect(testUI.Out).To(Say(`domain:\s+%s`, domainName)) + Expect(testUI.Out).To(Say(`host:\s+%s`, cmd.Hostname)) + Expect(testUI.Out).To(Say(`path:\s+%s`, cmd.Path.Path)) + Expect(testUI.Out).To(Say(`protocol:\s+http`)) + Expect(testUI.Out).To(Say(`\n`)) + Expect(testUI.Out).To(Say(`Destinations:`)) + Expect(testUI.Out).To(Say(`\s+app\s+process\s+port\s+protocol`)) + Expect(testUI.Out).To(Say(`\s+app-name\s+web\s+8080\s+http1`)) + Expect(testUI.Out).To(Say(`\s+other-app-name\s+web\s+1337\s+http2`)) + + Expect(fakeActor.GetRouteByAttributesCallCount()).To(Equal(1)) + givenDomain, givenHostname, givenPath, givenPort := fakeActor.GetRouteByAttributesArgsForCall(0) + Expect(givenDomain.Name).To(Equal(domainName)) + Expect(givenHostname).To(Equal("some-host")) + Expect(givenPath).To(Equal("/some-path")) + Expect(givenPort).To(Equal(0)) + }) + }) + Describe("RouteRetrieval display logic", func() { + When("passing in just a domain", func() { + BeforeEach(func() { + cmd.Hostname = "" + cmd.Path.Path = "" + }) + It(" displays the right stuff", func() { + Expect(testUI.Out).To(Say(`Showing route %s in org some-org / space some-space as some-user\.\.\.`, domainName)) + }) + }) + When("passing in a domain and hostname", func() { + BeforeEach(func() { + cmd.Hostname = "some-host" + cmd.Path.Path = "" + }) + It(" displays the right stuff", func() { + Expect(testUI.Out).To(Say(`Showing route some-host\.%s in org some-org / space some-space as some-user\.\.\.`, domainName)) + }) + }) + + When("passing in a domain, a hostname, and a path", func() { + BeforeEach(func() { + cmd.Hostname = "some-host" + cmd.Path.Path = "/some-path" + }) + It(" displays the right stuff", func() { + Expect(testUI.Out).To(Say(`Showing route some-host\.%s\/some-path in org some-org / space some-space as some-user\.\.\.`, domainName)) + }) + }) + When("passing in a domain and a port", func() { + BeforeEach(func() { + cmd.Hostname = "" + cmd.Path.Path = "" + cmd.Port = 8080 + }) + It(" displays the right stuff", func() { + Expect(testUI.Out).To(Say(`Showing route %s:8080 in org some-org / space some-space as some-user\.\.\.`, domainName)) + }) + }) + }) +}) diff --git a/command/v7/v7fakes/fake_actor.go b/command/v7/v7fakes/fake_actor.go index 18191235ae6..96679e882bc 100644 --- a/command/v7/v7fakes/fake_actor.go +++ b/command/v7/v7fakes/fake_actor.go @@ -1070,6 +1070,21 @@ type FakeActor struct { result2 v7action.Warnings result3 error } + GetApplicationMapForRouteStub func(resources.Route) (map[string]resources.Application, v7action.Warnings, error) + getApplicationMapForRouteMutex sync.RWMutex + getApplicationMapForRouteArgsForCall []struct { + arg1 resources.Route + } + getApplicationMapForRouteReturns struct { + result1 map[string]resources.Application + result2 v7action.Warnings + result3 error + } + getApplicationMapForRouteReturnsOnCall map[int]struct { + result1 map[string]resources.Application + result2 v7action.Warnings + result3 error + } GetApplicationPackagesStub func(string, string) ([]resources.Package, v7action.Warnings, error) getApplicationPackagesMutex sync.RWMutex getApplicationPackagesArgsForCall []struct { @@ -8095,6 +8110,72 @@ func (fake *FakeActor) GetApplicationLabelsReturnsOnCall(i int, result1 map[stri }{result1, result2, result3} } +func (fake *FakeActor) GetApplicationMapForRoute(arg1 resources.Route) (map[string]resources.Application, v7action.Warnings, error) { + fake.getApplicationMapForRouteMutex.Lock() + ret, specificReturn := fake.getApplicationMapForRouteReturnsOnCall[len(fake.getApplicationMapForRouteArgsForCall)] + fake.getApplicationMapForRouteArgsForCall = append(fake.getApplicationMapForRouteArgsForCall, struct { + arg1 resources.Route + }{arg1}) + fake.recordInvocation("GetApplicationMapForRoute", []interface{}{arg1}) + fake.getApplicationMapForRouteMutex.Unlock() + if fake.GetApplicationMapForRouteStub != nil { + return fake.GetApplicationMapForRouteStub(arg1) + } + if specificReturn { + return ret.result1, ret.result2, ret.result3 + } + fakeReturns := fake.getApplicationMapForRouteReturns + return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3 +} + +func (fake *FakeActor) GetApplicationMapForRouteCallCount() int { + fake.getApplicationMapForRouteMutex.RLock() + defer fake.getApplicationMapForRouteMutex.RUnlock() + return len(fake.getApplicationMapForRouteArgsForCall) +} + +func (fake *FakeActor) GetApplicationMapForRouteCalls(stub func(resources.Route) (map[string]resources.Application, v7action.Warnings, error)) { + fake.getApplicationMapForRouteMutex.Lock() + defer fake.getApplicationMapForRouteMutex.Unlock() + fake.GetApplicationMapForRouteStub = stub +} + +func (fake *FakeActor) GetApplicationMapForRouteArgsForCall(i int) resources.Route { + fake.getApplicationMapForRouteMutex.RLock() + defer fake.getApplicationMapForRouteMutex.RUnlock() + argsForCall := fake.getApplicationMapForRouteArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeActor) GetApplicationMapForRouteReturns(result1 map[string]resources.Application, result2 v7action.Warnings, result3 error) { + fake.getApplicationMapForRouteMutex.Lock() + defer fake.getApplicationMapForRouteMutex.Unlock() + fake.GetApplicationMapForRouteStub = nil + fake.getApplicationMapForRouteReturns = struct { + result1 map[string]resources.Application + result2 v7action.Warnings + result3 error + }{result1, result2, result3} +} + +func (fake *FakeActor) GetApplicationMapForRouteReturnsOnCall(i int, result1 map[string]resources.Application, result2 v7action.Warnings, result3 error) { + fake.getApplicationMapForRouteMutex.Lock() + defer fake.getApplicationMapForRouteMutex.Unlock() + fake.GetApplicationMapForRouteStub = nil + if fake.getApplicationMapForRouteReturnsOnCall == nil { + fake.getApplicationMapForRouteReturnsOnCall = make(map[int]struct { + result1 map[string]resources.Application + result2 v7action.Warnings + result3 error + }) + } + fake.getApplicationMapForRouteReturnsOnCall[i] = struct { + result1 map[string]resources.Application + result2 v7action.Warnings + result3 error + }{result1, result2, result3} +} + func (fake *FakeActor) GetApplicationPackages(arg1 string, arg2 string) ([]resources.Package, v7action.Warnings, error) { fake.getApplicationPackagesMutex.Lock() ret, specificReturn := fake.getApplicationPackagesReturnsOnCall[len(fake.getApplicationPackagesArgsForCall)] @@ -18835,6 +18916,8 @@ func (fake *FakeActor) Invocations() map[string][][]interface{} { defer fake.getApplicationDropletsMutex.RUnlock() fake.getApplicationLabelsMutex.RLock() defer fake.getApplicationLabelsMutex.RUnlock() + fake.getApplicationMapForRouteMutex.RLock() + defer fake.getApplicationMapForRouteMutex.RUnlock() fake.getApplicationPackagesMutex.RLock() defer fake.getApplicationPackagesMutex.RUnlock() fake.getApplicationProcessHealthChecksByNameAndSpaceMutex.RLock() diff --git a/integration/v7/isolated/route_command_test.go b/integration/v7/isolated/route_command_test.go new file mode 100644 index 00000000000..373243bdab5 --- /dev/null +++ b/integration/v7/isolated/route_command_test.go @@ -0,0 +1,233 @@ +package isolated + +import ( + "fmt" + + . "code.cloudfoundry.org/cli/cf/util/testhelpers/matchers" + "code.cloudfoundry.org/cli/integration/helpers" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gbytes" + . "github.com/onsi/gomega/gexec" +) + +var _ = Describe("route command", func() { + Context("Help", func() { + It("appears in cf help -a", func() { + session := helpers.CF("help", "-a") + Eventually(session).Should(Exit(0)) + Expect(session).To(HaveCommandInCategoryWithDescription("route", "ROUTES", "Display route details and mapped destinations")) + }) + + It("displays the help information", func() { + session := helpers.CF("route", "--help") + Eventually(session).Should(Say(`NAME:`)) + Eventually(session).Should(Say(`route - Display route details and mapped destinations`)) + Eventually(session).Should(Say(`\n`)) + + Eventually(session).Should(Say(`USAGE:`)) + Eventually(session).Should(Say(`Display an HTTP route:`)) + Eventually(session).Should(Say(`cf route DOMAIN \[--hostname HOSTNAME\] \[--path PATH\]\n`)) + Eventually(session).Should(Say(`Display a TCP route:`)) + Eventually(session).Should(Say(`cf route DOMAIN --port PORT\n`)) + Eventually(session).Should(Say(`\n`)) + + Eventually(session).Should(Say(`EXAMPLES:`)) + Eventually(session).Should(Say(`cf route example.com # example.com`)) + Eventually(session).Should(Say(`cf route example.com -n myhost --path foo # myhost.example.com/foo`)) + Eventually(session).Should(Say(`cf route example.com --path foo # example.com/foo`)) + Eventually(session).Should(Say(`cf route example.com --port 5000 # example.com:5000`)) + Eventually(session).Should(Say(`\n`)) + + Eventually(session).Should(Say(`OPTIONS:`)) + Eventually(session).Should(Say(`--hostname, -n\s+Hostname used to identify the HTTP route`)) + Eventually(session).Should(Say(`--path\s+Path used to identify the HTTP route`)) + Eventually(session).Should(Say(`--port\s+Port used to identify the TCP route`)) + Eventually(session).Should(Say(`\n`)) + + Eventually(session).Should(Say(`SEE ALSO:`)) + Eventually(session).Should(Say(`create-route, delete-route, routes`)) + + Eventually(session).Should(Exit(0)) + }) + }) + + When("the environment is not setup correctly", func() { + It("fails with the appropriate errors", func() { + helpers.CheckEnvironmentTargetedCorrectly(true, false, ReadOnlyOrg, "route", "some-domain") + }) + }) + + When("the environment is set up correctly", func() { + var ( + userName string + orgName string + spaceName string + ) + + BeforeEach(func() { + orgName = helpers.NewOrgName() + spaceName = helpers.NewSpaceName() + + helpers.SetupCF(orgName, spaceName) + userName, _ = helpers.GetCredentials() + }) + + AfterEach(func() { + helpers.QuickDeleteOrg(orgName) + }) + + When("the domain exists", func() { + var ( + domainName string + ) + + BeforeEach(func() { + domainName = helpers.NewDomainName() + }) + + When("the route exists", func() { + var ( + domain helpers.Domain + hostname string + path string + ) + + When("it's an HTTP route", func() { + BeforeEach(func() { + domain = helpers.NewDomain(orgName, domainName) + hostname = "key-lime-pie" + path = "/some-path" + domain.CreatePrivate() + Eventually(helpers.CF("create-app", "killer")).Should(Exit(0)) + Eventually(helpers.CF("create-route", domain.Name, "--hostname", hostname, "--path", path)).Should(Exit(0)) + Eventually(helpers.CF("map-route", "killer", domain.Name, "--hostname", hostname, "--path", path)).Should(Exit(0)) + }) + + AfterEach(func() { + domain.Delete() + }) + + It("displays the route summary and exits without failing", func() { + session := helpers.CF("route", domainName, "--hostname", hostname, "--path", path) + Eventually(session).Should(Say(`Showing route %s\.%s%s in org %s / space %s as %s\.\.\.`, hostname, domainName, path, orgName, spaceName, userName)) + Eventually(session).Should(Say(`domain:\s+%s`, domainName)) + Eventually(session).Should(Say(`host:\s+%s`, hostname)) + Eventually(session).Should(Say(`port:\s+\n`)) + Eventually(session).Should(Say(`path:\s+%s`, path)) + Eventually(session).Should(Say(`protocol:\s+http`)) + Eventually(session).Should(Say(`\n`)) + Eventually(session).Should(Say(`Destinations:`)) + Eventually(session).Should(Say(`\s+app\s+process\s+port\s+protocol`)) + Eventually(session).Should(Say(`\s+killer\s+web\s+8080\s+http1`)) + Eventually(session).Should(Exit(0)) + }) + }) + + When("it's a TCP route", func() { + var ( + routerGroup helpers.RouterGroup + port int + tcpDomain helpers.Domain + ) + + BeforeEach(func() { + routerGroup = helpers.NewRouterGroup(helpers.NewRouterGroupName(), "1024-2048") + routerGroup.Create() + + tcpDomain = helpers.NewDomain(orgName, helpers.NewDomainName("TCP-DOMAIN")) + tcpDomain.CreateWithRouterGroup(routerGroup.Name) + + port = 1024 + + Eventually(helpers.CF("create-app", "killer")).Should(Exit(0)) + Eventually(helpers.CF("create-route", tcpDomain.Name, "--port", fmt.Sprintf("%d", port))).Should(Exit(0)) + Eventually(helpers.CF("map-route", "killer", tcpDomain.Name, "--port", "1024")).Should(Exit(0)) + }) + + AfterEach(func() { + tcpDomain.DeleteShared() + routerGroup.Delete() + }) + + It("displays the route summary and exits without failing", func() { + session := helpers.CF("route", tcpDomain.Name, "--port", fmt.Sprintf("%d", port)) + Eventually(session).Should(Say(`Showing route %s:%d in org %s / space %s as %s\.\.\.`, tcpDomain.Name, port, orgName, spaceName, userName)) + Eventually(session).Should(Say(`domain:\s+%s`, tcpDomain.Name)) + Eventually(session).Should(Say(`host:\s+\n`)) + Eventually(session).Should(Say(`port:\s+%d`, port)) + Eventually(session).Should(Say(`path:\s+\n`)) + Eventually(session).Should(Say(`protocol:\s+tcp`)) + Eventually(session).Should(Say(`\n`)) + Eventually(session).Should(Say(`Destinations:`)) + Eventually(session).Should(Say(`\s+app\s+process\s+port\s+protocol`)) + Eventually(session).Should(Say(`\s+killer\s+web\s+8080\s+tcp`)) + Eventually(session).Should(Exit(0)) + }) + }) + }) + + When("the route does not exist", func() { + var domain helpers.Domain + + BeforeEach(func() { + domain = helpers.NewDomain(orgName, domainName) + domain.Create() + }) + + AfterEach(func() { + domain.Delete() + }) + + When("no flags are used", func() { + It("checks the route", func() { + session := helpers.CF("route", domainName) + Eventually(session).Should(Say(`Showing route %s in org %s / space %s as %s\.\.\.`, domainName, orgName, spaceName, userName)) + Eventually(session.Err).Should(Say(`Route with host '', domain '%s', and path '/' not found\.`, domainName)) + Eventually(session).Should(Exit(1)) + }) + }) + + When("passing in a hostname", func() { + It("checks the route with the hostname", func() { + hostname := "tiramisu" + session := helpers.CF("route", domainName, "-n", hostname) + Eventually(session).Should(Say(`Showing route %s.%s in org %s / space %s as %s\.\.\.`, hostname, domainName, orgName, spaceName, userName)) + Eventually(session.Err).Should(Say(`Route with host '%s', domain '%s', and path '/' not found\.`, hostname, domainName)) + Eventually(session).Should(Exit(1)) + }) + }) + + When("passing in hostname and path with a leading '/'", func() { + It("checks the route with hostname and path", func() { + hostname := "tiramisu" + pathString := "/recipes" + session := helpers.CF("route", domainName, "-n", hostname, "--path", pathString) + Eventually(session).Should(Say(`Showing route %s.%s%s in org %s / space %s as %s\.\.\.`, hostname, domainName, pathString, orgName, spaceName, userName)) + Eventually(session.Err).Should(Say(`Route with host '%s', domain '%s', and path '%s' not found`, hostname, domainName, pathString)) + Eventually(session).Should(Exit(1)) + }) + }) + }) + }) + + When("the domain does not exist", func() { + It("displays error and exits 1", func() { + session := helpers.CF("route", "some-domain") + Eventually(session).Should(Say(`FAILED`)) + Eventually(session.Err).Should(Say(`Domain 'some-domain' not found.`)) + Eventually(session).Should(Exit(1)) + }) + }) + + When("the domain is not specified", func() { + It("displays error and exits 1", func() { + session := helpers.CF("route") + Eventually(session.Err).Should(Say("Incorrect Usage: the required argument `DOMAIN` was not provided\n")) + Eventually(session.Err).Should(Say("\n")) + Eventually(session).Should(Say("NAME:\n")) + Eventually(session).Should(Exit(1)) + }) + }) + }) +}) diff --git a/resources/route_resource.go b/resources/route_resource.go index 1a7d20f2d1e..5bf31b49b25 100644 --- a/resources/route_resource.go +++ b/resources/route_resource.go @@ -14,8 +14,10 @@ type RouteDestinationApp struct { } type RouteDestination struct { - GUID string - App RouteDestinationApp + GUID string + App RouteDestinationApp + Port int + Protocol string } type Route struct {