Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

POST /orgs/{id}/services/{id}/search doesn't account for nodes running an IBM service #226

Merged
merged 5 commits into from
Sep 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,10 @@ To build an exchange container with code that is targeted for a git branch:
- If maxAgreements>1, for CS, in search don't return node to agbot if agbot from same org already has agreement for same service.
- Consider changing all creates to POST, and update (via put/patch) return codes to 200

## Changes in 1.116.0

- Issue 224: New route `POST /v1/orgs/{orgid}/search/nodes/service` as the previous service search route did not account for IBM services.

## Changes in 1.115.0

- More scale driver streamlining
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.115.0
1.116.0
58 changes: 58 additions & 0 deletions src/main/scala/com/horizon/exchangeapi/NodesRoutes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ case class PostNodeErrorRequest() {
}
case class PostNodeErrorResponse(nodes: scala.Seq[String])

case class PostServiceSearchRequest(orgid: String, serviceURL: String, serviceVersion: String, serviceArch: String) {
def validate() = {}
}
case class PostServiceSearchResponse(nodes: scala.collection.Seq[(String, String)])


/** Input for service-based (citizen scientist) search, POST /orgs/"+orgid+"/search/nodes */
case class PostSearchNodesRequest(desiredServices: List[RegServiceSearch], secondsStale: Int, propertiesToReturn: Option[List[String]], startIndex: Int, numEntries: Int) {
/** Halts the request with an error msg if the user input is invalid. */
Expand Down Expand Up @@ -603,6 +609,58 @@ trait NodesRoutes extends ScalatraBase with FutureSupport with SwaggerSupport wi
})
})

// =========== POST /orgs/{orgid}/search/nodes/service ===============================

val postServiceSearch =
(apiOperation[PostServiceSearchResponse]("postServiceSearch")
summary("Returns the nodes a service is running on")
description
"""Returns a list of all the nodes a service is running on. The **request body** structure:
```
{
"orgid": "string", // orgid of the service to be searched on
"serviceURL": "string",
"serviceVersion": "string",
"serviceArch": "string"
}
```"""
parameters(
Parameter("orgid", DataType.String, Option[String]("Organization id."), paramType=ParamType.Path),
Parameter("id", DataType.String, Option[String]("Username of exchange user, or ID of an agbot. This parameter can also be passed in the HTTP Header."), paramType=ParamType.Query, required=false),
Parameter("token", DataType.String, Option[String]("Password of exchange user, or token of the agbot. This parameter can also be passed in the HTTP Header."), paramType=ParamType.Query, required=false),
)
responseMessages(ResponseMessage(HttpCode.POST_OK,"created/updated"), ResponseMessage(HttpCode.BADCREDS,"invalid credentials"), ResponseMessage(HttpCode.ACCESS_DENIED,"access denied"), ResponseMessage(HttpCode.BAD_INPUT,"bad input"), ResponseMessage(HttpCode.NOT_FOUND,"not found"))
)
val postServiceSearch2 = (apiOperation[PostNodeHealthRequest]("postSearchNodeHealth2") summary("a") description("a"))

/** Called by the agbot to get recent info about nodes with no pattern (and the agreements the node has). */
post("/orgs/:orgid/search/nodes/service", operation(postServiceSearch)) ({
val orgid = params("orgid")
authenticate().authorizeTo(TNode(OrgAndId(orgid,"*").toString),Access.READ)
val searchProps = try { parse(request.body).extract[PostServiceSearchRequest] }
catch { case e: Exception => halt(HttpCode.BAD_INPUT, ApiResponse(ApiResponseType.BAD_INPUT, ExchangeMessage.translateMessage("error.parsing.input.json", e))) } // the specific exception is MappingException
searchProps.validate()
// service = svcUrl_svcVersion_svcArch
val service = searchProps.serviceURL+"_"+searchProps.serviceVersion+"_"+searchProps.serviceArch
logger.debug("POST /orgs/"+orgid+"/search/nodehealth criteria: "+searchProps.toString)
val resp = response
val orgService = "%|"+searchProps.orgid+"/"+service+"|%"
val q = for {
(n, s) <- (NodesTQ.rows.filter(_.orgid === orgid)) join (NodeStatusTQ.rows.filter(_.runningServices like orgService)) on (_.id === _.nodeId)
} yield (n.id, n.lastHeartbeat)

db.run(q.result).map({ list =>
logger.debug("POST /orgs/"+orgid+"/services/"+service+"/search result size: "+list.size)
if (list.nonEmpty) {
resp.setStatus(HttpCode.POST_OK)
PostServiceSearchResponse(list)
}
else {
resp.setStatus(HttpCode.NOT_FOUND)
}
})
})

