diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index e404f2dc45..b37b1bef65 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -12,7 +12,7 @@ jobs: analyze: name: Analyze runs-on: ubuntu-latest - if: ${{ github.action_repository == 'cloudfoundry/cli' }} + if: ${{ github.repository == 'cloudfoundry/cli' }} permissions: actions: read contents: read diff --git a/.github/workflows/units.yml b/.github/workflows/units.yml index db2f329155..55b534dc9c 100644 --- a/.github/workflows/units.yml +++ b/.github/workflows/units.yml @@ -1,10 +1,6 @@ name: Units Tests -on: - push: - branches: - - "*" - +on: [push] permissions: contents: write @@ -83,9 +79,8 @@ jobs: strategy: matrix: os: - - windows-2022 + - windows-latest - windows-2019 - - windows-2016 runs-on: ${{ matrix.os }} needs: shared-values defaults: diff --git a/actor/v7action/cloud_controller_client.go b/actor/v7action/cloud_controller_client.go index 756c0528ff..050b4d5fda 100644 --- a/actor/v7action/cloud_controller_client.go +++ b/actor/v7action/cloud_controller_client.go @@ -140,6 +140,7 @@ type CloudControllerClient interface { GetUsers(query ...ccv3.Query) ([]resources.User, ccv3.Warnings, error) MakeRequestSendReceiveRaw(Method string, URL string, headers http.Header, requestBody []byte) ([]byte, *http.Response, error) MapRoute(routeGUID string, appGUID string, destinationProtocol string) (ccv3.Warnings, error) + MoveRoute(routeGUID string, spaceGUID string) (ccv3.Warnings, error) PollJob(jobURL ccv3.JobURL) (ccv3.Warnings, error) PollJobForState(jobURL ccv3.JobURL, state constant.JobState) (ccv3.Warnings, error) PollJobToEventStream(jobURL ccv3.JobURL) chan ccv3.PollJobEvent diff --git a/actor/v7action/route.go b/actor/v7action/route.go index 16f7bc9f4c..7cac618000 100644 --- a/actor/v7action/route.go +++ b/actor/v7action/route.go @@ -426,6 +426,11 @@ func (actor Actor) GetApplicationRoutes(appGUID string) ([]resources.Route, Warn return routes, allWarnings, nil } +func (actor Actor) MoveRoute(routeGUID string, spaceGUID string) (Warnings, error) { + warnings, err := actor.CloudControllerClient.MoveRoute(routeGUID, spaceGUID) + return Warnings(warnings), err +} + func getDomainName(fullURL, host, path string, port int) string { domainWithoutHost := strings.TrimPrefix(fullURL, host+".") domainWithoutPath := strings.TrimSuffix(domainWithoutHost, path) diff --git a/actor/v7action/v7actionfakes/fake_cloud_controller_client.go b/actor/v7action/v7actionfakes/fake_cloud_controller_client.go index 15a378271c..8b2d424073 100644 --- a/actor/v7action/v7actionfakes/fake_cloud_controller_client.go +++ b/actor/v7action/v7actionfakes/fake_cloud_controller_client.go @@ -1966,6 +1966,20 @@ type FakeCloudControllerClient struct { result1 ccv3.Warnings result2 error } + MoveRouteStub func(string, string) (ccv3.Warnings, error) + moveRouteMutex sync.RWMutex + moveRouteArgsForCall []struct { + arg1 string + arg2 string + } + moveRouteReturns struct { + result1 ccv3.Warnings + result2 error + } + moveRouteReturnsOnCall map[int]struct { + result1 ccv3.Warnings + result2 error + } PollJobStub func(ccv3.JobURL) (ccv3.Warnings, error) pollJobMutex sync.RWMutex pollJobArgsForCall []struct { @@ -11192,6 +11206,70 @@ func (fake *FakeCloudControllerClient) MapRouteReturnsOnCall(i int, result1 ccv3 }{result1, result2} } +func (fake *FakeCloudControllerClient) MoveRoute(arg1 string, arg2 string) (ccv3.Warnings, error) { + fake.moveRouteMutex.Lock() + ret, specificReturn := fake.moveRouteReturnsOnCall[len(fake.moveRouteArgsForCall)] + fake.moveRouteArgsForCall = append(fake.moveRouteArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + fake.recordInvocation("MoveRoute", []interface{}{arg1, arg2}) + fake.moveRouteMutex.Unlock() + if fake.MoveRouteStub != nil { + return fake.MoveRouteStub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.moveRouteReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeCloudControllerClient) MoveRouteCallCount() int { + fake.moveRouteMutex.RLock() + defer fake.moveRouteMutex.RUnlock() + return len(fake.moveRouteArgsForCall) +} + +func (fake *FakeCloudControllerClient) MoveRouteCalls(stub func(string, string) (ccv3.Warnings, error)) { + fake.moveRouteMutex.Lock() + defer fake.moveRouteMutex.Unlock() + fake.MoveRouteStub = stub +} + +func (fake *FakeCloudControllerClient) MoveRouteArgsForCall(i int) (string, string) { + fake.moveRouteMutex.RLock() + defer fake.moveRouteMutex.RUnlock() + argsForCall := fake.moveRouteArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeCloudControllerClient) MoveRouteReturns(result1 ccv3.Warnings, result2 error) { + fake.moveRouteMutex.Lock() + defer fake.moveRouteMutex.Unlock() + fake.MoveRouteStub = nil + fake.moveRouteReturns = struct { + result1 ccv3.Warnings + result2 error + }{result1, result2} +} + +func (fake *FakeCloudControllerClient) MoveRouteReturnsOnCall(i int, result1 ccv3.Warnings, result2 error) { + fake.moveRouteMutex.Lock() + defer fake.moveRouteMutex.Unlock() + fake.MoveRouteStub = nil + if fake.moveRouteReturnsOnCall == nil { + fake.moveRouteReturnsOnCall = make(map[int]struct { + result1 ccv3.Warnings + result2 error + }) + } + fake.moveRouteReturnsOnCall[i] = struct { + result1 ccv3.Warnings + result2 error + }{result1, result2} +} + func (fake *FakeCloudControllerClient) PollJob(arg1 ccv3.JobURL) (ccv3.Warnings, error) { fake.pollJobMutex.Lock() ret, specificReturn := fake.pollJobReturnsOnCall[len(fake.pollJobArgsForCall)] @@ -14721,6 +14799,8 @@ func (fake *FakeCloudControllerClient) Invocations() map[string][][]interface{} defer fake.makeRequestSendReceiveRawMutex.RUnlock() fake.mapRouteMutex.RLock() defer fake.mapRouteMutex.RUnlock() + fake.moveRouteMutex.RLock() + defer fake.moveRouteMutex.RUnlock() fake.pollJobMutex.RLock() defer fake.pollJobMutex.RUnlock() fake.pollJobForStateMutex.RLock() diff --git a/actor/v7pushaction/handle_app_path_override_test.go b/actor/v7pushaction/handle_app_path_override_test.go index 60376947d7..b77a6d521d 100644 --- a/actor/v7pushaction/handle_app_path_override_test.go +++ b/actor/v7pushaction/handle_app_path_override_test.go @@ -4,6 +4,7 @@ import ( "io/ioutil" "os" "path/filepath" + "runtime" . "code.cloudfoundry.org/cli/actor/v7pushaction" "code.cloudfoundry.org/cli/cf/util/testhelpers/matchers" @@ -94,7 +95,11 @@ var _ = Describe("HandleAppPathOverride", func() { absoluteAppFilehandle, err = ioutil.TempFile("", "") Expect(err).NotTo(HaveOccurred()) defer absoluteAppFilehandle.Close() - relativeAppFilePath = filepath.Join(".", absoluteAppFilehandle.Name()) + if runtime.GOOS == "windows" { + relativeAppFilePath = absoluteAppFilehandle.Name() + } else { + relativeAppFilePath = filepath.Join(filepath.Dir("/"), absoluteAppFilehandle.Name()) + } flagOverrides.ProvidedAppPath = relativeAppFilePath // TODO: Do NOT use Chdir! it affects ALL other threads diff --git a/api/cloudcontroller/ccv3/internal/api_routes.go b/api/cloudcontroller/ccv3/internal/api_routes.go index 7029d8073f..5da806e7d2 100644 --- a/api/cloudcontroller/ccv3/internal/api_routes.go +++ b/api/cloudcontroller/ccv3/internal/api_routes.go @@ -130,6 +130,7 @@ const ( PatchSpaceFeaturesRequest = "PatchSpaceFeatures" PatchSpaceQuotaRequest = "PatchSpaceQuota" PatchStackRequest = "PatchStack" + PatchMoveRouteRequest = "PatchMoveRouteRequest" PostApplicationActionApplyManifest = "PostApplicationActionApplyM" PostApplicationActionRestartRequest = "PostApplicationActionRestart" PostApplicationActionStartRequest = "PostApplicationActionStart" @@ -279,6 +280,7 @@ var APIRoutes = map[string]Route{ UnmapRouteRequest: {Path: "/v3/routes/:route_guid/destinations/:destination_guid", Method: http.MethodDelete}, PatchDestinationRequest: {Path: "/v3/routes/:route_guid/destinations/:destination_guid", Method: http.MethodPatch}, ShareRouteRequest: {Path: "/v3/routes/:route_guid/relationships/shared_spaces", Method: http.MethodPost}, + PatchMoveRouteRequest: {Path: "/v3/routes/:route_guid/relationships/space", Method: http.MethodPatch}, GetSecurityGroupsRequest: {Path: "/v3/security_groups", Method: http.MethodGet}, PostSecurityGroupRequest: {Path: "/v3/security_groups", Method: http.MethodPost}, DeleteSecurityGroupRequest: {Path: "/v3/security_groups/:security_group_guid", Method: http.MethodDelete}, diff --git a/api/cloudcontroller/ccv3/route.go b/api/cloudcontroller/ccv3/route.go index d7ad9a2c18..56f068640a 100644 --- a/api/cloudcontroller/ccv3/route.go +++ b/api/cloudcontroller/ccv3/route.go @@ -173,3 +173,28 @@ func (client Client) ShareRoute(routeGUID string, spaceGUID string) (Warnings, e }) return warnings, err } + +func (client Client) MoveRoute(routeGUID string, spaceGUID string) (Warnings, error) { + type space struct { + GUID string `json:"guid"` + } + + type body struct { + Data space `json:"data"` + } + + requestBody := body{ + Data: space{ + GUID: spaceGUID, + }, + } + + var responseBody resources.Build + _, warnings, err := client.MakeRequest(RequestParams{ + RequestName: internal.PatchMoveRouteRequest, + URIParams: internal.Params{"route_guid": routeGUID}, + RequestBody: &requestBody, + ResponseBody: &responseBody, + }) + return warnings, err +} diff --git a/command/common/command_list_v7.go b/command/common/command_list_v7.go index fada59bcb0..588e47e391 100644 --- a/command/common/command_list_v7.go +++ b/command/common/command_list_v7.go @@ -173,6 +173,7 @@ type commandList struct { Target v7.TargetCommand `command:"target" alias:"t" description:"Set or view the targeted org or space"` Tasks v7.TasksCommand `command:"tasks" description:"List tasks of an app"` TerminateTask v7.TerminateTaskCommand `command:"terminate-task" description:"Terminate a running task of an app"` + MoveRoute v7.MoveRouteCommand `command:"move-route" description:"Assign a route to a different space"` UnbindRouteService v7.UnbindRouteServiceCommand `command:"unbind-route-service" alias:"urs" description:"Unbind a service instance from an HTTP route"` UnbindRunningSecurityGroup v7.UnbindRunningSecurityGroupCommand `command:"unbind-running-security-group" description:"Unbind a security group from the set of security groups for running applications globally"` UnbindSecurityGroup v7.UnbindSecurityGroupCommand `command:"unbind-security-group" description:"Unbind a security group from a space"` diff --git a/command/common/internal/help_all_display.go b/command/common/internal/help_all_display.go index 8c6e9957f0..97990a5002 100644 --- a/command/common/internal/help_all_display.go +++ b/command/common/internal/help_all_display.go @@ -69,6 +69,7 @@ var HelpCategoryList = []HelpCategory{ {"delete-orphaned-routes"}, {"update-destination"}, {"share-route"}, + {"move-route"}, }, }, { diff --git a/command/v7/actor.go b/command/v7/actor.go index 64ef63dd1f..3bbae507c6 100644 --- a/command/v7/actor.go +++ b/command/v7/actor.go @@ -183,6 +183,7 @@ type Actor interface { MakeCurlRequest(httpMethod string, path string, customHeaders []string, httpData string, failOnHTTPError bool) ([]byte, *http.Response, error) MapRoute(routeGUID string, appGUID string, destinationProtocol string) (v7action.Warnings, error) Marketplace(filter v7action.MarketplaceFilter) ([]v7action.ServiceOfferingWithPlans, v7action.Warnings, error) + MoveRoute(routeGUID string, spaceGUID string) (v7action.Warnings, error) ParseAccessToken(accessToken string) (jwt.JWT, error) PollBuild(buildGUID string, appName string) (resources.Droplet, v7action.Warnings, error) PollPackage(pkg resources.Package) (resources.Package, v7action.Warnings, error) diff --git a/command/v7/move_route_command.go b/command/v7/move_route_command.go new file mode 100644 index 0000000000..4ba150568d --- /dev/null +++ b/command/v7/move_route_command.go @@ -0,0 +1,101 @@ +package v7 + +import ( + "code.cloudfoundry.org/cli/actor/actionerror" + "code.cloudfoundry.org/cli/command/flag" +) + +type MoveRouteCommand struct { + BaseCommand + + RequireArgs flag.Domain `positional-args:"yes"` + Hostname string `long:"hostname" short:"n" description:"Hostname for the HTTP route (required for shared domains)"` + Path flag.V7RoutePath `long:"path" description:"Path for the HTTP route"` + DestinationOrg string `short:"o" description:"The org of the destination app (Default: targeted org)"` + DestinationSpace string `short:"s" description:"The space of the destination app (Default: targeted space)"` + + relatedCommands interface{} `related_commands:"create-route, map-route, unmap-route, routes"` +} + +func (cmd MoveRouteCommand) Usage() string { + return ` + Transfers the ownership of a route to a another space: + CF_NAME move-route DOMAIN [--hostname HOSTNAME] [--path PATH] -s OTHER_SPACE [-o OTHER_ORG]` +} + +func (cmd MoveRouteCommand) Examples() string { + return ` + CF_NAME move-route example.com --hostname myHost --path foo -s TargetSpace -o TargetOrg # myhost.example.com/foo + CF_NAME move-route example.com --hostname myHost -s TargetSpace # myhost.example.com + CF_NAME move-route example.com --hostname myHost -s TargetSpace -o TargetOrg # myhost.example.com` +} + +func (cmd MoveRouteCommand) Execute(args []string) error { + err := cmd.SharedActor.CheckTarget(true, true) + if err != nil { + return err + } + + user, err := cmd.Actor.GetCurrentUser() + if err != nil { + return err + } + + domain, warnings, err := cmd.Actor.GetDomainByName(cmd.RequireArgs.Domain) + cmd.UI.DisplayWarnings(warnings) + if err != nil { + return err + } + + path := cmd.Path.Path + route, warnings, err := cmd.Actor.GetRouteByAttributes(domain, cmd.Hostname, path, 0) + cmd.UI.DisplayWarnings(warnings) + if err != nil { + if _, ok := err.(actionerror.RouteNotFoundError); ok { + cmd.UI.DisplayText("Can not transfer ownership of route:") + return err + } + } + + destinationOrgName := cmd.DestinationOrg + + if destinationOrgName == "" { + destinationOrgName = cmd.Config.TargetedOrganizationName() + } + + destinationOrg, warnings, err := cmd.Actor.GetOrganizationByName(destinationOrgName) + + if err != nil { + if _, ok := err.(actionerror.OrganizationNotFoundError); ok { + cmd.UI.DisplayText("Can not transfer ownership of route:") + return err + } + } + + targetedSpace, warnings, err := cmd.Actor.GetSpaceByNameAndOrganization(cmd.DestinationSpace, destinationOrg.GUID) + if err != nil { + if _, ok := err.(actionerror.SpaceNotFoundError); ok { + cmd.UI.DisplayText("Can not transfer ownership of route:") + return err + } + } + + url := desiredURL(domain.Name, cmd.Hostname, path, 0) + cmd.UI.DisplayTextWithFlavor("Move ownership of route {{.URL}} to space {{.DestinationSpace}} as {{.User}}", + map[string]interface{}{ + "URL": url, + "DestinationSpace": cmd.DestinationSpace, + "User": user.Name, + }) + warnings, err = cmd.Actor.MoveRoute( + route.GUID, + targetedSpace.GUID, + ) + cmd.UI.DisplayWarnings(warnings) + if err != nil { + return err + } + cmd.UI.DisplayOK() + + return nil +} diff --git a/command/v7/move_route_command_test.go b/command/v7/move_route_command_test.go new file mode 100644 index 0000000000..4ff7b7360b --- /dev/null +++ b/command/v7/move_route_command_test.go @@ -0,0 +1,279 @@ +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("move-route Command", func() { + var ( + cmd v7.MoveRouteCommand + testUI *ui.UI + fakeConfig *commandfakes.FakeConfig + fakeSharedActor *commandfakes.FakeSharedActor + fakeActor *v7fakes.FakeActor + binaryName string + executeErr error + domainName string + orgName string + spaceName string + hostname string + path string + ) + + BeforeEach(func() { + testUI = ui.NewTestUI(nil, NewBuffer(), NewBuffer()) + fakeConfig = new(commandfakes.FakeConfig) + fakeSharedActor = new(commandfakes.FakeSharedActor) + fakeActor = new(v7fakes.FakeActor) + + binaryName = "myBinaryBread" + fakeConfig.BinaryNameReturns(binaryName) + + domainName = "some-domain.com" + orgName = "org-name-a" + spaceName = "space-name-a" + hostname = "myHostname" + path = "myPath" + + cmd = v7.MoveRouteCommand{ + BaseCommand: v7.BaseCommand{ + UI: testUI, + Config: fakeConfig, + SharedActor: fakeSharedActor, + Actor: fakeActor, + }, + RequireArgs: flag.Domain{Domain: domainName}, + Hostname: hostname, + Path: flag.V7RoutePath{Path: path}, + DestinationOrg: orgName, + DestinationSpace: spaceName, + } + + 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) + }) + + JustBeforeEach(func() { + executeErr = cmd.Execute(nil) + }) + + It("checks that target", func() { + Expect(fakeSharedActor.CheckTargetCallCount()).To(Equal(1)) + checkTargetedOrg, checkTargetedSpace := fakeSharedActor.CheckTargetArgsForCall(0) + Expect(checkTargetedOrg).To(BeTrue()) + Expect(checkTargetedSpace).To(BeTrue()) + }) + + When("checking target fails", func() { + BeforeEach(func() { + fakeSharedActor.CheckTargetReturns(actionerror.NoOrganizationTargetedError{BinaryName: binaryName}) + }) + It("returns an error", func() { + Expect(executeErr).To(MatchError(actionerror.NoOrganizationTargetedError{BinaryName: binaryName})) + + Expect(fakeSharedActor.CheckTargetCallCount()).To(Equal(1)) + checkTargetedOrg, checkTargetedSpace := fakeSharedActor.CheckTargetArgsForCall(0) + Expect(checkTargetedOrg).To(BeTrue()) + Expect(checkTargetedSpace).To(BeTrue()) + }) + }) + + When("the user is not logged in", func() { + var expectedErr error + + BeforeEach(func() { + expectedErr = errors.New("some current user error") + fakeActor.GetCurrentUserReturns(configv3.User{}, expectedErr) + }) + + It("return an error", func() { + Expect(executeErr).To(Equal(expectedErr)) + }) + }) + + When("the user is logged in and targeted", func() { + 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.GetDomainByNameCallCount()).To(Equal(1)) + Expect(fakeActor.GetDomainByNameArgsForCall(0)).To(Equal(domainName)) + + Expect(fakeActor.GetRouteByAttributesCallCount()).To(Equal(0)) + + Expect(fakeActor.GetSpaceByNameAndOrganizationCallCount()).To(Equal(0)) + + Expect(fakeActor.MoveRouteCallCount()).To(Equal(0)) + }) + }) + + When("getting the domain succeeds", func() { + BeforeEach(func() { + fakeActor.GetDomainByNameReturns( + resources.Domain{Name: domainName, GUID: "domain-guid"}, + v7action.Warnings{"get-domain-warnings"}, + nil, + ) + }) + + When("the requested route does not exist", func() { + BeforeEach(func() { + fakeActor.GetRouteByAttributesReturns( + resources.Route{}, + v7action.Warnings{"get-route-warnings"}, + actionerror.RouteNotFoundError{}, + ) + }) + + It("displays error message", func() { + Expect(testUI.Err).To(Say("get-domain-warnings")) + Expect(testUI.Err).To(Say("get-route-warnings")) + Expect(executeErr).To(HaveOccurred()) + + Expect(fakeActor.GetDomainByNameCallCount()).To(Equal(1)) + Expect(fakeActor.GetDomainByNameArgsForCall(0)).To(Equal(domainName)) + + Expect(fakeActor.GetRouteByAttributesCallCount()).To(Equal(1)) + actualDomain, actualHostname, actualPath, actualPort := fakeActor.GetRouteByAttributesArgsForCall(0) + Expect(actualDomain.Name).To(Equal(domainName)) + Expect(actualDomain.GUID).To(Equal("domain-guid")) + Expect(actualHostname).To(Equal(hostname)) + Expect(actualPath).To(Equal(path)) + Expect(actualPort).To(Equal(0)) + }) + }) + + When("the requested route exists", func() { + BeforeEach(func() { + fakeActor.GetRouteByAttributesReturns( + resources.Route{GUID: "route-guid"}, + v7action.Warnings{"get-route-warnings"}, + nil, + ) + }) + When("getting the target space errors", func() { + BeforeEach(func() { + fakeActor.GetOrganizationByNameReturns( + resources.Organization{GUID: "org-guid-a"}, + v7action.Warnings{"get-route-warnings"}, + nil, + ) + fakeActor.GetSpaceByNameAndOrganizationReturns( + resources.Space{}, + v7action.Warnings{"get-route-warnings"}, + actionerror.SpaceNotFoundError{}, + ) + }) + It("returns the error and warnings", func() { + Expect(executeErr).To(HaveOccurred()) + + Expect(fakeActor.GetDomainByNameCallCount()).To(Equal(1)) + Expect(fakeActor.GetDomainByNameArgsForCall(0)).To(Equal(domainName)) + + Expect(fakeActor.GetRouteByAttributesCallCount()).To(Equal(1)) + actualDomain, actualHostname, actualPath, actualPort := fakeActor.GetRouteByAttributesArgsForCall(0) + Expect(actualDomain.Name).To(Equal(domainName)) + Expect(actualDomain.GUID).To(Equal("domain-guid")) + Expect(actualHostname).To(Equal(hostname)) + Expect(actualPath).To(Equal(path)) + Expect(actualPort).To(Equal(0)) + + Expect(fakeActor.GetOrganizationByNameCallCount()).To(Equal(1)) + Expect(fakeActor.GetOrganizationByNameArgsForCall(0)).To(Equal(orgName)) + Expect(fakeActor.GetSpaceByNameAndOrganizationCallCount()).To(Equal(1)) + spaceName, orgGuid := fakeActor.GetSpaceByNameAndOrganizationArgsForCall(0) + Expect(spaceName).To(Equal("space-name-a")) + Expect(orgGuid).To(Equal("org-guid-a")) + + Expect(fakeActor.MoveRouteCallCount()).To(Equal(0)) + }) + }) + When("getting the target org errors", func() { + BeforeEach(func() { + fakeActor.GetOrganizationByNameReturns( + resources.Organization{}, + v7action.Warnings{"get-route-warnings"}, + actionerror.OrganizationNotFoundError{}, + ) + }) + It("returns the error and warnings", func() { + Expect(executeErr).To(HaveOccurred()) + + Expect(fakeActor.GetDomainByNameCallCount()).To(Equal(1)) + Expect(fakeActor.GetDomainByNameArgsForCall(0)).To(Equal(domainName)) + + Expect(fakeActor.GetRouteByAttributesCallCount()).To(Equal(1)) + actualDomain, actualHostname, actualPath, actualPort := fakeActor.GetRouteByAttributesArgsForCall(0) + Expect(actualDomain.Name).To(Equal(domainName)) + Expect(actualDomain.GUID).To(Equal("domain-guid")) + Expect(actualHostname).To(Equal(hostname)) + Expect(actualPath).To(Equal(path)) + Expect(actualPort).To(Equal(0)) + + Expect(fakeActor.GetOrganizationByNameCallCount()).To(Equal(1)) + orgName := fakeActor.GetOrganizationByNameArgsForCall(0) + Expect(orgName).To(Equal("org-name-a")) + + Expect(fakeActor.MoveRouteCallCount()).To(Equal(0)) + }) + }) + When("getting the target space succeeds", func() { + BeforeEach(func() { + fakeActor.GetOrganizationByNameReturns( + resources.Organization{GUID: "org-guid-a"}, + v7action.Warnings{"get-route-warnings"}, + nil, + ) + fakeActor.GetSpaceByNameAndOrganizationReturns( + resources.Space{GUID: "space-guid-b"}, + v7action.Warnings{"get-route-warnings"}, + nil, + ) + }) + It("exits 0 with helpful message that the route is now transferred", func() { + Expect(executeErr).ShouldNot(HaveOccurred()) + + Expect(fakeActor.GetDomainByNameCallCount()).To(Equal(1)) + Expect(fakeActor.GetDomainByNameArgsForCall(0)).To(Equal(domainName)) + + Expect(fakeActor.GetRouteByAttributesCallCount()).To(Equal(1)) + actualDomain, actualHostname, actualPath, actualPort := fakeActor.GetRouteByAttributesArgsForCall(0) + Expect(actualDomain.Name).To(Equal(domainName)) + Expect(actualDomain.GUID).To(Equal("domain-guid")) + Expect(actualHostname).To(Equal(hostname)) + Expect(actualPath).To(Equal(path)) + Expect(actualPort).To(Equal(0)) + + Expect(fakeActor.GetOrganizationByNameCallCount()).To(Equal(1)) + orgName := fakeActor.GetOrganizationByNameArgsForCall(0) + Expect(orgName).To(Equal("org-name-a")) + + Expect(fakeActor.GetSpaceByNameAndOrganizationCallCount()).To(Equal(1)) + spaceName, orgGuid := fakeActor.GetSpaceByNameAndOrganizationArgsForCall(0) + Expect(spaceName).To(Equal("space-name-a")) + Expect(orgGuid).To(Equal("org-guid-a")) + Expect(fakeActor.MoveRouteCallCount()).To(Equal(1)) + }) + }) + }) + }) + }) +}) diff --git a/command/v7/v7fakes/fake_actor.go b/command/v7/v7fakes/fake_actor.go index 1a950e4692..f3cddabd4f 100644 --- a/command/v7/v7fakes/fake_actor.go +++ b/command/v7/v7fakes/fake_actor.go @@ -2489,6 +2489,20 @@ type FakeActor struct { result2 v7action.Warnings result3 error } + MoveRouteStub func(string, string) (v7action.Warnings, error) + moveRouteMutex sync.RWMutex + moveRouteArgsForCall []struct { + arg1 string + arg2 string + } + moveRouteReturns struct { + result1 v7action.Warnings + result2 error + } + moveRouteReturnsOnCall map[int]struct { + result1 v7action.Warnings + result2 error + } ParseAccessTokenStub func(string) (jwt.JWT, error) parseAccessTokenMutex sync.RWMutex parseAccessTokenArgsForCall []struct { @@ -14180,6 +14194,70 @@ func (fake *FakeActor) MarketplaceReturnsOnCall(i int, result1 []v7action.Servic }{result1, result2, result3} } +func (fake *FakeActor) MoveRoute(arg1 string, arg2 string) (v7action.Warnings, error) { + fake.moveRouteMutex.Lock() + ret, specificReturn := fake.moveRouteReturnsOnCall[len(fake.moveRouteArgsForCall)] + fake.moveRouteArgsForCall = append(fake.moveRouteArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + fake.recordInvocation("MoveRoute", []interface{}{arg1, arg2}) + fake.moveRouteMutex.Unlock() + if fake.MoveRouteStub != nil { + return fake.MoveRouteStub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + fakeReturns := fake.moveRouteReturns + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeActor) MoveRouteCallCount() int { + fake.moveRouteMutex.RLock() + defer fake.moveRouteMutex.RUnlock() + return len(fake.moveRouteArgsForCall) +} + +func (fake *FakeActor) MoveRouteCalls(stub func(string, string) (v7action.Warnings, error)) { + fake.moveRouteMutex.Lock() + defer fake.moveRouteMutex.Unlock() + fake.MoveRouteStub = stub +} + +func (fake *FakeActor) MoveRouteArgsForCall(i int) (string, string) { + fake.moveRouteMutex.RLock() + defer fake.moveRouteMutex.RUnlock() + argsForCall := fake.moveRouteArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeActor) MoveRouteReturns(result1 v7action.Warnings, result2 error) { + fake.moveRouteMutex.Lock() + defer fake.moveRouteMutex.Unlock() + fake.MoveRouteStub = nil + fake.moveRouteReturns = struct { + result1 v7action.Warnings + result2 error + }{result1, result2} +} + +func (fake *FakeActor) MoveRouteReturnsOnCall(i int, result1 v7action.Warnings, result2 error) { + fake.moveRouteMutex.Lock() + defer fake.moveRouteMutex.Unlock() + fake.MoveRouteStub = nil + if fake.moveRouteReturnsOnCall == nil { + fake.moveRouteReturnsOnCall = make(map[int]struct { + result1 v7action.Warnings + result2 error + }) + } + fake.moveRouteReturnsOnCall[i] = struct { + result1 v7action.Warnings + result2 error + }{result1, result2} +} + func (fake *FakeActor) ParseAccessToken(arg1 string) (jwt.JWT, error) { fake.parseAccessTokenMutex.Lock() ret, specificReturn := fake.parseAccessTokenReturnsOnCall[len(fake.parseAccessTokenArgsForCall)] @@ -19326,6 +19404,8 @@ func (fake *FakeActor) Invocations() map[string][][]interface{} { defer fake.mapRouteMutex.RUnlock() fake.marketplaceMutex.RLock() defer fake.marketplaceMutex.RUnlock() + fake.moveRouteMutex.RLock() + defer fake.moveRouteMutex.RUnlock() fake.parseAccessTokenMutex.RLock() defer fake.parseAccessTokenMutex.RUnlock() fake.pollBuildMutex.RLock() diff --git a/integration/v7/isolated/move_route_command_test.go b/integration/v7/isolated/move_route_command_test.go new file mode 100644 index 0000000000..b973fb1d00 --- /dev/null +++ b/integration/v7/isolated/move_route_command_test.go @@ -0,0 +1,204 @@ +package isolated + +import ( + "code.cloudfoundry.org/cli/api/cloudcontroller/ccversion" + . "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("move 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("move-route", "ROUTES", "Assign a route to a different space")) + }) + + It("displays the help information", func() { + session := helpers.CF("move-route", "--help") + Eventually(session).Should(Say(`NAME:`)) + Eventually(session).Should(Say("move-route - Assign a route to a different space")) + Eventually(session).Should(Say(`\n`)) + + Eventually(session).Should(Say(`USAGE:`)) + Eventually(session).Should(Say(`Transfers the ownership of a route to a another space:`)) + Eventually(session).Should(Say(`cf move-route DOMAIN \[--hostname HOSTNAME\] \[--path PATH\] -s OTHER_SPACE \[-o OTHER_ORG\]`)) + Eventually(session).Should(Say(`\n`)) + + Eventually(session).Should(Say(`EXAMPLES:`)) + Eventually(session).Should(Say(`cf move-route example.com --hostname myHost --path foo -s TargetSpace -o TargetOrg # myhost.example.com/foo`)) + Eventually(session).Should(Say(`cf move-route example.com --hostname myHost -s TargetSpace # myhost.example.com`)) + Eventually(session).Should(Say(`cf move-route example.com --hostname myHost -s TargetSpace -o TargetOrg # myhost.example.com`)) + Eventually(session).Should(Say(`\n`)) + + Eventually(session).Should(Say(`OPTIONS:`)) + Eventually(session).Should(Say(`--hostname, -n\s+Hostname for the HTTP route \(required for shared domains\)`)) + Eventually(session).Should(Say(`--path\s+Path for the HTTP route`)) + Eventually(session).Should(Say(`-o\s+The org of the destination app \(Default: targeted org\)`)) + Eventually(session).Should(Say(`-s\s+The space of the destination app \(Default: targeted space\)`)) + Eventually(session).Should(Say(`\n`)) + + Eventually(session).Should(Say(`SEE ALSO:`)) + Eventually(session).Should(Say(`create-route, map-route, routes, unmap-route`)) + + 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, "move-route", "some-domain", "-s SOME_SPACE") + }) + }) + + When("the environment is set up correctly", func() { + var ( + userName string + orgName string + spaceName string + ) + + BeforeEach(func() { + helpers.SkipIfVersionLessThan(ccversion.MinVersionHTTP2RoutingV3) + 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 + targetSpaceName string + ) + + BeforeEach(func() { + domainName = helpers.NewDomainName() + }) + + When("the route exists", func() { + var ( + domain helpers.Domain + hostname string + ) + When("the target space exists in targeted org", func() { + BeforeEach(func() { + domain = helpers.NewDomain(orgName, domainName) + hostname = "panera-bread" + targetSpaceName = helpers.NewSpaceName() + helpers.CreateSpace(targetSpaceName) + domain.CreateShared() + Eventually(helpers.CF("create-route", domain.Name, "--hostname", hostname)).Should(Exit(0)) + }) + + AfterEach(func() { + domain.DeleteShared() + }) + + It("transfers the route to the destination space", func() { + session := helpers.CF("move-route", domainName, "--hostname", hostname, "-s", targetSpaceName) + Eventually(session).Should(Say(`Move ownership of route %s.%s to space %s as %s`, hostname, domainName, targetSpaceName, userName)) + Eventually(session).Should(Say(`OK`)) + Eventually(session).Should(Exit(0)) + }) + }) + + When("the target organization does not exist", func() { + var targetOrgName string + BeforeEach(func() { + domain = helpers.NewDomain(orgName, domainName) + hostname = "panera-bread" + targetSpaceName = helpers.NewSpaceName() + targetOrgName = helpers.NewOrgName() + domain.CreateShared() + Eventually(helpers.CF("create-route", domain.Name, "--hostname", hostname)).Should(Exit(0)) + }) + + It("exists with 1 and an error message", func() { + session := helpers.CF("move-route", domainName, "--hostname", hostname, "-o", targetOrgName, "-s", targetSpaceName) + Eventually(session).Should(Say("Can not transfer ownership of route:")) + Eventually(session).Should(Say(`FAILED`)) + Eventually(session).Should(Exit(1)) + }) + }) + + When("the target space exists in another existing org", func() { + var targetOrgName string + BeforeEach(func() { + domain = helpers.NewDomain(orgName, domainName) + hostname = "menchies-icecream" + targetOrgName = helpers.NewOrgName() + targetSpaceName = helpers.NewSpaceName() + helpers.CreateOrgAndSpace(targetOrgName, targetSpaceName) + helpers.SetupCF(orgName, spaceName) + domain.CreateShared() + Eventually(helpers.CF("create-route", domain.Name, "--hostname", hostname)).Should(Exit(0)) + }) + + AfterEach(func() { + domain.DeleteShared() + }) + + It("Transfers ownership of the route to the destination space", func() { + session := helpers.CF("move-route", domainName, "--hostname", hostname, "-o", targetOrgName, "-s", targetSpaceName) + Eventually(session).Should(Say(`Move ownership of route %s.%s to space %s as %s`, hostname, domainName, targetSpaceName, userName)) + Eventually(session).Should(Say(`OK`)) + Eventually(session).Should(Exit(0)) + }) + }) + + When("the space does not exist", func() { + var destinationSpaceName string + BeforeEach(func() { + domain = helpers.NewDomain(orgName, domainName) + hostname = "menchies-icecream" + destinationSpaceName = "doesNotExistSpace" + domain.CreateShared() + Eventually(helpers.CF("create-route", domain.Name, "--hostname", hostname)).Should(Exit(0)) + }) + + It("exists with 1 with an error", func() { + session := helpers.CF("move-route", domainName, "--hostname", hostname, "-s", destinationSpaceName) + Eventually(session).Should(Say("Can not transfer ownership of route:")) + Eventually(session).Should(Say(`FAILED`)) + Eventually(session).Should(Exit(1)) + }) + }) + }) + + When("the route does not exist", func() { + var ( + domain helpers.Domain + hostname string + ) + + When("the target space exists", func() { + BeforeEach(func() { + domain = helpers.NewDomain(orgName, domainName) + hostname = "panera-bread" + targetSpaceName = helpers.NewSpaceName() + helpers.CreateSpace(targetSpaceName) + domain.CreateShared() + }) + + It("exits with 1 with an error message", func() { + session := helpers.CF("move-route", domainName, "--hostname", hostname, "-s", targetSpaceName) + Eventually(session).Should(Say("Can not transfer ownership of route:")) + Eventually(session).Should(Say(`FAILED`)) + Eventually(session).Should(Exit(1)) + }) + }) + }) + }) + }) +})