diff --git a/actor/actionerror/route_option_error.go b/actor/actionerror/route_option_error.go new file mode 100644 index 00000000000..005633e59e8 --- /dev/null +++ b/actor/actionerror/route_option_error.go @@ -0,0 +1,22 @@ +package actionerror + +import "fmt" + +// RouteOptionError is returned when a route option was specified in the wrong format +type RouteOptionError struct { + Name string + Host string + DomainName string + Path string +} + +func (e RouteOptionError) Error() string { + return fmt.Sprintf("Route option '%s' for route with host '%s', domain '%s', and path '%s' was specified incorrectly. Please use key-value pair format key=value.", e.Name, e.Host, e.DomainName, e.path()) +} + +func (e RouteOptionError) path() string { + if e.Path == "" { + return "/" + } + return e.Path +} diff --git a/actor/actionerror/route_option_support_error.go b/actor/actionerror/route_option_support_error.go new file mode 100644 index 00000000000..9fcffbb4054 --- /dev/null +++ b/actor/actionerror/route_option_support_error.go @@ -0,0 +1,12 @@ +package actionerror + +import "fmt" + +// RouteOptionSupportError is returned when route options are not supported +type RouteOptionSupportError struct { + ErrorText string +} + +func (e RouteOptionSupportError) Error() string { + return fmt.Sprintf("Route option support: '%s'", e.ErrorText) +} diff --git a/actor/v7action/cloud_controller_client.go b/actor/v7action/cloud_controller_client.go index e8793ed0ea4..1350fd09908 100644 --- a/actor/v7action/cloud_controller_client.go +++ b/actor/v7action/cloud_controller_client.go @@ -177,6 +177,7 @@ type CloudControllerClient interface { UpdateOrganizationQuota(orgQuota resources.OrganizationQuota) (resources.OrganizationQuota, ccv3.Warnings, error) UpdateProcess(process resources.Process) (resources.Process, ccv3.Warnings, error) UpdateResourceMetadata(resource string, resourceGUID string, metadata resources.Metadata) (ccv3.JobURL, ccv3.Warnings, error) + UpdateRoute(routeGUID string, options map[string]*string) (resources.Route, ccv3.Warnings, error) UpdateSecurityGroupRunningSpace(securityGroupGUID string, spaceGUIDs []string) (ccv3.Warnings, error) UpdateSecurityGroupStagingSpace(securityGroupGUID string, spaceGUIDs []string) (ccv3.Warnings, error) UpdateSecurityGroup(securityGroup resources.SecurityGroup) (resources.SecurityGroup, ccv3.Warnings, error) diff --git a/actor/v7action/route.go b/actor/v7action/route.go index 7bccf6bdb38..2f2c2f70fab 100644 --- a/actor/v7action/route.go +++ b/actor/v7action/route.go @@ -26,7 +26,7 @@ type RouteSummary struct { ServiceInstanceName string } -func (actor Actor) CreateRoute(spaceGUID, domainName, hostname, path string, port int) (resources.Route, Warnings, error) { +func (actor Actor) CreateRoute(spaceGUID, domainName, hostname, path string, port int, options map[string]*string) (resources.Route, Warnings, error) { allWarnings := Warnings{} domain, warnings, err := actor.GetDomainByName(domainName) allWarnings = append(allWarnings, warnings...) @@ -41,6 +41,7 @@ func (actor Actor) CreateRoute(spaceGUID, domainName, hostname, path string, por Host: hostname, Path: path, Port: port, + Options: options, }) actorWarnings := Warnings(apiWarnings) @@ -401,6 +402,11 @@ func (actor Actor) MapRoute(routeGUID string, appGUID string, destinationProtoco return Warnings(warnings), err } +func (actor Actor) UpdateRoute(routeGUID string, options map[string]*string) (resources.Route, Warnings, error) { + route, warnings, err := actor.CloudControllerClient.UpdateRoute(routeGUID, options) + return route, Warnings(warnings), err +} + func (actor Actor) UpdateDestination(routeGUID string, destinationGUID string, protocol string) (Warnings, error) { warnings, err := actor.CloudControllerClient.UpdateDestination(routeGUID, destinationGUID, protocol) return Warnings(warnings), err diff --git a/actor/v7action/route_test.go b/actor/v7action/route_test.go index 1143b1c1282..2600a0c9d29 100644 --- a/actor/v7action/route_test.go +++ b/actor/v7action/route_test.go @@ -33,16 +33,20 @@ var _ = Describe("Route Actions", func() { hostname string path string port int + options map[string]*string ) BeforeEach(func() { hostname = "" path = "" port = 0 + lbLCVal := "least-connections" + lbLeastConnections := &lbLCVal + options = map[string]*string{"loadbalancing": lbLeastConnections} }) JustBeforeEach(func() { - _, warnings, executeErr = actor.CreateRoute("space-guid", "domain-name", hostname, path, port) + _, warnings, executeErr = actor.CreateRoute("space-guid", "domain-name", hostname, path, port, options) }) When("the API layer calls are successful", func() { @@ -56,7 +60,7 @@ var _ = Describe("Route Actions", func() { ) fakeCloudControllerClient.CreateRouteReturns( - resources.Route{GUID: "route-guid", SpaceGUID: "space-guid", DomainGUID: "domain-guid", Host: "hostname", Path: "path-name"}, + resources.Route{GUID: "route-guid", SpaceGUID: "space-guid", DomainGUID: "domain-guid", Host: "hostname", Path: "path-name", Options: options}, ccv3.Warnings{"create-warning-1", "create-warning-2"}, nil) }) @@ -80,6 +84,7 @@ var _ = Describe("Route Actions", func() { DomainGUID: "domain-guid", Host: hostname, Path: path, + Options: options, }, )) }) @@ -102,6 +107,7 @@ var _ = Describe("Route Actions", func() { SpaceGUID: "space-guid", DomainGUID: "domain-guid", Port: 1234, + Options: options, }, )) }) diff --git a/actor/v7action/v7actionfakes/fake_cloud_controller_client.go b/actor/v7action/v7actionfakes/fake_cloud_controller_client.go index f9035d65ab1..0c532958586 100644 --- a/actor/v7action/v7actionfakes/fake_cloud_controller_client.go +++ b/actor/v7action/v7actionfakes/fake_cloud_controller_client.go @@ -2511,6 +2511,22 @@ type FakeCloudControllerClient struct { result2 ccv3.Warnings result3 error } + UpdateRouteStub func(string, map[string]*string) (resources.Route, ccv3.Warnings, error) + updateRouteMutex sync.RWMutex + updateRouteArgsForCall []struct { + arg1 string + arg2 map[string]*string + } + updateRouteReturns struct { + result1 resources.Route + result2 ccv3.Warnings + result3 error + } + updateRouteReturnsOnCall map[int]struct { + result1 resources.Route + result2 ccv3.Warnings + result3 error + } UpdateSecurityGroupStub func(resources.SecurityGroup) (resources.SecurityGroup, ccv3.Warnings, error) updateSecurityGroupMutex sync.RWMutex updateSecurityGroupArgsForCall []struct { @@ -13876,6 +13892,74 @@ func (fake *FakeCloudControllerClient) UpdateResourceMetadataReturnsOnCall(i int }{result1, result2, result3} } +func (fake *FakeCloudControllerClient) UpdateRoute(arg1 string, arg2 map[string]*string) (resources.Route, ccv3.Warnings, error) { + fake.updateRouteMutex.Lock() + ret, specificReturn := fake.updateRouteReturnsOnCall[len(fake.updateRouteArgsForCall)] + fake.updateRouteArgsForCall = append(fake.updateRouteArgsForCall, struct { + arg1 string + arg2 map[string]*string + }{arg1, arg2}) + stub := fake.UpdateRouteStub + fakeReturns := fake.updateRouteReturns + fake.recordInvocation("UpdateRoute", []interface{}{arg1, arg2}) + fake.updateRouteMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2, ret.result3 + } + return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3 +} + +func (fake *FakeCloudControllerClient) UpdateRouteCallCount() int { + fake.updateRouteMutex.RLock() + defer fake.updateRouteMutex.RUnlock() + return len(fake.updateRouteArgsForCall) +} + +func (fake *FakeCloudControllerClient) UpdateRouteCalls(stub func(string, map[string]*string) (resources.Route, ccv3.Warnings, error)) { + fake.updateRouteMutex.Lock() + defer fake.updateRouteMutex.Unlock() + fake.UpdateRouteStub = stub +} + +func (fake *FakeCloudControllerClient) UpdateRouteArgsForCall(i int) (string, map[string]*string) { + fake.updateRouteMutex.RLock() + defer fake.updateRouteMutex.RUnlock() + argsForCall := fake.updateRouteArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeCloudControllerClient) UpdateRouteReturns(result1 resources.Route, result2 ccv3.Warnings, result3 error) { + fake.updateRouteMutex.Lock() + defer fake.updateRouteMutex.Unlock() + fake.UpdateRouteStub = nil + fake.updateRouteReturns = struct { + result1 resources.Route + result2 ccv3.Warnings + result3 error + }{result1, result2, result3} +} + +func (fake *FakeCloudControllerClient) UpdateRouteReturnsOnCall(i int, result1 resources.Route, result2 ccv3.Warnings, result3 error) { + fake.updateRouteMutex.Lock() + defer fake.updateRouteMutex.Unlock() + fake.UpdateRouteStub = nil + if fake.updateRouteReturnsOnCall == nil { + fake.updateRouteReturnsOnCall = make(map[int]struct { + result1 resources.Route + result2 ccv3.Warnings + result3 error + }) + } + fake.updateRouteReturnsOnCall[i] = struct { + result1 resources.Route + result2 ccv3.Warnings + result3 error + }{result1, result2, result3} +} + func (fake *FakeCloudControllerClient) UpdateSecurityGroup(arg1 resources.SecurityGroup) (resources.SecurityGroup, ccv3.Warnings, error) { fake.updateSecurityGroupMutex.Lock() ret, specificReturn := fake.updateSecurityGroupReturnsOnCall[len(fake.updateSecurityGroupArgsForCall)] @@ -15372,6 +15456,8 @@ func (fake *FakeCloudControllerClient) Invocations() map[string][][]interface{} defer fake.updateProcessMutex.RUnlock() fake.updateResourceMetadataMutex.RLock() defer fake.updateResourceMetadataMutex.RUnlock() + fake.updateRouteMutex.RLock() + defer fake.updateRouteMutex.RUnlock() fake.updateSecurityGroupMutex.RLock() defer fake.updateSecurityGroupMutex.RUnlock() fake.updateSecurityGroupRunningSpaceMutex.RLock() diff --git a/actor/v7pushaction/v7_actor.go b/actor/v7pushaction/v7_actor.go index 91356c9df46..e113b13568f 100644 --- a/actor/v7pushaction/v7_actor.go +++ b/actor/v7pushaction/v7_actor.go @@ -16,7 +16,7 @@ type V7Actor interface { CreateBitsPackageByApplication(appGUID string) (resources.Package, v7action.Warnings, error) CreateDeployment(dep resources.Deployment) (string, v7action.Warnings, error) CreateDockerPackageByApplication(appGUID string, dockerImageCredentials v7action.DockerImageCredentials) (resources.Package, v7action.Warnings, error) - CreateRoute(spaceGUID, domainName, hostname, path string, port int) (resources.Route, v7action.Warnings, error) + CreateRoute(spaceGUID, domainName, hostname, path string, port int, options map[string]*string) (resources.Route, v7action.Warnings, error) GetApplicationByNameAndSpace(appName string, spaceGUID string) (resources.Application, v7action.Warnings, error) GetApplicationDroplets(appName string, spaceGUID string) ([]resources.Droplet, v7action.Warnings, error) GetApplicationRoutes(appGUID string) ([]resources.Route, v7action.Warnings, error) @@ -41,6 +41,7 @@ type V7Actor interface { UnmapRoute(routeGUID string, destinationGUID string) (v7action.Warnings, error) UpdateApplication(app resources.Application) (resources.Application, v7action.Warnings, error) UpdateProcessByTypeAndApplication(processType string, appGUID string, updatedProcess resources.Process) (v7action.Warnings, error) + UpdateRoute(routeGUID string, options map[string]*string) (resources.Route, v7action.Warnings, error) UploadBitsPackage(pkg resources.Package, matchedResources []sharedaction.V3Resource, newResources io.Reader, newResourcesLength int64) (resources.Package, v7action.Warnings, error) UploadDroplet(dropletGUID string, dropletPath string, progressReader io.Reader, fileSize int64) (v7action.Warnings, error) } diff --git a/actor/v7pushaction/v7pushactionfakes/fake_v7actor.go b/actor/v7pushaction/v7pushactionfakes/fake_v7actor.go index 76d8d4d185d..1b99cb41074 100644 --- a/actor/v7pushaction/v7pushactionfakes/fake_v7actor.go +++ b/actor/v7pushaction/v7pushactionfakes/fake_v7actor.go @@ -89,7 +89,7 @@ type FakeV7Actor struct { result2 v7action.Warnings result3 error } - CreateRouteStub func(string, string, string, string, int) (resources.Route, v7action.Warnings, error) + CreateRouteStub func(string, string, string, string, int, map[string]*string) (resources.Route, v7action.Warnings, error) createRouteMutex sync.RWMutex createRouteArgsForCall []struct { arg1 string @@ -97,6 +97,7 @@ type FakeV7Actor struct { arg3 string arg4 string arg5 int + arg6 map[string]*string } createRouteReturns struct { result1 resources.Route @@ -467,6 +468,22 @@ type FakeV7Actor struct { result1 v7action.Warnings result2 error } + UpdateRouteStub func(string, map[string]*string) (resources.Route, v7action.Warnings, error) + updateRouteMutex sync.RWMutex + updateRouteArgsForCall []struct { + arg1 string + arg2 map[string]*string + } + updateRouteReturns struct { + result1 resources.Route + result2 v7action.Warnings + result3 error + } + updateRouteReturnsOnCall map[int]struct { + result1 resources.Route + result2 v7action.Warnings + result3 error + } UploadBitsPackageStub func(resources.Package, []sharedaction.V3Resource, io.Reader, int64) (resources.Package, v7action.Warnings, error) uploadBitsPackageMutex sync.RWMutex uploadBitsPackageArgsForCall []struct { @@ -842,7 +859,7 @@ func (fake *FakeV7Actor) CreateDockerPackageByApplicationReturnsOnCall(i int, re }{result1, result2, result3} } -func (fake *FakeV7Actor) CreateRoute(arg1 string, arg2 string, arg3 string, arg4 string, arg5 int) (resources.Route, v7action.Warnings, error) { +func (fake *FakeV7Actor) CreateRoute(arg1 string, arg2 string, arg3 string, arg4 string, arg5 int, arg6 map[string]*string) (resources.Route, v7action.Warnings, error) { fake.createRouteMutex.Lock() ret, specificReturn := fake.createRouteReturnsOnCall[len(fake.createRouteArgsForCall)] fake.createRouteArgsForCall = append(fake.createRouteArgsForCall, struct { @@ -851,13 +868,14 @@ func (fake *FakeV7Actor) CreateRoute(arg1 string, arg2 string, arg3 string, arg4 arg3 string arg4 string arg5 int - }{arg1, arg2, arg3, arg4, arg5}) + arg6 map[string]*string + }{arg1, arg2, arg3, arg4, arg5, arg6}) stub := fake.CreateRouteStub fakeReturns := fake.createRouteReturns - fake.recordInvocation("CreateRoute", []interface{}{arg1, arg2, arg3, arg4, arg5}) + fake.recordInvocation("CreateRoute", []interface{}{arg1, arg2, arg3, arg4, arg5, arg6}) fake.createRouteMutex.Unlock() if stub != nil { - return stub(arg1, arg2, arg3, arg4, arg5) + return stub(arg1, arg2, arg3, arg4, arg5, arg6) } if specificReturn { return ret.result1, ret.result2, ret.result3 @@ -871,17 +889,17 @@ func (fake *FakeV7Actor) CreateRouteCallCount() int { return len(fake.createRouteArgsForCall) } -func (fake *FakeV7Actor) CreateRouteCalls(stub func(string, string, string, string, int) (resources.Route, v7action.Warnings, error)) { +func (fake *FakeV7Actor) CreateRouteCalls(stub func(string, string, string, string, int, map[string]*string) (resources.Route, v7action.Warnings, error)) { fake.createRouteMutex.Lock() defer fake.createRouteMutex.Unlock() fake.CreateRouteStub = stub } -func (fake *FakeV7Actor) CreateRouteArgsForCall(i int) (string, string, string, string, int) { +func (fake *FakeV7Actor) CreateRouteArgsForCall(i int) (string, string, string, string, int, map[string]*string) { fake.createRouteMutex.RLock() defer fake.createRouteMutex.RUnlock() argsForCall := fake.createRouteArgsForCall[i] - return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4, argsForCall.arg5 + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4, argsForCall.arg5, argsForCall.arg6 } func (fake *FakeV7Actor) CreateRouteReturns(result1 resources.Route, result2 v7action.Warnings, result3 error) { @@ -2528,6 +2546,74 @@ func (fake *FakeV7Actor) UpdateProcessByTypeAndApplicationReturnsOnCall(i int, r }{result1, result2} } +func (fake *FakeV7Actor) UpdateRoute(arg1 string, arg2 map[string]*string) (resources.Route, v7action.Warnings, error) { + fake.updateRouteMutex.Lock() + ret, specificReturn := fake.updateRouteReturnsOnCall[len(fake.updateRouteArgsForCall)] + fake.updateRouteArgsForCall = append(fake.updateRouteArgsForCall, struct { + arg1 string + arg2 map[string]*string + }{arg1, arg2}) + stub := fake.UpdateRouteStub + fakeReturns := fake.updateRouteReturns + fake.recordInvocation("UpdateRoute", []interface{}{arg1, arg2}) + fake.updateRouteMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2, ret.result3 + } + return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3 +} + +func (fake *FakeV7Actor) UpdateRouteCallCount() int { + fake.updateRouteMutex.RLock() + defer fake.updateRouteMutex.RUnlock() + return len(fake.updateRouteArgsForCall) +} + +func (fake *FakeV7Actor) UpdateRouteCalls(stub func(string, map[string]*string) (resources.Route, v7action.Warnings, error)) { + fake.updateRouteMutex.Lock() + defer fake.updateRouteMutex.Unlock() + fake.UpdateRouteStub = stub +} + +func (fake *FakeV7Actor) UpdateRouteArgsForCall(i int) (string, map[string]*string) { + fake.updateRouteMutex.RLock() + defer fake.updateRouteMutex.RUnlock() + argsForCall := fake.updateRouteArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeV7Actor) UpdateRouteReturns(result1 resources.Route, result2 v7action.Warnings, result3 error) { + fake.updateRouteMutex.Lock() + defer fake.updateRouteMutex.Unlock() + fake.UpdateRouteStub = nil + fake.updateRouteReturns = struct { + result1 resources.Route + result2 v7action.Warnings + result3 error + }{result1, result2, result3} +} + +func (fake *FakeV7Actor) UpdateRouteReturnsOnCall(i int, result1 resources.Route, result2 v7action.Warnings, result3 error) { + fake.updateRouteMutex.Lock() + defer fake.updateRouteMutex.Unlock() + fake.UpdateRouteStub = nil + if fake.updateRouteReturnsOnCall == nil { + fake.updateRouteReturnsOnCall = make(map[int]struct { + result1 resources.Route + result2 v7action.Warnings + result3 error + }) + } + fake.updateRouteReturnsOnCall[i] = struct { + result1 resources.Route + result2 v7action.Warnings + result3 error + }{result1, result2, result3} +} + func (fake *FakeV7Actor) UploadBitsPackage(arg1 resources.Package, arg2 []sharedaction.V3Resource, arg3 io.Reader, arg4 int64) (resources.Package, v7action.Warnings, error) { var arg2Copy []sharedaction.V3Resource if arg2 != nil { @@ -2733,6 +2819,8 @@ func (fake *FakeV7Actor) Invocations() map[string][][]interface{} { defer fake.updateApplicationMutex.RUnlock() fake.updateProcessByTypeAndApplicationMutex.RLock() defer fake.updateProcessByTypeAndApplicationMutex.RUnlock() + fake.updateRouteMutex.RLock() + defer fake.updateRouteMutex.RUnlock() fake.uploadBitsPackageMutex.RLock() defer fake.uploadBitsPackageMutex.RUnlock() fake.uploadDropletMutex.RLock() diff --git a/api/cloudcontroller/ccv3/internal/api_routes.go b/api/cloudcontroller/ccv3/internal/api_routes.go index 05d5607eb33..3f1c78c61ba 100644 --- a/api/cloudcontroller/ccv3/internal/api_routes.go +++ b/api/cloudcontroller/ccv3/internal/api_routes.go @@ -107,6 +107,7 @@ const ( GetUserRequest = "GetUser" GetUsersRequest = "GetUsers" MapRouteRequest = "MapRoute" + UpdateRouteRequest = "UpdateRoute" PatchApplicationCurrentDropletRequest = "PatchApplicationCurrentDroplet" PatchApplicationEnvironmentVariablesRequest = "PatchApplicationEnvironmentVariables" PatchApplicationRequest = "PatchApplication" @@ -281,6 +282,7 @@ var APIRoutes = map[string]Route{ PatchRouteRequest: {Path: "/v3/routes/:route_guid", Method: http.MethodPatch}, GetRouteDestinationsRequest: {Path: "/v3/routes/:route_guid/destinations", Method: http.MethodGet}, MapRouteRequest: {Path: "/v3/routes/:route_guid/destinations", Method: http.MethodPost}, + UpdateRouteRequest: {Path: "/v3/routes/:route_guid", Method: http.MethodPatch}, 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}, diff --git a/api/cloudcontroller/ccv3/route.go b/api/cloudcontroller/ccv3/route.go index afff0af5e9a..399bee2f3a7 100644 --- a/api/cloudcontroller/ccv3/route.go +++ b/api/cloudcontroller/ccv3/route.go @@ -82,6 +82,24 @@ func (client Client) GetRoutes(query ...Query) ([]resources.Route, Warnings, err return routes, warnings, err } +func (client Client) UpdateRoute(routeGUID string, options map[string]*string) (resources.Route, Warnings, error) { + var responseBody resources.Route + var route = resources.Route{} + var uriParams = internal.Params{"route_guid": routeGUID} + + route.Options = options + + _, warnings, err := client.MakeRequest(RequestParams{ + RequestName: internal.UpdateRouteRequest, + URIParams: uriParams, + RequestBody: route, + ResponseBody: &responseBody, + }) + + return responseBody, warnings, err + +} + func (client Client) MapRoute(routeGUID string, appGUID string, destinationProtocol string) (Warnings, error) { type destinationProcess struct { ProcessType string `json:"process_type"` diff --git a/api/cloudcontroller/ccv3/route_test.go b/api/cloudcontroller/ccv3/route_test.go index c1597c71db9..efe7f60adc1 100644 --- a/api/cloudcontroller/ccv3/route_test.go +++ b/api/cloudcontroller/ccv3/route_test.go @@ -31,6 +31,7 @@ var _ = Describe("Route", func() { path string port int ccv3Route resources.Route + options map[string]*string ) BeforeEach(func() { @@ -42,7 +43,11 @@ var _ = Describe("Route", func() { JustBeforeEach(func() { spaceGUID = "space-guid" domainGUID = "domain-guid" - ccv3Route = resources.Route{SpaceGUID: spaceGUID, DomainGUID: domainGUID, Host: host, Path: path, Port: port} + lbLCVal := "least-connections" + lbLeastConnections := &lbLCVal + options = map[string]*string{"loadbalancing": lbLeastConnections} + + ccv3Route = resources.Route{SpaceGUID: spaceGUID, DomainGUID: domainGUID, Host: host, Path: path, Port: port, Options: options} route, warnings, executeErr = client.CreateRoute(ccv3Route) }) @@ -60,18 +65,28 @@ var _ = Describe("Route", func() { "data": { "guid": "domain-guid" } } }, - "host": "" + "options": { + "loadbalancing": "least-connections" + }, + "host": "" }` expectedBody := `{ - "relationships": { - "space": { - "data": { "guid": "space-guid" } + "relationships": { + "space": { + "data": { + "guid": "space-guid" + } + }, + "domain": { + "data": { + "guid": "domain-guid" + } + } }, - "domain": { - "data": { "guid": "domain-guid" } + "options": { + "loadbalancing": "least-connections" } - } }` server.AppendHandlers( @@ -91,6 +106,7 @@ var _ = Describe("Route", func() { GUID: "some-route-guid", SpaceGUID: "space-guid", DomainGUID: "domain-guid", + Options: options, })) }) }) @@ -109,7 +125,10 @@ var _ = Describe("Route", func() { "data": { "guid": "domain-guid" } } }, - "host": "cheesecake" + "options": { + "loadbalancing": "least-connections" + }, + "host": "cheesecake" }` expectedBody := `{ @@ -121,7 +140,10 @@ var _ = Describe("Route", func() { "data": { "guid": "domain-guid" } } }, - "host": "cheesecake" + "options": { + "loadbalancing": "least-connections" + }, + "host": "cheesecake" }` server.AppendHandlers( @@ -142,6 +164,7 @@ var _ = Describe("Route", func() { SpaceGUID: "space-guid", DomainGUID: "domain-guid", Host: "cheesecake", + Options: options, })) }) }) @@ -164,7 +187,10 @@ var _ = Describe("Route", func() { } } }, - "path": "lion" + "path": "lion", + "options": { + "loadbalancing": "least-connections" + } }` expectedRequestBody := `{ "relationships": { @@ -179,7 +205,10 @@ var _ = Describe("Route", func() { } } }, - "path": "lion" + "path": "lion", + "options": { + "loadbalancing": "least-connections" + } }` server.AppendHandlers( @@ -200,6 +229,7 @@ var _ = Describe("Route", func() { SpaceGUID: "space-guid", DomainGUID: "domain-guid", Path: "lion", + Options: options, })) }) }) @@ -223,6 +253,9 @@ var _ = Describe("Route", func() { } } }, + "options": { + "loadbalancing": "least-connections" + }, "port": 1234 }` expectedRequestBody := `{ @@ -238,6 +271,9 @@ var _ = Describe("Route", func() { } } }, + "options": { + "loadbalancing": "least-connections" + }, "port": 1234 }` @@ -259,6 +295,7 @@ var _ = Describe("Route", func() { SpaceGUID: "space-guid", DomainGUID: "domain-guid", Port: 1234, + Options: options, })) }) }) diff --git a/api/cloudcontroller/ccversion/minimum_version.go b/api/cloudcontroller/ccversion/minimum_version.go index 35f606511e4..6f2ababb1fd 100644 --- a/api/cloudcontroller/ccversion/minimum_version.go +++ b/api/cloudcontroller/ccversion/minimum_version.go @@ -15,4 +15,5 @@ const ( MinVersionSpaceSupporterV3 = "3.104.0" MinVersionLogRateLimitingV3 = "3.125.0" + MinVersionPerRouteOpts = "3.183.0" ) diff --git a/command/common/command_list_v7.go b/command/common/command_list_v7.go index bba46d9fa91..da3513fd39c 100644 --- a/command/common/command_list_v7.go +++ b/command/common/command_list_v7.go @@ -43,6 +43,7 @@ type commandList struct { CreateOrgQuota v7.CreateOrgQuotaCommand `command:"create-org-quota" alias:"create-quota" description:"Define a new quota for an organization"` CreatePrivateDomain v7.CreatePrivateDomainCommand `command:"create-private-domain" alias:"create-domain" description:"Create a private domain for a specific org"` CreateRoute v7.CreateRouteCommand `command:"create-route" description:"Create a route for later use"` + UpdateRoute v7.UpdateRouteCommand `command:"update-route" description:"Update a route by route specific options, e.g. load balancing algorithm"` CreateSecurityGroup v7.CreateSecurityGroupCommand `command:"create-security-group" description:"Create a security group"` CreateService v7.CreateServiceCommand `command:"create-service" alias:"cs" description:"Create a service instance"` CreateServiceBroker v7.CreateServiceBrokerCommand `command:"create-service-broker" alias:"csb" description:"Create a service broker"` diff --git a/command/common/internal/help_all_display.go b/command/common/internal/help_all_display.go index 9de9243d3fc..5de55da5289 100644 --- a/command/common/internal/help_all_display.go +++ b/command/common/internal/help_all_display.go @@ -66,7 +66,7 @@ var HelpCategoryList = []HelpCategory{ CategoryName: "ROUTES:", CommandList: [][]string{ {"routes", "route"}, - {"create-route", "check-route", "map-route", "unmap-route", "delete-route"}, + {"create-route", "update-route", "check-route", "map-route", "unmap-route", "delete-route"}, {"delete-orphaned-routes"}, {"update-destination"}, {"share-route", "unshare-route"}, diff --git a/command/v7/actor.go b/command/v7/actor.go index d75347909ed..026669f8d62 100644 --- a/command/v7/actor.go +++ b/command/v7/actor.go @@ -45,7 +45,7 @@ type Actor interface { CreateOrganization(orgName string) (resources.Organization, v7action.Warnings, error) CreateOrganizationQuota(name string, limits v7action.QuotaLimits) (v7action.Warnings, error) CreatePrivateDomain(domainName string, orgName string) (v7action.Warnings, error) - CreateRoute(spaceGUID, domainName, hostname, path string, port int) (resources.Route, v7action.Warnings, error) + CreateRoute(spaceGUID, domainName, hostname, path string, port int, options map[string]*string) (resources.Route, v7action.Warnings, error) CreateRouteBinding(params v7action.CreateRouteBindingParams) (chan v7action.PollJobEvent, v7action.Warnings, error) CreateSecurityGroup(name, filePath string) (v7action.Warnings, error) CreateServiceAppBinding(params v7action.CreateServiceAppBindingParams) (chan v7action.PollJobEvent, v7action.Warnings, error) @@ -244,6 +244,7 @@ type Actor interface { UpdateOrganizationLabelsByOrganizationName(string, map[string]types.NullString) (v7action.Warnings, error) UpdateOrganizationQuota(quotaName string, newName string, limits v7action.QuotaLimits) (v7action.Warnings, error) UpdateProcessByTypeAndApplication(processType string, appGUID string, updatedProcess resources.Process) (v7action.Warnings, error) + UpdateRoute(routeGUID string, options map[string]*string) (resources.Route, v7action.Warnings, error) UpdateRouteLabels(string, string, map[string]types.NullString) (v7action.Warnings, error) UpdateSecurityGroup(name, filePath string) (v7action.Warnings, error) UpdateSecurityGroupGloballyEnabled(securityGroupName string, lifecycle constant.SecurityGroupLifecycle, enabled bool) (v7action.Warnings, error) diff --git a/command/v7/apps_command.go b/command/v7/apps_command.go index 84d936652bd..3f6ee31d8dc 100644 --- a/command/v7/apps_command.go +++ b/command/v7/apps_command.go @@ -79,7 +79,7 @@ func (cmd AppsCommand) Execute(args []string) error { func getURLs(routes []resources.Route) string { var routeURLs []string for _, route := range routes { - routeURLs = append(routeURLs, route.URL) + routeURLs = append(routeURLs, route.URL+route.FormattedOptions()) } return strings.Join(routeURLs, ", ") diff --git a/command/v7/apps_command_test.go b/command/v7/apps_command_test.go index 4402ab40bd5..96e92c721bc 100644 --- a/command/v7/apps_command_test.go +++ b/command/v7/apps_command_test.go @@ -144,6 +144,8 @@ var _ = Describe("apps Command", func() { When("the route actor does not return any errors", func() { Context("with existing apps", func() { BeforeEach(func() { + lbLCVal := "least-connections" + lbLeastConnections := &lbLCVal appSummaries := []v7action.ApplicationSummary{ { Application: resources.Application{ @@ -187,8 +189,9 @@ var _ = Describe("apps Command", func() { }, Routes: []resources.Route{ { - Host: "some-app-1", - URL: "some-app-1.some-other-domain", + Host: "some-app-1", + URL: "some-app-1.some-other-domain", + Options: map[string]*string{"loadbalancing": lbLeastConnections}, }, { Host: "some-app-1", @@ -236,8 +239,7 @@ var _ = Describe("apps Command", func() { Expect(testUI.Out).To(Say(`Getting apps in org some-org / space some-space as steve\.\.\.`)) Expect(testUI.Out).To(Say(`name\s+requested state\s+processes\s+routes`)) - Expect(testUI.Out).To(Say(`some-app-1\s+started\s+web:2/2, console:0/0, worker:0/1\s+some-app-1.some-other-domain, some-app-1.some-domain`)) - Expect(testUI.Out).To(Say(`some-app-2\s+stopped\s+web:0/2\s+some-app-2.some-domain`)) + Expect(testUI.Out).To(Say(`some-app-1\s+started\s+web:2/2, console:0/0, worker:0/1\s+some-app-1.some-other-domain {loadbalancing=least-connections}, some-app-1.some-domain`)) Expect(testUI.Err).To(Say("warning-1")) Expect(testUI.Err).To(Say("warning-2")) diff --git a/command/v7/create_route_command.go b/command/v7/create_route_command.go index 7d6fc07e6bd..95928e35ded 100644 --- a/command/v7/create_route_command.go +++ b/command/v7/create_route_command.go @@ -3,6 +3,10 @@ package v7 import ( "fmt" + "code.cloudfoundry.org/cli/api/cloudcontroller/ccversion" + "code.cloudfoundry.org/cli/command" + "code.cloudfoundry.org/cli/resources" + "code.cloudfoundry.org/cli/actor/actionerror" "code.cloudfoundry.org/cli/command/flag" ) @@ -11,11 +15,29 @@ type CreateRouteCommand struct { BaseCommand RequiredArgs flag.Domain `positional-args:"yes"` - usage interface{} `usage:"Create an HTTP route:\n CF_NAME create-route DOMAIN [--hostname HOSTNAME] [--path PATH]\n\n Create a TCP route:\n CF_NAME create-route DOMAIN [--port PORT]\n\nEXAMPLES:\n CF_NAME create-route example.com # example.com\n CF_NAME create-route example.com --hostname myapp # myapp.example.com\n CF_NAME create-route example.com --hostname myapp --path foo # myapp.example.com/foo\n CF_NAME create-route example.com --port 5000 # example.com:5000"` 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"` Port int `long:"port" description:"Port for the TCP route (default: random port)"` - relatedCommands interface{} `related_commands:"check-route, domains, map-route, routes, unmap-route"` + Options []string `long:"option" short:"o" description:"Set the value of a per-route option"` + relatedCommands interface{} `related_commands:"check-route, update-route, domains, map-route, routes, unmap-route"` +} + +func (cmd CreateRouteCommand) Usage() string { + return ` +Create an HTTP route: + CF_NAME create-route DOMAIN [--hostname HOSTNAME] [--path PATH] [--option OPTION=VALUE] +Create a TCP route: + CF_NAME create-route DOMAIN [--port PORT] [--option OPTION=VALUE]` +} + +func (cmd CreateRouteCommand) Examples() string { + return ` +CF_NAME create-route example.com # example.com +CF_NAME create-route example.com --hostname myapp # myapp.example.com +CF_NAME create-route example.com --hostname myapp --path foo # myapp.example.com/foo +CF_NAME create-route example.com --port 5000 # example.com:5000 +CF_NAME create-route example.com --hostname myapp -o loadbalancing=least-connections # myapp.example.com with a per-route option +` } func (cmd CreateRouteCommand) Execute(args []string) error { @@ -46,7 +68,21 @@ func (cmd CreateRouteCommand) Execute(args []string) error { "Organization": orgName, }) - route, warnings, err := cmd.Actor.CreateRoute(spaceGUID, domain, hostname, pathName, port) + err = cmd.validateAPIVersionForPerRouteOptions() + if err != nil { + return err + } + + routeOptions, wrongOptSpec := resources.CreateRouteOptions(cmd.Options) + if wrongOptSpec != nil { + return actionerror.RouteOptionError{ + Name: *wrongOptSpec, + DomainName: domain, + Path: pathName, + Host: hostname, + } + } + route, warnings, err := cmd.Actor.CreateRoute(spaceGUID, domain, hostname, pathName, port, routeOptions) cmd.UI.DisplayWarnings(warnings) if err != nil { @@ -86,3 +122,14 @@ func desiredURL(domain, hostname, path string, port int) string { return url } + +func (cmd CreateRouteCommand) validateAPIVersionForPerRouteOptions() error { + err := command.MinimumCCAPIVersionCheck(cmd.Config.APIVersion(), ccversion.MinVersionPerRouteOpts) + if err != nil { + cmd.UI.DisplayWarning("Your CC API version ({{.APIVersion}}) does not support per-route options. Those will be ignored. Upgrade to a newer version of the API (minimum version {{.MinSupportedVersion}}).", map[string]interface{}{ + "APIVersion": cmd.Config.APIVersion(), + "MinSupportedVersion": ccversion.MinVersionPerRouteOpts, + }) + } + return err +} diff --git a/command/v7/create_route_command_test.go b/command/v7/create_route_command_test.go index 25e808f69a1..d708ad411f3 100644 --- a/command/v7/create_route_command_test.go +++ b/command/v7/create_route_command_test.go @@ -3,9 +3,11 @@ package v7_test import ( "errors" "fmt" + "strconv" "code.cloudfoundry.org/cli/actor/actionerror" "code.cloudfoundry.org/cli/actor/v7action" + "code.cloudfoundry.org/cli/api/cloudcontroller/ccversion" "code.cloudfoundry.org/cli/command/commandfakes" "code.cloudfoundry.org/cli/command/flag" . "code.cloudfoundry.org/cli/command/v7" @@ -29,19 +31,23 @@ var _ = Describe("create-route Command", func() { executeErr error - binaryName string - domainName string - spaceName string - spaceGUID string - orgName string - hostname string - path string - port int + binaryName string + domainName string + spaceName string + spaceGUID string + orgName string + hostname string + path string + port int + cmdOptions []string + options map[string]*string + cCAPIOldVersion string ) BeforeEach(func() { testUI = ui.NewTestUI(nil, NewBuffer(), NewBuffer()) fakeConfig = new(commandfakes.FakeConfig) + fakeConfig.APIVersionReturns(ccversion.MinVersionPerRouteOpts) fakeSharedActor = new(commandfakes.FakeSharedActor) fakeActor = new(v7fakes.FakeActor) @@ -53,6 +59,11 @@ var _ = Describe("create-route Command", func() { path = "" port = 0 + cmdOptions = []string{"loadbalancing=least-connections"} + lbLCVal := "least-connections" + lbLeastConnections := &lbLCVal + options = map[string]*string{"loadbalancing": lbLeastConnections} + binaryName = "faceman" fakeConfig.BinaryNameReturns(binaryName) }) @@ -65,6 +76,7 @@ var _ = Describe("create-route Command", func() { Hostname: hostname, Path: flag.V7RoutePath{Path: path}, Port: port, + Options: cmdOptions, BaseCommand: BaseCommand{ UI: testUI, Config: fakeConfig, @@ -152,10 +164,36 @@ var _ = Describe("create-route Command", func() { }) }) + When("creating the route fails when the CC API version is too old for route options", func() { + BeforeEach(func() { + cCAPIOldVersion = strconv.Itoa(1) + fakeConfig.APIVersionReturns(cCAPIOldVersion) + }) + + It("does not create a route and gives error message", func() { + Expect(executeErr).To(HaveOccurred()) + Expect(fakeActor.CreateRouteCallCount()).To(Equal(0)) + Expect(testUI.Err).To(Say("Your CC API")) + Expect(testUI.Err).To(Say("does not support per-route options")) + }) + }) + + When("creating the route fails when route options are specified incorrectly", func() { + BeforeEach(func() { + cmdOptions = []string{"loadbalancing"} + }) + + It("does not create a route and gives an error message", func() { + Expect(executeErr).To(MatchError(actionerror.RouteOptionError{Name: "loadbalancing", DomainName: domainName, Path: path, Host: hostname})) + Expect(fakeActor.CreateRouteCallCount()).To(Equal(0)) + }) + }) + When("creating the route is successful", func() { BeforeEach(func() { fakeActor.CreateRouteReturns(resources.Route{ - URL: domainName, + URL: domainName, + Options: options, }, v7action.Warnings{"warnings-1", "warnings-2"}, nil) }) @@ -169,10 +207,11 @@ var _ = Describe("create-route Command", func() { It("creates the route", func() { Expect(fakeActor.CreateRouteCallCount()).To(Equal(1)) - expectedSpaceGUID, expectedDomainName, expectedHostname, _, _ := fakeActor.CreateRouteArgsForCall(0) + expectedSpaceGUID, expectedDomainName, expectedHostname, _, _, expectedOptions := fakeActor.CreateRouteArgsForCall(0) Expect(expectedSpaceGUID).To(Equal(spaceGUID)) Expect(expectedDomainName).To(Equal(domainName)) Expect(expectedHostname).To(Equal(hostname)) + Expect(expectedOptions).To(Equal(options)) }) When("passing in a hostname", func() { @@ -194,7 +233,7 @@ var _ = Describe("create-route Command", func() { It("creates the route", func() { Expect(fakeActor.CreateRouteCallCount()).To(Equal(1)) - expectedSpaceGUID, expectedDomainName, expectedHostname, _, _ := fakeActor.CreateRouteArgsForCall(0) + expectedSpaceGUID, expectedDomainName, expectedHostname, _, _, _ := fakeActor.CreateRouteArgsForCall(0) Expect(expectedSpaceGUID).To(Equal(spaceGUID)) Expect(expectedDomainName).To(Equal(domainName)) Expect(expectedHostname).To(Equal(hostname)) @@ -220,11 +259,12 @@ var _ = Describe("create-route Command", func() { It("calls the actor with the correct arguments", func() { Expect(fakeActor.CreateRouteCallCount()).To(Equal(1)) - expectedSpaceGUID, expectedDomainName, expectedHostname, _, expectedPort := fakeActor.CreateRouteArgsForCall(0) + expectedSpaceGUID, expectedDomainName, expectedHostname, _, expectedPort, expectedOptions := fakeActor.CreateRouteArgsForCall(0) Expect(expectedSpaceGUID).To(Equal(spaceGUID)) Expect(expectedDomainName).To(Equal(domainName)) Expect(expectedHostname).To(Equal(hostname)) Expect(expectedPort).To(Equal(port)) + Expect(expectedOptions).To(Equal(options)) }) }) }) diff --git a/command/v7/map_route_command.go b/command/v7/map_route_command.go index c9ca6b381ac..107afd98856 100644 --- a/command/v7/map_route_command.go +++ b/command/v7/map_route_command.go @@ -2,37 +2,41 @@ package v7 import ( "code.cloudfoundry.org/cli/actor/actionerror" + "code.cloudfoundry.org/cli/api/cloudcontroller/ccversion" + "code.cloudfoundry.org/cli/command" "code.cloudfoundry.org/cli/command/flag" + "code.cloudfoundry.org/cli/resources" ) type MapRouteCommand struct { BaseCommand - RequiredArgs flag.AppDomain `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"` - Port int `long:"port" description:"Port for the TCP route (default: random port)"` - AppProtocol string `long:"app-protocol" description:"[Beta flag, subject to change] Protocol for the route destination (default: http1). Only applied to HTTP routes"` - - relatedCommands interface{} `related_commands:"create-route, routes, unmap-route"` + RequiredArgs flag.AppDomain `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"` + Port int `long:"port" description:"Port for the TCP route (default: random port)"` + AppProtocol string `long:"app-protocol" description:"[Beta flag, subject to change] Protocol for the route destination (default: http1). Only applied to HTTP routes"` + Options []string `long:"option" short:"o" description:"Set the value of a per-route option"` + relatedCommands interface{} `related_commands:"create-route, update-route, routes, unmap-route"` } func (cmd MapRouteCommand) Usage() string { return ` Map an HTTP route: - CF_NAME map-route APP_NAME DOMAIN [--hostname HOSTNAME] [--path PATH] [--app-protocol PROTOCOL] + CF_NAME map-route APP_NAME DOMAIN [--hostname HOSTNAME] [--path PATH] [--app-protocol PROTOCOL] [--option OPTION=VALUE] Map a TCP route: - CF_NAME map-route APP_NAME DOMAIN [--port PORT]` + CF_NAME map-route APP_NAME DOMAIN [--port PORT] [--option OPTION=VALUE]` } func (cmd MapRouteCommand) Examples() string { return ` -CF_NAME map-route my-app example.com # example.com -CF_NAME map-route my-app example.com --hostname myhost # myhost.example.com -CF_NAME map-route my-app example.com --hostname myhost --path foo # myhost.example.com/foo -CF_NAME map-route my-app example.com --hostname myhost --app-protocol http2 # myhost.example.com -CF_NAME map-route my-app example.com --port 5000 # example.com:5000` +CF_NAME map-route my-app example.com # example.com +CF_NAME map-route my-app example.com --hostname myhost # myhost.example.com +CF_NAME map-route my-app example.com --hostname myhost -o loadbalancing=least-connections # myhost.example.com with a per-route option +CF_NAME map-route my-app example.com --hostname myhost --path foo # myhost.example.com/foo +CF_NAME map-route my-app example.com --hostname myhost --app-protocol http2 # myhost.example.com +CF_NAME map-route my-app example.com --port 5000 # example.com:5000` } func (cmd MapRouteCommand) Execute(args []string) error { @@ -61,12 +65,28 @@ func (cmd MapRouteCommand) Execute(args []string) error { path := cmd.Path.Path route, warnings, err := cmd.Actor.GetRouteByAttributes(domain, cmd.Hostname, path, cmd.Port) + url := desiredURL(domain.Name, cmd.Hostname, path, cmd.Port) cmd.UI.DisplayWarnings(warnings) if err != nil { if _, ok := err.(actionerror.RouteNotFoundError); !ok { return err } + if cmd.Options != nil && len(cmd.Options) > 0 { + err := cmd.validateAPIVersionForPerRouteOptions() + if err != nil { + return err + } + } + routeOptions, wrongOptSpec := resources.CreateRouteOptions(cmd.Options) + if wrongOptSpec != nil { + return actionerror.RouteOptionError{ + Name: *wrongOptSpec, + DomainName: domain.Name, + Path: path, + Host: cmd.Hostname, + } + } cmd.UI.DisplayTextWithFlavor("Creating route {{.URL}} for org {{.OrgName}} / space {{.SpaceName}} as {{.User}}...", map[string]interface{}{ "URL": url, @@ -74,18 +94,24 @@ func (cmd MapRouteCommand) Execute(args []string) error { "SpaceName": cmd.Config.TargetedSpace().Name, "OrgName": cmd.Config.TargetedOrganization().Name, }) + route, warnings, err = cmd.Actor.CreateRoute( cmd.Config.TargetedSpace().GUID, domain.Name, cmd.Hostname, path, cmd.Port, + routeOptions, ) cmd.UI.DisplayWarnings(warnings) if err != nil { return err } cmd.UI.DisplayOK() + } else { + if cmd.Options != nil && len(cmd.Options) > 0 { + return actionerror.RouteOptionSupportError{ErrorText: "Route specific options can only be specified for nonexistent routes."} + } } if cmd.AppProtocol != "" { @@ -121,6 +147,7 @@ func (cmd MapRouteCommand) Execute(args []string) error { cmd.UI.DisplayOK() return nil } + warnings, err = cmd.Actor.MapRoute(route.GUID, app.GUID, cmd.AppProtocol) cmd.UI.DisplayWarnings(warnings) if err != nil { @@ -130,3 +157,15 @@ func (cmd MapRouteCommand) Execute(args []string) error { return nil } + +func (cmd MapRouteCommand) validateAPIVersionForPerRouteOptions() error { + err := command.MinimumCCAPIVersionCheck(cmd.Config.APIVersion(), ccversion.MinVersionPerRouteOpts) + if err != nil { + cmd.UI.DisplayWarning("Your CC API version ({{.APIVersion}}) does not support per-route options."+ + "Upgrade to a newer version of the API (minimum version {{.MinSupportedVersion}}). ", map[string]interface{}{ + "APIVersion": cmd.Config.APIVersion(), + "MinSupportedVersion": ccversion.MinVersionPerRouteOpts, + }) + } + return err +} diff --git a/command/v7/map_route_command_test.go b/command/v7/map_route_command_test.go index 13e78d31459..e1ef2c09b7b 100644 --- a/command/v7/map_route_command_test.go +++ b/command/v7/map_route_command_test.go @@ -2,6 +2,9 @@ package v7_test import ( "errors" + "strconv" + + "code.cloudfoundry.org/cli/api/cloudcontroller/ccversion" "code.cloudfoundry.org/cli/actor/actionerror" "code.cloudfoundry.org/cli/actor/v7action" @@ -34,6 +37,9 @@ var _ = Describe("map-route Command", func() { path string orgGUID string spaceGUID string + options []string + expectedOptions map[string]*string + cCAPIOldVersion string ) BeforeEach(func() { @@ -42,6 +48,9 @@ var _ = Describe("map-route Command", func() { fakeConfig = new(commandfakes.FakeConfig) fakeSharedActor = new(commandfakes.FakeSharedActor) fakeActor = new(v7fakes.FakeActor) + fakeConfig.APIVersionReturns(ccversion.MinVersionPerRouteOpts) + + expectedOptions = map[string]*string{} binaryName = "faceman" fakeConfig.BinaryNameReturns(binaryName) @@ -51,12 +60,14 @@ var _ = Describe("map-route Command", func() { path = `path` orgGUID = "some-org-guid" spaceGUID = "some-space-guid" + options = []string{} cmd = MapRouteCommand{ RequiredArgs: flag.AppDomain{App: appName, Domain: domain}, Hostname: hostname, Path: flag.V7RoutePath{Path: path}, AppProtocol: "http2", + Options: options, BaseCommand: BaseCommand{ UI: testUI, Config: fakeConfig, @@ -220,11 +231,15 @@ var _ = Describe("map-route Command", func() { When("the requested route does not exist", func() { BeforeEach(func() { + lbLCVal := "least-connections" + lbLeastConnections := &lbLCVal fakeActor.GetRouteByAttributesReturns( resources.Route{}, v7action.Warnings{"get-route-warnings"}, actionerror.RouteNotFoundError{}, ) + cmd.Options = []string{"loadbalancing=least-connections"} + expectedOptions = map[string]*string{"loadbalancing": lbLeastConnections} }) It("creates the route", func() { @@ -250,19 +265,80 @@ var _ = Describe("map-route Command", func() { Expect(actualPort).To(Equal(cmd.Port)) Expect(fakeActor.CreateRouteCallCount()).To(Equal(1)) - actualSpaceGUID, actualDomainName, actualHostname, actualPath, actualPort := fakeActor.CreateRouteArgsForCall(0) + actualSpaceGUID, actualDomainName, actualHostname, actualPath, actualPort, actualOptions := fakeActor.CreateRouteArgsForCall(0) Expect(actualSpaceGUID).To(Equal(spaceGUID)) Expect(actualDomainName).To(Equal("some-domain.com")) Expect(actualHostname).To(Equal(hostname)) Expect(actualPath).To(Equal(path)) Expect(actualPort).To(Equal(cmd.Port)) + Expect(actualOptions).To(Equal(expectedOptions)) + }) + }) + + When("the requested route does not exist and options are specified incorrectly", func() { + BeforeEach(func() { + fakeActor.GetRouteByAttributesReturns( + resources.Route{}, + nil, + actionerror.RouteNotFoundError{}, + ) + cmd.Options = []string{"loadbalancing"} + }) + + It("gives an error message", func() { + Expect(testUI.Err).To(Say("get-domain-warnings")) + Expect(testUI.Err).To(Say("get-app-warnings")) + Expect(executeErr).To(MatchError(actionerror.RouteOptionError{Name: "loadbalancing", DomainName: domain, Path: path, Host: hostname})) + Expect(fakeActor.CreateRouteCallCount()).To(Equal(0)) + }) + }) + + When("the requested route does not exist and CC API version is too old for route options", func() { + BeforeEach(func() { + fakeActor.GetRouteByAttributesReturns( + resources.Route{}, + v7action.Warnings{"get-route-warnings"}, + actionerror.RouteNotFoundError{}, + ) + cmd.Options = []string{"loadbalancing=round-robin"} + cCAPIOldVersion = strconv.Itoa(1) + fakeConfig.APIVersionReturns(cCAPIOldVersion) + }) + + It("gives an error message", func() { + Expect(testUI.Err).To(Say("get-domain-warnings")) + Expect(testUI.Err).To(Say("get-app-warnings")) + Expect(testUI.Err).To(Say("CC API version")) + Expect(testUI.Err).To(Say("does not support per-route options")) + Expect(executeErr).To(HaveOccurred()) + Expect(fakeActor.CreateRouteCallCount()).To(Equal(0)) + }) + }) + + When("the requested route does not exist and CC API version is too old for route options", func() { + BeforeEach(func() { + fakeActor.GetRouteByAttributesReturns( + resources.Route{}, + v7action.Warnings{"get-route-warnings"}, + actionerror.RouteNotFoundError{}, + ) + cmd.Options = nil + cCAPIOldVersion = strconv.Itoa(1) + fakeConfig.APIVersionReturns(cCAPIOldVersion) + }) + + It("succeeds because the options were not specified", func() { + Expect(testUI.Err).To(Say("get-domain-warnings")) + Expect(testUI.Err).To(Say("get-app-warnings")) + Expect(executeErr).ToNot(HaveOccurred()) + Expect(fakeActor.CreateRouteCallCount()).To(Equal(1)) }) }) When("the requested route exists", func() { BeforeEach(func() { fakeActor.GetRouteByAttributesReturns( - resources.Route{GUID: "route-guid"}, + resources.Route{GUID: "route-guid", Options: map[string]*string{}}, v7action.Warnings{"get-route-warnings"}, nil, ) @@ -421,6 +497,27 @@ var _ = Describe("map-route Command", func() { }) }) + When("the requested route exists and the options are specified", func() { + BeforeEach(func() { + fakeActor.GetRouteByAttributesReturns( + resources.Route{GUID: "route-guid", Options: map[string]*string{}}, + v7action.Warnings{"get-route-warnings"}, + nil, + ) + cmd.Options = []string{"loadbalancing=least-connections"} + }) + + When("getting the per-route options error", func() { + BeforeEach(func() { + fakeActor.GetRouteDestinationByAppGUIDReturns(resources.RouteDestination{}, nil) + }) + It("returns the error message", func() { + Expect(executeErr).To(MatchError(actionerror.RouteOptionSupportError{ErrorText: "Route specific options can only be specified for nonexistent routes."})) + Expect(fakeActor.MapRouteCallCount()).To(Equal(0)) + }) + }) + }) + When("a tcp route is requested without a port", func() { BeforeEach(func() { fakeActor.GetRouteByAttributesReturns( diff --git a/command/v7/route_command.go b/command/v7/route_command.go index 1ed64478ee6..4ddb6eca5cb 100644 --- a/command/v7/route_command.go +++ b/command/v7/route_command.go @@ -97,6 +97,7 @@ func (cmd RouteCommand) Execute(args []string) error { {cmd.UI.TranslateText("port:"), port}, {cmd.UI.TranslateText("path:"), route.Path}, {cmd.UI.TranslateText("protocol:"), route.Protocol}, + {cmd.UI.TranslateText("options:"), route.FormattedOptions()}, } cmd.UI.DisplayKeyValueTable("", table, 3) diff --git a/command/v7/route_command_test.go b/command/v7/route_command_test.go index 7a101409149..ec5acbc5d4b 100644 --- a/command/v7/route_command_test.go +++ b/command/v7/route_command_test.go @@ -26,6 +26,7 @@ var _ = Describe("route Command", func() { binaryName string executeErr error domainName string + options map[string]*string ) BeforeEach(func() { @@ -192,7 +193,12 @@ var _ = Describe("route Command", func() { 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} + + lbLCVal := "least-connections" + lbLeastConnections := &lbLCVal + options = map[string]*string{"loadbalancing": lbLeastConnections} + + route := resources.Route{GUID: "route-guid", Host: cmd.Hostname, Path: cmd.Path.Path, Protocol: "http", Destinations: destinations, Options: options} fakeActor.GetRouteByAttributesReturns( route, @@ -218,6 +224,7 @@ var _ = Describe("route Command", func() { 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(`options:\s+{loadbalancing=%s}`, *options["loadbalancing"])) Expect(testUI.Out).To(Say(`\n`)) Expect(testUI.Out).To(Say(`Destinations:`)) Expect(testUI.Out).To(Say(`\s+app\s+process\s+port\s+app-protocol`)) diff --git a/command/v7/routes_command.go b/command/v7/routes_command.go index 437704cdc8f..a62beb14de7 100644 --- a/command/v7/routes_command.go +++ b/command/v7/routes_command.go @@ -85,6 +85,7 @@ func (cmd RoutesCommand) displayRoutesTable(routeSummaries []v7action.RouteSumma cmd.UI.TranslateText("app-protocol"), cmd.UI.TranslateText("apps"), cmd.UI.TranslateText("service instance"), + cmd.UI.TranslateText("options"), }, } @@ -103,6 +104,7 @@ func (cmd RoutesCommand) displayRoutesTable(routeSummaries []v7action.RouteSumma strings.Join(routeSummary.AppProtocols, ", "), strings.Join(routeSummary.AppNames, ", "), routeSummary.ServiceInstanceName, + routeSummary.Route.FormattedOptions(), }) } diff --git a/command/v7/shared/app_summary_displayer.go b/command/v7/shared/app_summary_displayer.go index f7d386a66b1..fc1768c57e7 100644 --- a/command/v7/shared/app_summary_displayer.go +++ b/command/v7/shared/app_summary_displayer.go @@ -59,7 +59,7 @@ func (display AppSummaryDisplayer) AppDisplay(summary v7action.DetailedApplicati func routeSummary(rs []resources.Route) string { formattedRoutes := []string{} for _, route := range rs { - formattedRoutes = append(formattedRoutes, route.URL) + formattedRoutes = append(formattedRoutes, route.URL+route.FormattedOptions()) } return strings.Join(formattedRoutes, ", ") } diff --git a/command/v7/shared/app_summary_displayer_test.go b/command/v7/shared/app_summary_displayer_test.go index 833dc595cd3..f803bf732f9 100644 --- a/command/v7/shared/app_summary_displayer_test.go +++ b/command/v7/shared/app_summary_displayer_test.go @@ -605,6 +605,22 @@ var _ = Describe("app summary displayer", func() { }) }) + When("the application has routes with options", func() { + BeforeEach(func() { + lbLCVal := "least-connections" + options := map[string]*string{"loadbalancing": &lbLCVal} + + summary.Routes = []resources.Route{ + {Host: "route1", URL: "route1.example.com", Options: options}, + {Host: "route2", URL: "route2.example.com"}, + } + }) + + It("displays routes", func() { + Expect(testUI.Out).To(Say(`routes:\s+%s, %s`, "route1.example.com {loadbalancing=least-connections}", "route2.example.com")) + }) + }) + When("the application has a stack", func() { BeforeEach(func() { summary.CurrentDroplet.Stack = "some-stack" @@ -694,7 +710,7 @@ var _ = Describe("app summary displayer", func() { When("there is an active deployment", func() { var LastStatusChangeTimeString = "2024-07-29T17:32:29Z" - var dateTimeRegexPattern = `[a-zA-Z]{3}\s\d{2}\s[a-zA-Z]{3}\s\d{2}\:\d{2}\:\d{2}\s[A-Z]{3}\s\d{4}` + var dateTimeRegexPattern = `[a-zA-Z]{3}\s\d{2}\s[a-zA-Z]{3}\s\d{2}\:\d{2}\:\d{2}\s[A-Z]{3,4}\s\d{4}` var maxInFlightDefaultValue = 1 When("the deployment strategy is rolling", func() { diff --git a/command/v7/update_route_command.go b/command/v7/update_route_command.go new file mode 100644 index 00000000000..2aaae9eff20 --- /dev/null +++ b/command/v7/update_route_command.go @@ -0,0 +1,134 @@ +package v7 + +import ( + "fmt" + + "code.cloudfoundry.org/cli/actor/actionerror" + "code.cloudfoundry.org/cli/api/cloudcontroller/ccversion" + "code.cloudfoundry.org/cli/command" + "code.cloudfoundry.org/cli/command/flag" + "code.cloudfoundry.org/cli/resources" +) + +type UpdateRouteCommand struct { + BaseCommand + + RequiredArgs 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"` + Options []string `long:"option" short:"o" description:"Set the value of a per-route option"` + RemoveOptions []string `long:"remove-option" short:"r" description:"Remove an option with the given name"` + relatedCommands interface{} `related_commands:"check-route, domains, map-route, routes, unmap-route"` +} + +func (cmd UpdateRouteCommand) Usage() string { + return ` +Update an existing HTTP route: + CF_NAME update-route DOMAIN [--hostname HOSTNAME] [--path PATH] [--option OPTION=VALUE] [--remove-option OPTION]` +} + +func (cmd UpdateRouteCommand) Examples() string { + return ` +CF_NAME update-route example.com -o loadbalancing=round-robin, +CF_NAME update-route example.com -o loadbalancing=least-connections, +CF_NAME update-route example.com -r loadbalancing, +CF_NAME update-route example.com --hostname myhost --path foo -o loadbalancing=round-robin` +} +func (cmd UpdateRouteCommand) 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.RequiredArgs.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) + url := desiredURL(domain.Name, cmd.Hostname, path, 0) + cmd.UI.DisplayWarnings(warnings) + + if err != nil { + if _, ok := err.(actionerror.RouteNotFoundError); !ok { + return err + } + } + err = cmd.validateAPIVersionForPerRouteOptions() + if err != nil { + return err + } + + if cmd.Options == nil && cmd.RemoveOptions == nil { + return actionerror.RouteOptionSupportError{ + ErrorText: fmt.Sprintf("No options were specified for the update of the Route %s", route.URL)} + } + + if cmd.Options != nil { + routeOpts, wrongOptSpec := resources.CreateRouteOptions(cmd.Options) + if wrongOptSpec != nil { + return actionerror.RouteOptionError{ + Name: *wrongOptSpec, + DomainName: domain.Name, + Path: path, + Host: cmd.Hostname, + } + } + + cmd.UI.DisplayTextWithFlavor("Updating route {{.URL}} for org {{.OrgName}} / space {{.SpaceName}} as {{.User}}...", + map[string]interface{}{ + "URL": url, + "User": user.Name, + "SpaceName": cmd.Config.TargetedSpace().Name, + "OrgName": cmd.Config.TargetedOrganization().Name, + }) + route, warnings, err = cmd.Actor.UpdateRoute( + route.GUID, + routeOpts, + ) + cmd.UI.DisplayWarnings(warnings) + if err != nil { + return err + } + } + + if cmd.RemoveOptions != nil { + inputRouteOptions := resources.RemoveRouteOptions(cmd.RemoveOptions) + route, warnings, err = cmd.Actor.UpdateRoute( + route.GUID, + inputRouteOptions, + ) + cmd.UI.DisplayWarnings(warnings) + if err != nil { + return err + } + } + + cmd.UI.DisplayText("Route {{.URL}} has been updated", + map[string]interface{}{ + "URL": route.URL, + }) + cmd.UI.DisplayOK() + + return nil +} + +func (cmd UpdateRouteCommand) validateAPIVersionForPerRouteOptions() error { + err := command.MinimumCCAPIVersionCheck(cmd.Config.APIVersion(), ccversion.MinVersionPerRouteOpts) + if err != nil { + cmd.UI.DisplayWarning("Your CC API version ({{.APIVersion}}) does not support per-route options."+ + "Upgrade to a newer version of the API (minimum version {{.MinSupportedVersion}}). ", map[string]interface{}{ + "APIVersion": cmd.Config.APIVersion(), + "MinSupportedVersion": ccversion.MinVersionPerRouteOpts, + }) + } + return err +} diff --git a/command/v7/update_route_command_test.go b/command/v7/update_route_command_test.go new file mode 100644 index 00000000000..12b73f52337 --- /dev/null +++ b/command/v7/update_route_command_test.go @@ -0,0 +1,286 @@ +package v7_test + +import ( + "errors" + "fmt" + "strconv" + + "code.cloudfoundry.org/cli/api/cloudcontroller/ccversion" + + "code.cloudfoundry.org/cli/actor/actionerror" + "code.cloudfoundry.org/cli/actor/v7action" + "code.cloudfoundry.org/cli/command/commandfakes" + "code.cloudfoundry.org/cli/command/flag" + . "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/v2" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gbytes" +) + +var _ = Describe("update-route Command", func() { + var ( + cmd UpdateRouteCommand + testUI *ui.UI + fakeConfig *commandfakes.FakeConfig + fakeSharedActor *commandfakes.FakeSharedActor + fakeActor *v7fakes.FakeActor + input *Buffer + binaryName string + executeErr error + domain string + hostname string + path string + orgGUID string + spaceGUID string + commandOptions []string + removeOptions []string + options map[string]*string + cCAPIOldVersion string + routeGuid string + ) + + BeforeEach(func() { + input = NewBuffer() + testUI = ui.NewTestUI(input, NewBuffer(), NewBuffer()) + fakeConfig = new(commandfakes.FakeConfig) + fakeSharedActor = new(commandfakes.FakeSharedActor) + fakeActor = new(v7fakes.FakeActor) + fakeConfig.APIVersionReturns(ccversion.MinVersionPerRouteOpts) + + binaryName = "faceman" + fakeConfig.BinaryNameReturns(binaryName) + domain = "some-domain.com" + hostname = "host" + path = `path` + orgGUID = "some-org-guid" + spaceGUID = "some-space-guid" + commandOptions = []string{"loadbalancing=least-connections"} + removeOptions = []string{"loadbalancing"} + lbLCVal := "least-connections" + lbLeastConnections := &lbLCVal + options = map[string]*string{"loadbalancing": lbLeastConnections} + routeGuid = "route-guid" + + cmd = UpdateRouteCommand{ + RequiredArgs: flag.Domain{Domain: domain}, + Hostname: hostname, + Path: flag.V7RoutePath{Path: path}, + Options: commandOptions, + RemoveOptions: removeOptions, + BaseCommand: BaseCommand{ + UI: testUI, + Config: fakeConfig, + SharedActor: fakeSharedActor, + Actor: fakeActor, + }, + } + + fakeConfig.TargetedOrganizationReturns(configv3.Organization{ + Name: "some-org", + GUID: orgGUID, + }) + + fakeConfig.TargetedSpaceReturns(configv3.Space{ + Name: "some-space", + GUID: spaceGUID, + }) + + fakeActor.GetCurrentUserReturns(configv3.User{Name: "steve"}, nil) + + fakeActor.GetRouteByAttributesReturns( + resources.Route{GUID: routeGuid, URL: domain}, + v7action.Warnings{"get-route-warnings"}, + nil, + ) + }) + + JustBeforeEach(func() { + executeErr = cmd.Execute(nil) + }) + + 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(domain)) + + Expect(fakeActor.GetApplicationByNameAndSpaceCallCount()).To(Equal(0)) + + Expect(fakeActor.GetRouteByAttributesCallCount()).To(Equal(0)) + + Expect(fakeActor.CreateRouteCallCount()).To(Equal(0)) + + Expect(fakeActor.MapRouteCallCount()).To(Equal(0)) + }) + }) + + When("getting the domain succeeds", func() { + BeforeEach(func() { + fakeActor.GetDomainByNameReturns( + resources.Domain{Name: "some-domain.com", GUID: "domain-guid"}, + v7action.Warnings{"get-domain-warnings"}, + nil, + ) + fakeActor.UpdateRouteReturns( + resources.Route{GUID: routeGuid, URL: domain, Options: options}, + nil, + nil, + ) + }) + When("updating the route fails when the CC API version is too old for route options", func() { + BeforeEach(func() { + cmd.Options = []string{} + cCAPIOldVersion = strconv.Itoa(1) + fakeConfig.APIVersionReturns(cCAPIOldVersion) + }) + + It("does not update a route giving the error message", func() { + Expect(executeErr).To(HaveOccurred()) + Expect(fakeActor.UpdateRouteCallCount()).To(Equal(0)) + Expect(testUI.Err).To(Say("CC API version")) + Expect(testUI.Err).To(Say("does not support per-route options")) + }) + }) + + When("the route options are not specified", func() { + BeforeEach(func() { + cmd.Options = nil + cmd.RemoveOptions = nil + }) + It("does not update a route giving the error message", func() { + Expect(executeErr).To(MatchError(actionerror.RouteOptionSupportError{ + ErrorText: fmt.Sprintf("No options were specified for the update of the Route %s", domain)})) + Expect(fakeActor.UpdateRouteCallCount()).To(Equal(0)) + }) + }) + + When("the route options are specified incorrectly", func() { + BeforeEach(func() { + cmd.Options = []string{"loadbalancing"} + }) + It("does not update a route giving the error message", func() { + Expect(executeErr).To(MatchError(actionerror.RouteOptionError{Name: "loadbalancing", DomainName: domain, Path: path, Host: hostname})) + Expect(fakeActor.UpdateRouteCallCount()).To(Equal(0)) + }) + }) + + When("removing the options of the route succeeds", func() { + BeforeEach(func() { + cmd.RemoveOptions = []string{"loadbalancing"} + fakeActor.GetRouteByAttributesReturns( + resources.Route{GUID: routeGuid, URL: domain, Options: options}, + nil, + nil, + ) + }) + + It("updates a given route", func() { + Expect(executeErr).ToNot(HaveOccurred()) + expectedRouteGuid, expectedOptions := fakeActor.UpdateRouteArgsForCall(0) + Expect(expectedRouteGuid).To(Equal(routeGuid)) + Expect(expectedOptions).To(Equal(options)) + + expectedRouteGuid, expectedOptions = fakeActor.UpdateRouteArgsForCall(1) + Expect(expectedRouteGuid).To(Equal(routeGuid)) + Expect(expectedOptions).To(Equal(map[string]*string{"loadbalancing": nil})) + Expect(fakeActor.UpdateRouteCallCount()).To(Equal(2)) + + Expect(testUI.Out).To(Say("Updating route")) + Expect(testUI.Out).To(Say("has been updated")) + Expect(testUI.Out).To(Say("OK")) + }) + }) + + When("a requested route exists", func() { + BeforeEach(func() { + fakeActor.GetRouteByAttributesReturns( + resources.Route{GUID: "route-guid", URL: domain}, + nil, + nil, + ) + }) + + It("calls update route passing the proper arguments", func() { + By("passing the expected arguments to the actor ", func() { + Expect(fakeActor.GetDomainByNameCallCount()).To(Equal(1)) + Expect(fakeActor.GetDomainByNameArgsForCall(0)).To(Equal(domain)) + + Expect(fakeActor.GetRouteByAttributesCallCount()).To(Equal(1)) + actualDomain, actualHostname, actualPath, actualPort := fakeActor.GetRouteByAttributesArgsForCall(0) + Expect(actualDomain.Name).To(Equal("some-domain.com")) + Expect(actualDomain.GUID).To(Equal("domain-guid")) + Expect(actualHostname).To(Equal("host")) + Expect(actualPath).To(Equal(path)) + Expect(actualPort).To(Equal(0)) + + Expect(fakeActor.UpdateRouteCallCount()).To(Equal(2)) + actualRouteGUID, actualOptions := fakeActor.UpdateRouteArgsForCall(0) + Expect(actualRouteGUID).To(Equal("route-guid")) + Expect(actualOptions).To(Equal(options)) + + //Second update route call to remove the option + actualRouteGUID, actualOptions = fakeActor.UpdateRouteArgsForCall(1) + Expect(actualRouteGUID).To(Equal("route-guid")) + options["loadbalancing"] = nil + Expect(actualOptions).To(Equal(options)) + }) + }) + }) + }) + }) + 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"))) + }) + }) + +}) diff --git a/command/v7/v7fakes/fake_actor.go b/command/v7/v7fakes/fake_actor.go index 62df07b77aa..8783e2a5e37 100644 --- a/command/v7/v7fakes/fake_actor.go +++ b/command/v7/v7fakes/fake_actor.go @@ -371,7 +371,7 @@ type FakeActor struct { result1 v7action.Warnings result2 error } - CreateRouteStub func(string, string, string, string, int) (resources.Route, v7action.Warnings, error) + CreateRouteStub func(string, string, string, string, int, map[string]*string) (resources.Route, v7action.Warnings, error) createRouteMutex sync.RWMutex createRouteArgsForCall []struct { arg1 string @@ -379,6 +379,7 @@ type FakeActor struct { arg3 string arg4 string arg5 int + arg6 map[string]*string } createRouteReturns struct { result1 resources.Route @@ -3372,6 +3373,22 @@ type FakeActor struct { result1 v7action.Warnings result2 error } + UpdateRouteStub func(string, map[string]*string) (resources.Route, v7action.Warnings, error) + updateRouteMutex sync.RWMutex + updateRouteArgsForCall []struct { + arg1 string + arg2 map[string]*string + } + updateRouteReturns struct { + result1 resources.Route + result2 v7action.Warnings + result3 error + } + updateRouteReturnsOnCall map[int]struct { + result1 resources.Route + result2 v7action.Warnings + result3 error + } UpdateRouteLabelsStub func(string, string, map[string]types.NullString) (v7action.Warnings, error) updateRouteLabelsMutex sync.RWMutex updateRouteLabelsArgsForCall []struct { @@ -5208,7 +5225,7 @@ func (fake *FakeActor) CreatePrivateDomainReturnsOnCall(i int, result1 v7action. }{result1, result2} } -func (fake *FakeActor) CreateRoute(arg1 string, arg2 string, arg3 string, arg4 string, arg5 int) (resources.Route, v7action.Warnings, error) { +func (fake *FakeActor) CreateRoute(arg1 string, arg2 string, arg3 string, arg4 string, arg5 int, arg6 map[string]*string) (resources.Route, v7action.Warnings, error) { fake.createRouteMutex.Lock() ret, specificReturn := fake.createRouteReturnsOnCall[len(fake.createRouteArgsForCall)] fake.createRouteArgsForCall = append(fake.createRouteArgsForCall, struct { @@ -5217,13 +5234,14 @@ func (fake *FakeActor) CreateRoute(arg1 string, arg2 string, arg3 string, arg4 s arg3 string arg4 string arg5 int - }{arg1, arg2, arg3, arg4, arg5}) + arg6 map[string]*string + }{arg1, arg2, arg3, arg4, arg5, arg6}) stub := fake.CreateRouteStub fakeReturns := fake.createRouteReturns - fake.recordInvocation("CreateRoute", []interface{}{arg1, arg2, arg3, arg4, arg5}) + fake.recordInvocation("CreateRoute", []interface{}{arg1, arg2, arg3, arg4, arg5, arg6}) fake.createRouteMutex.Unlock() if stub != nil { - return stub(arg1, arg2, arg3, arg4, arg5) + return stub(arg1, arg2, arg3, arg4, arg5, arg6) } if specificReturn { return ret.result1, ret.result2, ret.result3 @@ -5237,17 +5255,17 @@ func (fake *FakeActor) CreateRouteCallCount() int { return len(fake.createRouteArgsForCall) } -func (fake *FakeActor) CreateRouteCalls(stub func(string, string, string, string, int) (resources.Route, v7action.Warnings, error)) { +func (fake *FakeActor) CreateRouteCalls(stub func(string, string, string, string, int, map[string]*string) (resources.Route, v7action.Warnings, error)) { fake.createRouteMutex.Lock() defer fake.createRouteMutex.Unlock() fake.CreateRouteStub = stub } -func (fake *FakeActor) CreateRouteArgsForCall(i int) (string, string, string, string, int) { +func (fake *FakeActor) CreateRouteArgsForCall(i int) (string, string, string, string, int, map[string]*string) { fake.createRouteMutex.RLock() defer fake.createRouteMutex.RUnlock() argsForCall := fake.createRouteArgsForCall[i] - return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4, argsForCall.arg5 + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4, argsForCall.arg5, argsForCall.arg6 } func (fake *FakeActor) CreateRouteReturns(result1 resources.Route, result2 v7action.Warnings, result3 error) { @@ -18361,6 +18379,74 @@ func (fake *FakeActor) UpdateProcessByTypeAndApplicationReturnsOnCall(i int, res }{result1, result2} } +func (fake *FakeActor) UpdateRoute(arg1 string, arg2 map[string]*string) (resources.Route, v7action.Warnings, error) { + fake.updateRouteMutex.Lock() + ret, specificReturn := fake.updateRouteReturnsOnCall[len(fake.updateRouteArgsForCall)] + fake.updateRouteArgsForCall = append(fake.updateRouteArgsForCall, struct { + arg1 string + arg2 map[string]*string + }{arg1, arg2}) + stub := fake.UpdateRouteStub + fakeReturns := fake.updateRouteReturns + fake.recordInvocation("UpdateRoute", []interface{}{arg1, arg2}) + fake.updateRouteMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2, ret.result3 + } + return fakeReturns.result1, fakeReturns.result2, fakeReturns.result3 +} + +func (fake *FakeActor) UpdateRouteCallCount() int { + fake.updateRouteMutex.RLock() + defer fake.updateRouteMutex.RUnlock() + return len(fake.updateRouteArgsForCall) +} + +func (fake *FakeActor) UpdateRouteCalls(stub func(string, map[string]*string) (resources.Route, v7action.Warnings, error)) { + fake.updateRouteMutex.Lock() + defer fake.updateRouteMutex.Unlock() + fake.UpdateRouteStub = stub +} + +func (fake *FakeActor) UpdateRouteArgsForCall(i int) (string, map[string]*string) { + fake.updateRouteMutex.RLock() + defer fake.updateRouteMutex.RUnlock() + argsForCall := fake.updateRouteArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeActor) UpdateRouteReturns(result1 resources.Route, result2 v7action.Warnings, result3 error) { + fake.updateRouteMutex.Lock() + defer fake.updateRouteMutex.Unlock() + fake.UpdateRouteStub = nil + fake.updateRouteReturns = struct { + result1 resources.Route + result2 v7action.Warnings + result3 error + }{result1, result2, result3} +} + +func (fake *FakeActor) UpdateRouteReturnsOnCall(i int, result1 resources.Route, result2 v7action.Warnings, result3 error) { + fake.updateRouteMutex.Lock() + defer fake.updateRouteMutex.Unlock() + fake.UpdateRouteStub = nil + if fake.updateRouteReturnsOnCall == nil { + fake.updateRouteReturnsOnCall = make(map[int]struct { + result1 resources.Route + result2 v7action.Warnings + result3 error + }) + } + fake.updateRouteReturnsOnCall[i] = struct { + result1 resources.Route + result2 v7action.Warnings + result3 error + }{result1, result2, result3} +} + func (fake *FakeActor) UpdateRouteLabels(arg1 string, arg2 string, arg3 map[string]types.NullString) (v7action.Warnings, error) { fake.updateRouteLabelsMutex.Lock() ret, specificReturn := fake.updateRouteLabelsReturnsOnCall[len(fake.updateRouteLabelsArgsForCall)] @@ -20007,6 +20093,8 @@ func (fake *FakeActor) Invocations() map[string][][]interface{} { defer fake.updateOrganizationQuotaMutex.RUnlock() fake.updateProcessByTypeAndApplicationMutex.RLock() defer fake.updateProcessByTypeAndApplicationMutex.RUnlock() + fake.updateRouteMutex.RLock() + defer fake.updateRouteMutex.RUnlock() fake.updateRouteLabelsMutex.RLock() defer fake.updateRouteLabelsMutex.RUnlock() fake.updateSecurityGroupMutex.RLock() diff --git a/integration/helpers/name_generator.go b/integration/helpers/name_generator.go index c6fcfeed525..a612cf6f6d3 100644 --- a/integration/helpers/name_generator.go +++ b/integration/helpers/name_generator.go @@ -67,6 +67,14 @@ func NewOrgName() string { return PrefixedRandomName("INTEGRATION-ORG") } +// NewOptions provides a load balancing algorithm option +func NewOptions() map[string]*string { + lbRR := "round-robin" + return map[string]*string{ + "loadbalancing": &lbRR, + } +} + // NewServiceOfferingName provides a random name prefixed with INTEGRATION-SERVICE func NewServiceOfferingName() string { return PrefixedRandomName("INTEGRATION-SERVICE") diff --git a/integration/helpers/route.go b/integration/helpers/route.go index d4cad8196ae..2bf363b4755 100644 --- a/integration/helpers/route.go +++ b/integration/helpers/route.go @@ -41,29 +41,32 @@ func FindOrCreateTCPRouterGroup(node int) string { // Route represents a route. type Route struct { - Domain string - Host string - Path string - Port int - Space string + Domain string + Host string + Path string + Port int + Space string + Options map[string]*string } // NewRoute constructs a route with given space, domain, hostname, and path. -func NewRoute(space string, domain string, hostname string, path string) Route { +func NewRoute(space string, domain string, hostname string, path string, options map[string]*string) Route { return Route{ - Space: space, - Domain: domain, - Host: hostname, - Path: path, + Space: space, + Domain: domain, + Host: hostname, + Path: path, + Options: options, } } // NewTCPRoute constructs a TCP route with given space, domain, and port. -func NewTCPRoute(space string, domain string, port int) Route { +func NewTCPRoute(space string, domain string, port int, options map[string]*string) Route { return Route{ - Space: space, - Domain: domain, - Port: port, + Space: space, + Domain: domain, + Port: port, + Options: options, } } diff --git a/integration/v7/isolated/create_route_command_test.go b/integration/v7/isolated/create_route_command_test.go index 60e64ba82a6..8d71db79733 100644 --- a/integration/v7/isolated/create_route_command_test.go +++ b/integration/v7/isolated/create_route_command_test.go @@ -27,9 +27,9 @@ var _ = Describe("create-route command", func() { Eventually(session).Should(Say(`USAGE:`)) Eventually(session).Should(Say(`Create an HTTP route:\n`)) - Eventually(session).Should(Say(`cf create-route DOMAIN \[--hostname HOSTNAME\] \[--path PATH\]\n`)) + Eventually(session).Should(Say(`cf create-route DOMAIN \[--hostname HOSTNAME\] \[--path PATH\] \[--option OPTION=VALUE\]\n`)) Eventually(session).Should(Say(`Create a TCP route:\n`)) - Eventually(session).Should(Say(`cf create-route DOMAIN \[--port PORT\]\n`)) + Eventually(session).Should(Say(`cf create-route DOMAIN \[--port PORT\] \[--option OPTION=VALUE\]\n`)) Eventually(session).Should(Say(`\n`)) Eventually(session).Should(Say(`EXAMPLES:`)) @@ -37,16 +37,18 @@ var _ = Describe("create-route command", func() { Eventually(session).Should(Say(`cf create-route example.com --hostname myapp\s+# myapp.example.com`)) Eventually(session).Should(Say(`cf create-route example.com --hostname myapp --path foo\s+# myapp.example.com/foo`)) Eventually(session).Should(Say(`cf create-route example.com --port 5000\s+# example.com:5000`)) + Eventually(session).Should(Say(`cf create-route example.com --hostname myapp -o loadbalancing=least-connections\s+# myapp.example.com with a per-route option`)) 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(`--port\s+Port for the TCP route \(default: random port\)`)) + Eventually(session).Should(Say(`--option, -o\s+Set the value of a per-route option`)) Eventually(session).Should(Say(`\n`)) Eventually(session).Should(Say(`SEE ALSO:`)) - Eventually(session).Should(Say(`check-route, domains, map-route, routes, unmap-route`)) + Eventually(session).Should(Say(`check-route, domains, map-route, routes, unmap-route, update-route`)) Eventually(session).Should(Exit(0)) }) diff --git a/integration/v7/isolated/map_route_command_test.go b/integration/v7/isolated/map_route_command_test.go index a22b82c0c35..35f57d1c55c 100644 --- a/integration/v7/isolated/map_route_command_test.go +++ b/integration/v7/isolated/map_route_command_test.go @@ -29,17 +29,18 @@ var _ = Describe("map-route command", func() { Eventually(session).Should(Say(`USAGE:`)) Eventually(session).Should(Say(`Map an HTTP route:\n`)) - Eventually(session).Should(Say(`cf map-route APP_NAME DOMAIN \[--hostname HOSTNAME\] \[--path PATH\] \[--app-protocol PROTOCOL\]\n`)) + Eventually(session).Should(Say(`cf map-route APP_NAME DOMAIN \[--hostname HOSTNAME\] \[--path PATH\] \[--app-protocol PROTOCOL\] \[--option OPTION=VALUE\]\n`)) Eventually(session).Should(Say(`Map a TCP route:\n`)) - Eventually(session).Should(Say(`cf map-route APP_NAME DOMAIN \[--port PORT]\n`)) + Eventually(session).Should(Say(`cf map-route APP_NAME DOMAIN \[--port PORT] \[--option OPTION=VALUE\]\n`)) Eventually(session).Should(Say(`\n`)) Eventually(session).Should(Say(`EXAMPLES:`)) - Eventually(session).Should(Say(`cf map-route my-app example.com # example.com`)) - Eventually(session).Should(Say(`cf map-route my-app example.com --hostname myhost # myhost.example.com`)) - Eventually(session).Should(Say(`cf map-route my-app example.com --hostname myhost --path foo # myhost.example.com/foo`)) - Eventually(session).Should(Say(`cf map-route my-app example.com --hostname myhost --app-protocol http2 # myhost.example.com`)) - Eventually(session).Should(Say(`cf map-route my-app example.com --port 5000 # example.com:5000`)) + Eventually(session).Should(Say(`cf map-route my-app example.com # example.com`)) + Eventually(session).Should(Say(`cf map-route my-app example.com --hostname myhost # myhost.example.com`)) + Eventually(session).Should(Say(`cf map-route my-app example.com --hostname myhost -o loadbalancing=least-connections # myhost.example.com with a per-route option`)) + Eventually(session).Should(Say(`cf map-route my-app example.com --hostname myhost --path foo # myhost.example.com/foo`)) + Eventually(session).Should(Say(`cf map-route my-app example.com --hostname myhost --app-protocol http2 # myhost.example.com`)) + Eventually(session).Should(Say(`cf map-route my-app example.com --port 5000 # example.com:5000`)) Eventually(session).Should(Say(`\n`)) Eventually(session).Should(Say(`OPTIONS:`)) @@ -47,11 +48,12 @@ var _ = Describe("map-route command", func() { Eventually(session).Should(Say(`--path\s+Path for the HTTP route`)) Eventually(session).Should(Say(`--port\s+Port for the TCP route \(default: random port\)`)) Eventually(session).Should(Say(`--app-protocol\s+\[Beta flag, subject to change\] Protocol for the route destination \(default: http1\). Only applied to HTTP routes`)) + Eventually(session).Should(Say(`--option, -o\s+Set the value of a per-route option`)) Eventually(session).Should(Say(`\n`)) Eventually(session).Should(Say(`SEE ALSO:`)) - Eventually(session).Should(Say(`create-route, routes, unmap-route`)) + Eventually(session).Should(Say(`create-route, routes, unmap-route, update-route`)) Eventually(session).Should(Exit(0)) }) @@ -72,12 +74,14 @@ var _ = Describe("map-route command", func() { path string userName string appName string + options map[string]*string ) BeforeEach(func() { appName = helpers.NewAppName() hostName = helpers.NewHostName() path = helpers.NewPath() + options = helpers.NewOptions() orgName = helpers.NewOrgName() spaceName = helpers.NewSpaceName() helpers.SetupCF(orgName, spaceName) @@ -96,7 +100,7 @@ var _ = Describe("map-route command", func() { BeforeEach(func() { domainName = helpers.DefaultSharedDomain() - route = helpers.NewRoute(spaceName, domainName, hostName, path) + route = helpers.NewRoute(spaceName, domainName, hostName, path, options) route.V7Create() }) @@ -160,7 +164,7 @@ var _ = Describe("map-route command", func() { routerGroup.Create() domain.CreateWithRouterGroup(routerGroup.Name) - route = helpers.NewTCPRoute(spaceName, domainName, 1082) + route = helpers.NewTCPRoute(spaceName, domainName, 1082, options) }) AfterEach(func() { diff --git a/integration/v7/isolated/routes_command_test.go b/integration/v7/isolated/routes_command_test.go index 4f5ff25fcd4..c1b9887e67e 100644 --- a/integration/v7/isolated/routes_command_test.go +++ b/integration/v7/isolated/routes_command_test.go @@ -16,7 +16,7 @@ import ( var _ = Describe("routes command", func() { appProtocolValue := "http1" - const tableHeaders = `space\s+host\s+domain\s+port\s+path\s+protocol\s+app-protocol\s+apps\s+service instance` + const tableHeaders = `space\s+host\s+domain\s+port\s+path\s+protocol\s+app-protocol\s+apps\s+service instance\s+options` Context("Help", func() { It("appears in cf help -a", func() { session := helpers.CF("help", "-a") @@ -122,7 +122,7 @@ var _ = Describe("routes command", func() { Eventually(session).Should(Exit(0)) Expect(session).To(Say(`Getting routes for org %s / space %s as %s\.\.\.`, orgName, spaceName, userName)) Expect(session).To(Say(tableHeaders)) - Expect(session).To(Say(`%s\s+route1\s+%s\s+http\s+%s\s+%s\s+%s\n`, spaceName, domainName, appProtocolValue, appName1, serviceInstanceName)) + Expect(session).To(Say(`%s\s+route1\s+%s\s+http\s+%s\s+%s\s+%s\s+\n`, spaceName, domainName, appProtocolValue, appName1, serviceInstanceName)) Expect(session).To(Say(`%s\s+route1a\s+%s\s+http\s+%s\s+%s\s+\n`, spaceName, domainName, appProtocolValue, appName1)) Expect(session).To(Say(`%s\s+route1b\s+%s\s+http\s+%s\s+%s\s+\n`, spaceName, domainName, appProtocolValue, appName1)) Expect(session).ToNot(Say(`%s\s+route2\s+%s\s+http\s+%s\s+%s\s+\n`, spaceName, domainName, appProtocolValue, appName2)) @@ -145,7 +145,7 @@ var _ = Describe("routes command", func() { Eventually(session).Should(Exit(0)) Expect(session).To(Say(`Getting routes for org %s as %s\.\.\.`, orgName, userName)) Expect(session).To(Say(tableHeaders)) - Expect(session).To(Say(`%s\s+route1\s+%s\s+http\s+%s\s+%s\s+%s\n`, spaceName, domainName, appProtocolValue, appName1, serviceInstanceName)) + Expect(session).To(Say(`%s\s+route1\s+%s\s+http\s+%s\s+%s\s+%s\s+\n`, spaceName, domainName, appProtocolValue, appName1, serviceInstanceName)) Expect(session).To(Say(`%s\s+route2\s+%s\s+\/dodo\s+http\s+%s\s+%s\s+\n`, otherSpaceName, domainName, appProtocolValue, appName2)) }) }) diff --git a/integration/v7/isolated/unmap_route_command_test.go b/integration/v7/isolated/unmap_route_command_test.go index 7143031a98b..f662253ff0b 100644 --- a/integration/v7/isolated/unmap_route_command_test.go +++ b/integration/v7/isolated/unmap_route_command_test.go @@ -71,6 +71,7 @@ var _ = Describe("unmap-route command", func() { tcpRoute helpers.Route port int tcpDomain helpers.Domain + options map[string]*string ) BeforeEach(func() { @@ -82,12 +83,13 @@ var _ = Describe("unmap-route command", func() { helpers.SetupCF(orgName, spaceName) userName, _ = helpers.GetCredentials() domainName = helpers.DefaultSharedDomain() + options = helpers.NewOptions() routerGroupName := helpers.FindOrCreateTCPRouterGroup(4) tcpDomain = helpers.NewDomain(orgName, helpers.NewDomainName("TCP-DOMAIN")) tcpDomain.CreateWithRouterGroup(routerGroupName) - route = helpers.NewRoute(spaceName, domainName, hostName, path) + route = helpers.NewRoute(spaceName, domainName, hostName, path, options) route.V7Create() helpers.WithHelloWorldApp(func(dir string) { @@ -118,7 +120,7 @@ var _ = Describe("unmap-route command", func() { When("it's a TCP route", func() { BeforeEach(func() { port = helpers.GetPort() - tcpRoute = helpers.NewTCPRoute(spaceName, tcpDomain.Name, port) + tcpRoute = helpers.NewTCPRoute(spaceName, tcpDomain.Name, port, options) session := helpers.CF("map-route", appName, tcpDomain.Name, "--port", fmt.Sprintf("%d", tcpRoute.Port)) Eventually(session).Should(Exit(0)) }) diff --git a/integration/v7/isolated/update_route_command_test.go b/integration/v7/isolated/update_route_command_test.go new file mode 100644 index 00000000000..7f501f9b8c8 --- /dev/null +++ b/integration/v7/isolated/update_route_command_test.go @@ -0,0 +1,181 @@ +package isolated + +import ( + . "code.cloudfoundry.org/cli/cf/util/testhelpers/matchers" + "code.cloudfoundry.org/cli/integration/helpers" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gbytes" + . "github.com/onsi/gomega/gexec" +) + +var _ = Describe("update-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("update-route", "ROUTES", "Update a route by route specific options, e.g. load balancing algorithm")) + }) + + It("displays the help information", func() { + session := helpers.CF("update-route", "--help") + Eventually(session).Should(Say(`NAME:`)) + Eventually(session).Should(Say(`update-route - Update a route by route specific options, e.g. load balancing algorithm\n`)) + Eventually(session).Should(Say(`\n`)) + + Eventually(session).Should(Say(`USAGE:`)) + Eventually(session).Should(Say(`Update an existing HTTP route:\n`)) + Eventually(session).Should(Say(`cf update-route DOMAIN \[--hostname HOSTNAME\] \[--path PATH\] \[--option OPTION=VALUE\] \[--remove-option OPTION\]\n`)) + Eventually(session).Should(Say(`\n`)) + + Eventually(session).Should(Say(`EXAMPLES:`)) + Eventually(session).Should(Say(`cf update-route example.com -o loadbalancing=round-robin`)) + Eventually(session).Should(Say(`cf update-route example.com -o loadbalancing=least-connections`)) + Eventually(session).Should(Say(`cf update-route example.com -r loadbalancing`)) + Eventually(session).Should(Say(`cf update-route example.com --hostname myhost --path foo -o loadbalancing=round-robin`)) + 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(`--option, -o\s+Set the value of a per-route option`)) + Eventually(session).Should(Say(`--remove-option, -r\s+Remove an option with the given name`)) + Eventually(session).Should(Say(`\n`)) + + Eventually(session).Should(Say(`SEE ALSO:`)) + Eventually(session).Should(Say(`check-route, domains, 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, "update-route", "some-domain") + }) + }) + + When("the environment is set up correctly", func() { + var ( + orgName string + spaceName string + ) + + BeforeEach(func() { + orgName = helpers.NewOrgName() + spaceName = helpers.NewSpaceName() + + helpers.SetupCF(orgName, spaceName) + }) + + AfterEach(func() { + helpers.QuickDeleteOrg(orgName) + }) + + When("the space and domain exist", func() { + var ( + userName string + domainName string + ) + + BeforeEach(func() { + domainName = helpers.NewDomainName() + userName, _ = helpers.GetCredentials() + }) + + When("the route already exists", func() { + var ( + domain helpers.Domain + hostname string + option string + path string + ) + + BeforeEach(func() { + domain = helpers.NewDomain(orgName, domainName) + hostname = "key-lime-pie" + path = "/a" + domain.CreatePrivate() + Eventually(helpers.CF("create-route", domainName, "--hostname", hostname, "--path", path)).Should(Exit(0)) + }) + + AfterEach(func() { + domain.Delete() + }) + When("a route option is specified", func() { + It("updates the route and runs to completion without failing", func() { + option = "loadbalancing=round-robin" + session := helpers.CF("update-route", domainName, "--hostname", hostname, "--path", path, "--option", option) + Eventually(session).Should(Say(`Updating route %s\.%s%s for org %s / space %s as %s\.\.\.`, hostname, domainName, path, orgName, spaceName, userName)) + Eventually(session).Should(Say(`Route %s\.%s%s has been updated`, hostname, domainName, path)) + Eventually(session).Should(Say(`OK`)) + Eventually(session).Should(Exit(0)) + }) + }) + + When("route options are not specified", func() { + It("gives an error message and fails", func() { + session := helpers.CF("update-route", domainName, "--hostname", hostname, "--path", path) + Eventually(session.Err).Should(Say(`Route option support: 'No options were specified for the update of the Route %s\.%s\%s`, hostname, domainName, path)) + Eventually(session).Should(Exit(1)) + }) + }) + + When("route options are specified in the wrong format", func() { + It("gives an error message and fails", func() { + session := helpers.CF("update-route", domainName, "--hostname", hostname, "--path", path, "--option", "loadbalancing") + Eventually(session.Err).Should(Say(`Route option '%s' for route with host '%s', domain '%s', and path '%s' was specified incorrectly. Please use key-value pair format key=value.`, "loadbalancing", hostname, domainName, path)) + Eventually(session).Should(Say("FAILED")) + Eventually(session).Should(Exit(1)) + }) + }) + }) + + When("the route does not exist", func() { + var ( + domain helpers.Domain + hostname string + option string + ) + + BeforeEach(func() { + domain = helpers.NewDomain(orgName, domainName) + hostname = "key-lime-pie" + option = "loadbalancing=round-robin" + domain.CreatePrivate() + }) + + AfterEach(func() { + domain.Delete() + }) + + It("gives an error message", func() { + session := helpers.CF("update-route", domainName, "--hostname", hostname, "--option", option) + Eventually(session).Should(Say(`Updating route %s\.%s for org %s / space %s as %s\.\.\.`, hostname, domainName, orgName, spaceName, userName)) + Eventually(session.Err).Should(Say(`API endpoint not found at`)) + Eventually(session).Should(Exit(1)) + }) + }) + + }) + + When("the domain does not exist", func() { + It("gives an error message and exits", func() { + session := helpers.CF("update-route", "some-domain") + Eventually(session.Err).Should(Say(`Domain '%s' not found.`, "some-domain")) + Eventually(session).Should(Say("FAILED")) + Eventually(session).Should(Exit(1)) + }) + }) + + When("the domain is not specified", func() { + It("displays error and exits 1", func() { + session := helpers.CF("update-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/options_resource.go b/resources/options_resource.go new file mode 100644 index 00000000000..1730b9d4712 --- /dev/null +++ b/resources/options_resource.go @@ -0,0 +1,24 @@ +package resources + +import "strings" + +func CreateRouteOptions(options []string) (map[string]*string, *string) { + routeOptions := map[string]*string{} + for _, option := range options { + key, value, found := strings.Cut(option, "=") + if found { + routeOptions[key] = &value + } else { + return routeOptions, &option + } + } + return routeOptions, nil +} + +func RemoveRouteOptions(options []string) map[string]*string { + routeOptions := map[string]*string{} + for _, option := range options { + routeOptions[option] = nil + } + return routeOptions +} diff --git a/resources/route_resource.go b/resources/route_resource.go index 5bf31b49b25..6ec9efcf1fa 100644 --- a/resources/route_resource.go +++ b/resources/route_resource.go @@ -2,6 +2,7 @@ package resources import ( "encoding/json" + "strings" "code.cloudfoundry.org/cli/api/cloudcontroller" ) @@ -31,6 +32,7 @@ type Route struct { URL string Destinations []RouteDestination Metadata *Metadata + Options map[string]*string } func (r Route) MarshalJSON() ([]byte, error) { @@ -49,12 +51,13 @@ func (r Route) MarshalJSON() ([]byte, error) { // Building up the request body in ccRoute type ccRoute struct { - GUID string `json:"guid,omitempty"` - Host string `json:"host,omitempty"` - Path string `json:"path,omitempty"` - Protocol string `json:"protocol,omitempty"` - Port int `json:"port,omitempty"` - Relationships *Relationships `json:"relationships,omitempty"` + GUID string `json:"guid,omitempty"` + Host string `json:"host,omitempty"` + Path string `json:"path,omitempty"` + Protocol string `json:"protocol,omitempty"` + Port int `json:"port,omitempty"` + Relationships *Relationships `json:"relationships,omitempty"` + Options map[string]*string `json:"options,omitempty"` } ccR := ccRoute{ @@ -63,6 +66,7 @@ func (r Route) MarshalJSON() ([]byte, error) { Path: r.Path, Protocol: r.Protocol, Port: r.Port, + Options: r.Options, } if r.SpaceGUID != "" { @@ -85,6 +89,7 @@ func (r *Route) UnmarshalJSON(data []byte) error { URL string `json:"url,omitempty"` Destinations []RouteDestination `json:"destinations,omitempty"` Metadata *Metadata `json:"metadata,omitempty"` + Options map[string]*string `json:"options,omitempty"` Relationships struct { Space struct { @@ -115,6 +120,19 @@ func (r *Route) UnmarshalJSON(data []byte) error { r.URL = alias.URL r.Destinations = alias.Destinations r.Metadata = alias.Metadata + r.Options = alias.Options return nil } + +func (r *Route) FormattedOptions() string { + var routeOpts = []string{} + formattedOptions := "" + if r.Options != nil && len(r.Options) > 0 { + for optKey, optVal := range r.Options { + routeOpts = append(routeOpts, optKey+"="+*optVal) + } + formattedOptions = " {" + strings.Join(routeOpts, ", ") + "}" + } + return formattedOptions +}