// ======== POST /org/{orgid}/search/nodehealth ========================
val postSearchNodeHealth =
(apiOperation[PostNodeHealthResponse]("postSearchNodeHealth")
Expand Down
47 changes: 0 additions & 47 deletions src/main/scala/com/horizon/exchangeapi/ServicesRoutes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,6 @@ import scala.util.control.Breaks._
case class GetServicesResponse(services: Map[String,Service], lastIndex: Int)
case class GetServiceAttributeResponse(attribute: String, value: String)

/** Input and Output formats for POST /orgs/{orgid}/services/{service}/search */
case class PostServiceSearchRequest() {
def validate() = {}
}
case class PostServiceSearchResponse(nodes: scala.collection.Seq[(String, String)])


object SharableVals extends Enumeration {
type SharableVals = Value
val EXCLUSIVE = Value("exclusive")
Expand Down Expand Up @@ -1103,46 +1096,6 @@ trait ServiceRoutes extends ScalatraBase with FutureSupport with SwaggerSupport
})
})

// =========== POST /orgs/{orgid}/services/{service}/search ===============================

val postServiceSearch =
(apiOperation[PostServiceSearchResponse]("postServiceSearch")
summary("Returns the nodes a service is running on")
description """Returns a list of all the nodes a service is running on."""
parameters(
Parameter("orgid", DataType.String, Option[String]("Organization id."), paramType=ParamType.Path),
Parameter("id", DataType.String, Option[String]("Username of exchange user, or ID of an agbot. This parameter can also be passed in the HTTP Header."), paramType=ParamType.Query, required=false),
Parameter("token", DataType.String, Option[String]("Password of exchange user, or token of the agbot. This parameter can also be passed in the HTTP Header."), paramType=ParamType.Query, required=false),
)
responseMessages(ResponseMessage(HttpCode.POST_OK,"created/updated"), ResponseMessage(HttpCode.BADCREDS,"invalid credentials"), ResponseMessage(HttpCode.ACCESS_DENIED,"access denied"), ResponseMessage(HttpCode.BAD_INPUT,"bad input"), ResponseMessage(HttpCode.NOT_FOUND,"not found"))
)
val postServiceSearch2 = (apiOperation[PostNodeHealthRequest]("postSearchNodeHealth2") summary("a") description("a"))

/** Called by the agbot to get recent info about nodes with no pattern (and the agreements the node has). */
post("/orgs/:orgid/services/:service/search", operation(postServiceSearch)) ({
val orgid = params("orgid")
// service = svcUrl_svcVersion_svcArch
val service = params("service")
authenticate().authorizeTo(TNode(OrgAndId(orgid,"*").toString),Access.READ)
val resp = response
val orgService = "%|"+orgid+"/"+service+"|%"
logger.info("SADIYAH: orgService: " + orgService)
val q = for {
(n, s) <- (NodesTQ.rows.filter(_.orgid === orgid)) join (NodeStatusTQ.rows.filter(_.runningServices like orgService)) on (_.id === _.nodeId)
} yield (n.id, n.lastHeartbeat)

db.run(q.result).map({ list =>
logger.debug("POST /orgs/"+orgid+"/services/"+service+"/search result size: "+list.size)
if (list.nonEmpty) {
resp.setStatus(HttpCode.POST_OK)
PostServiceSearchResponse(list)
}
else {
resp.setStatus(HttpCode.NOT_FOUND)
}
})
})

// =========== DELETE /orgs/{orgid}/services/{service}/dockauths ===============================
val deleteServiceAllDockAuth =
(apiOperation[ApiResponse]("deleteServiceAllDockAuth")
Expand Down
167 changes: 167 additions & 0 deletions src/test/scala/exchangeapi/NodesSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ class NodesSuite extends FunSuite {
val AGBOT2AUTH = ("Authorization","Basic "+orgagbotId2+":"+agbotToken2)
val agProto = "ExchangeAutomatedTest" // using this to avoid db entries from real users and predefined ones
val ALL_VERSIONS = "[0.0.0,INFINITY)"
val ibmService = "TestIBMService"

implicit val formats = DefaultFormats // Brings in default date formats etc.

Expand Down Expand Up @@ -207,6 +208,13 @@ class NodesSuite extends FunSuite {
assert(response.code === HttpCode.POST_OK)
}

test("POST /orgs/IBM/services - add "+ibmService+" to be used in search later") {
val input = PostPutServiceRequest("test-service", None, public = false, None, ibmService, svcversion2, svcarch2, "multiple", None, None, None, "", "", None)
val response = Http(urlRoot+"/v1/orgs/IBM/services").postData(write(input)).method("post").headers(CONTENT).headers(ACCEPT).headers(ROOTAUTH).asString
info("code: "+response.code+", response.body: "+response.body)
assert(response.code === HttpCode.POST_OK)
}

test("POST /orgs/"+orgid+"/patterns/"+patid+" - so nodes can reference it") {
val input = PostPutPatternRequest(patid, None, None,
List(
Expand Down Expand Up @@ -1893,8 +1901,167 @@ class NodesSuite extends FunSuite {
}
}

// ~~~~~ POST /orgs/{orgid}/search/nodes/service tests ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Step 1: get a service running on more than one node
// add a pattern that references a service
val searchPattern = "SearchPattern"
val compositeSearchPattern = orgid+"/"+searchPattern
val nid1 = "SearchTestsNode1"
val ntoken1 = "SeachTestsNode1Token"

test("POST /orgs/"+orgid+"/patterns/"+searchPattern+" - so nodes can reference it") {
val input = PostPutPatternRequest(searchPattern, None, None,
List(
PServices(SDRSPEC_URL, orgid, svcarch, None, List(PServiceVersions(svcversion, None, None, None, None)), None, None ),
),
None, None
)
val response = Http(URL+"/patterns/"+searchPattern).postData(write(input)).method("post").headers(CONTENT).headers(ACCEPT).headers(USERAUTH).asString
info("code: "+response.code+", response.body: "+response.body)
assert(response.code === HttpCode.POST_OK)
}

test("PUT /orgs/"+orgid+"/nodes/"+nodeId+"/status - update running services to search later") {
val oneService = OneService("agreementid", SDRSPEC_URL, orgid, svcversion, svcarch, List[ContainerStatus]())
val oneService2 = OneService("agreementid2", NETSPEEDSPEC_URL, orgid, svcversion2, svcarch2, List[ContainerStatus]())
val input = PutNodeStatusRequest(Map[String,Boolean]("images.bluehorizon.network" -> true), List[OneService](oneService, oneService2))
val response = Http(URL+"/nodes/"+nodeId+"/status").postData(write(input)).method("put").headers(CONTENT).headers(ACCEPT).headers(NODEAUTH).asString
info("code: "+response.code+", response.body: "+response.body)
assert(response.code === HttpCode.PUT_OK)
}

test("PUT /orgs/"+orgid+"/nodes/"+nodeId2+"/status - update running services to search later") {
val oneService = OneService("agreementid", SDRSPEC_URL, orgid, svcversion, svcarch, List[ContainerStatus]())
val oneService2 = OneService("agreementid2", NETSPEEDSPEC_URL, orgid, svcversion2, svcarch2, List[ContainerStatus]())
val input = PutNodeStatusRequest(Map[String,Boolean]("images.bluehorizon.network" -> true), List[OneService](oneService, oneService2))
val response = Http(URL+"/nodes/"+nodeId2+"/status").postData(write(input)).method("put").headers(CONTENT).headers(ACCEPT).headers(NODE2AUTH).asString
info("code: "+response.code+", response.body: "+response.body)
assert(response.code === HttpCode.PUT_OK)
}

// test that the search route returns a list of more than one node
test("POST /orgs/"+orgid+"/search/nodes/service - should find " + SDRSPEC_URL + " running on 2 nodes") {
val input = PostServiceSearchRequest(orgid, SDRSPEC_URL, svcversion, svcarch)
val response = Http(URL+"/search/nodes/service").postData(write(input)).headers(CONTENT).headers(ACCEPT).headers(AGBOTAUTH).asString
info("code: "+response.code+", response.body: "+response.body)
info("code: "+response.code)
assert(response.code === HttpCode.POST_OK)
assert(response.body.contains(nodeId))
assert(response.body.contains(nodeId2))
}

test("POST /orgs/"+orgid+"/search/nodes/service - should find " + NETSPEEDSPEC_URL + " running on 2 nodes") {
val input = PostServiceSearchRequest(orgid, NETSPEEDSPEC_URL, svcversion2, svcarch2)
val response = Http(URL+"/search/nodes/service").postData(write(input)).headers(CONTENT).headers(ACCEPT).headers(AGBOTAUTH).asString
info("code: "+response.code+", response.body: "+response.body)
info("code: "+response.code)
assert(response.code === HttpCode.POST_OK)
assert(response.body.contains(nodeId))
assert(response.body.contains(nodeId2))
}

// test a service that no node is running has an empty resp
test("POST /orgs/"+orgid+"/search/nodes/service - should find " + PWSSPEC_URL + " running on 0 nodes") {
val input = PostServiceSearchRequest(orgid, PWSSPEC_URL, svcarch, svcversion)
val response = Http(URL+"/search/nodes/service").postData(write(input)).headers(CONTENT).headers(ACCEPT).headers(AGBOTAUTH).asString
info("code: "+response.code+", response.body: "+response.body)
info("code: "+response.code)
assert(response.code === HttpCode.NOT_FOUND)
assert(response.body.isEmpty)
}

test("PUT /orgs/"+orgid+"/nodes/"+nodeId2+"/status - add "+ibmService+" to search on later") {
val oneService = OneService("agreementid", SDRSPEC_URL, orgid, svcversion, svcarch, List[ContainerStatus]())
val oneService2 = OneService("agreementid2", ibmService, "IBM", svcversion2, svcarch2, List[ContainerStatus]())
val input = PutNodeStatusRequest(Map[String,Boolean]("images.bluehorizon.network" -> true), List[OneService](oneService, oneService2))
val response = Http(URL+"/nodes/"+nodeId2+"/status").postData(write(input)).method("put").headers(CONTENT).headers(ACCEPT).headers(NODE2AUTH).asString
info("code: "+response.code+", response.body: "+response.body)
assert(response.code === HttpCode.PUT_OK)
}

// test that search can find an org node running an IBM service
test("POST /orgs/"+orgid+"/search/nodes/service - should find " + ibmService + " running on 1 node") {
val input = PostServiceSearchRequest("IBM", ibmService, svcversion2, svcarch2)
val response = Http(URL+"/search/nodes/service").postData(write(input)).headers(CONTENT).headers(ACCEPT).headers(AGBOTAUTH).asString
info("code: "+response.code+", response.body: "+response.body)
info("code: "+response.code)
assert(response.code === HttpCode.POST_OK)
assert(response.body.contains(nodeId2))
}

test("PUT /orgs/"+orgid+"/nodes/"+nodeId+"/status - add \"+ibmService+\" to search on later") {
val oneService = OneService("agreementid", ibmService, "IBM", svcversion2, svcarch2, List[ContainerStatus]())
val oneService2 = OneService("agreementid2", NETSPEEDSPEC_URL, orgid, svcversion2, svcarch2, List[ContainerStatus]())
val input = PutNodeStatusRequest(Map[String,Boolean]("images.bluehorizon.network" -> true), List[OneService](oneService, oneService2))
val response = Http(URL+"/nodes/"+nodeId+"/status").postData(write(input)).method("put").headers(CONTENT).headers(ACCEPT).headers(NODEAUTH).asString
info("code: "+response.code+", response.body: "+response.body)
assert(response.code === HttpCode.PUT_OK)
}

test("POST /orgs/"+orgid+"/search/nodes/service - should find " + ibmService + " running on 2 nodes") {
val input = PostServiceSearchRequest("IBM", ibmService, svcversion2, svcarch2)
val response = Http(URL+"/search/nodes/service").postData(write(input)).headers(CONTENT).headers(ACCEPT).headers(AGBOTAUTH).asString
info("code: "+response.code+", response.body: "+response.body)
info("code: "+response.code)
assert(response.code === HttpCode.POST_OK)
assert(response.body.contains(nodeId))
assert(response.body.contains(nodeId2))
}

test("POST /orgs/"+orgid+"/search/nodes/service - should find " + SDRSPEC_URL + " running on 1 node") {
val input = PostServiceSearchRequest(orgid, SDRSPEC_URL, svcversion, svcarch)
val response = Http(URL+"/search/nodes/service").postData(write(input)).headers(CONTENT).headers(ACCEPT).headers(AGBOTAUTH).asString
info("code: "+response.code+", response.body: "+response.body)
info("code: "+response.code)
assert(response.code === HttpCode.POST_OK)
assert(response.body.contains(nodeId2))
assert(!response.body.contains(nodeId))
}

test("POST /orgs/"+orgid+"/search/nodes/service - should find " + NETSPEEDSPEC_URL + " running on 1 node") {
val input = PostServiceSearchRequest(orgid, NETSPEEDSPEC_URL, svcversion2, svcarch2)
val response = Http(URL+"/search/nodes/service").postData(write(input)).headers(CONTENT).headers(ACCEPT).headers(AGBOTAUTH).asString
info("code: "+response.code+", response.body: "+response.body)
info("code: "+response.code)
assert(response.code === HttpCode.POST_OK)
assert(response.body.contains(nodeId))
assert(!response.body.contains(nodeId2))
}

test("POST /orgs/"+orgid+"/search/nodes/service - should find " + PWSSPEC_URL + " running on 0 nodes still") {
val input = PostServiceSearchRequest(orgid, PWSSPEC_URL, svcarch, svcversion)
val response = Http(URL+"/search/nodes/service").postData(write(input)).headers(CONTENT).headers(ACCEPT).headers(AGBOTAUTH).asString
info("code: "+response.code+", response.body: "+response.body)
info("code: "+response.code)
assert(response.code === HttpCode.NOT_FOUND)
assert(response.body.isEmpty)
}

test("PUT /orgs/"+orgid+"/nodes/"+nodeId+"/status - change org of "+NETSPEEDSPEC_URL+" to test later") {
val oneService = OneService("agreementid", ibmService, "IBM", svcversion2, svcarch2, List[ContainerStatus]())
val oneService2 = OneService("agreementid2", NETSPEEDSPEC_URL, "FakeOrganization", svcversion2, svcarch2, List[ContainerStatus]())
val input = PutNodeStatusRequest(Map[String,Boolean]("images.bluehorizon.network" -> true), List[OneService](oneService, oneService2))
val response = Http(URL+"/nodes/"+nodeId+"/status").postData(write(input)).method("put").headers(CONTENT).headers(ACCEPT).headers(NODEAUTH).asString
info("code: "+response.code+", response.body: "+response.body)
assert(response.code === HttpCode.PUT_OK)
}

test("POST /orgs/"+orgid+"/search/nodes/service - should find " + NETSPEEDSPEC_URL + " running on 0 nodes") {
val input = PostServiceSearchRequest(orgid, NETSPEEDSPEC_URL, svcversion2, svcarch2)
val response = Http(URL+"/search/nodes/service").postData(write(input)).headers(CONTENT).headers(ACCEPT).headers(AGBOTAUTH).asString
info("code: "+response.code+", response.body: "+response.body)
info("code: "+response.code)
assert(response.code === HttpCode.NOT_FOUND)
assert(response.body.isEmpty)
}

//~~~~~ Break down ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

test("DELETE /orgs/IBM/services/"+ibmService+"_"+svcversion2+"_"+svcarch2) {
val response = Http(urlRoot+"/v1/orgs/IBM/services/"+ibmService+"_"+svcversion2+"_"+svcarch2).method("delete").headers(ACCEPT).headers(ROOTAUTH).asString
info("code: "+response.code+", response.body: "+response.body)
assert(response.code === HttpCode.DELETED)
}
test("Cleanup - DELETE everything and confirm they are gone") {
deleteAllOrgs()
}
Expand Down
Loading