diff --git a/OpenAPISpecification.yaml b/OpenAPISpecification.yaml index 737144f..4d7810c 100644 --- a/OpenAPISpecification.yaml +++ b/OpenAPISpecification.yaml @@ -24,7 +24,7 @@ schemes: - https - http paths: - /register: + /instances/register: post: tags: - Basic Operations @@ -54,7 +54,38 @@ paths: example: 42 '405': description: Invalid input - /deregister: + /instances: + get: + tags: + - Basic Operations + summary: Get all instances of the specified type + description: >- + This command retrieves a list of all instances that are registered at + the registry and that have the specified type. If no type is specified, + all instances are being returned. + operationId: instanceOfType + parameters: + - name: ComponentType + in: query + description: Type of the instances to be retrieved + required: false + type: string + enum: + - Crawler + - WebApi + - WebApp + - DelphiManagement + - ElasticSearch + responses: + '200': + description: List of instances of the specified type + schema: + type: array + items: + $ref: '#/definitions/Instance' + '400': + description: Invalid value + '/instances/{Id}/deregister': post: tags: - Basic Operations @@ -64,10 +95,10 @@ paths: registry. This means that it can no longer be matched to other instances, and it can not be monitored by the management application anymore. - operationId: deleteInstance + operationId: deregisterInstance parameters: - - in: query - name: Id + - name: Id + in: path description: The ID of the instance to be deregistered. required: true type: integer @@ -81,7 +112,7 @@ paths: description: Instance not found '405': description: Validation exception - /matchingInstance: + '/instances/{Id}/matchingInstance': get: tags: - Basic Operations @@ -92,7 +123,7 @@ paths: on the server. operationId: matchingInstance parameters: - - in: query + - in: path name: Id description: Id of the instance that is requesting a dependency required: true @@ -111,22 +142,22 @@ paths: - ElasticSearch responses: '200': - description: The ID of the registered instance + description: The instance that the registry matched with schema: $ref: '#/definitions/Instance' '400': description: Invalid status value - /instance: + '/instances/{Id}': get: tags: - Basic Operations summary: Get the instance with the specified id description: >- - This command retrieves the instance with the specified id from the server. - If that id is not present, 404 will be returned. + This command retrieves the instance with the specified id from the + server. If that id is not present, 404 will be returned. operationId: instance parameters: - - in: query + - in: path name: Id description: Id of the instance required: true @@ -141,50 +172,21 @@ paths: description: The id was not found on the server '500': description: Internal server error - /instances: - get: - tags: - - Basic Operations - summary: Get all instances of the specified type - description: >- - This command retrieves a list of all instances that are registered at - the registry and that have the specified type. - operationId: instanceOfType - parameters: - - name: ComponentType - in: query - description: Type of the instances to be retrieved - required: true - type: string - enum: - - Crawler - - WebApi - - WebApp - - DelphiManagement - - ElasticSearch - responses: - '200': - description: List of instances of the specified type - schema: - type: array - items: - $ref: '#/definitions/Instance' - '400': - description: Invalid value - /numberOfInstances: + /instances/count: get: tags: - Basic Operations summary: Gets the number of instances running for the specified type description: >- This command retrieves the number of registered instances of the - specified type that are currently running. + specified type that are currently running. If no type is specified, the + number of all instances is being returned operationId: numberOfInstances parameters: - name: ComponentType in: query description: Type of the instances to be counted - required: true + required: false type: string enum: - Crawler @@ -203,7 +205,7 @@ paths: description: Invalid ID supplied '404': description: Instances not found - /matchingResult: + '/instances/{Id}/matchingResult': post: tags: - Basic Operations @@ -214,24 +216,34 @@ paths: specified ID, and it was either successful or not (indicated by the parameter 'MatchingSuccessful'). operationId: matchInstance + consumes: + - application/json parameters: - - in: query - name: CallerId - description: The ID of the instance that is calling this endpoint + - in: body + name: MatchingData + description: Data necessary for processing the matching result required: true - type: integer - format: int64 - - in: query - name: MatchedInstanceId - description: The ID of the instance that the sender was matched to. + schema: + type: object + required: + - MatchingSuccessful + - SenderId + properties: + MatchingSuccessful: + description: >- + Boolean value indicating whether the matching was successful + or not + type: boolean + SenderId: + description: Id of the instance that is submitting the result + type: integer + format: int64 + - in: path + name: Id + description: The ID of the instance that the sender was trying to reach. required: true type: integer format: int64 - - name: MatchingSuccessful - in: query - description: Boolean indicating whether the macthing was successful or not - required: true - type: boolean responses: '200': description: successful operation @@ -239,7 +251,7 @@ paths: description: Invalid ID supplied '404': description: No match found - /eventList: + '/instances/{Id}/eventList': get: tags: - Basic Operations @@ -250,7 +262,7 @@ paths: operationId: eventList parameters: - name: Id - in: query + in: path description: Id of the instance required: true type: integer @@ -264,7 +276,7 @@ paths: $ref: '#/definitions/Event' '404': description: Instance not found - /linksFrom: + '/instances/{Id}/linksFrom': get: tags: - Basic Operations @@ -275,7 +287,7 @@ paths: operationId: linksFrom parameters: - name: Id - in: query + in: path description: Id of the instance required: true type: integer @@ -289,7 +301,7 @@ paths: $ref: '#/definitions/InstanceLink' '404': description: Instance not found - /linksTo: + '/instances/{Id}/linksTo': get: tags: - Basic Operations @@ -300,7 +312,7 @@ paths: operationId: linksTo parameters: - name: Id - in: query + in: path description: Id of the instance required: true type: integer @@ -314,24 +326,28 @@ paths: $ref: '#/definitions/InstanceLink' '404': description: Instance not found - /network: + /instances/network: get: tags: - Basic Operations summary: Retrieves the current instance network description: >- - Retrieves the instance network, meaning a list of all instances as well - as a list of all links currently registered at the registry. + Retrieves the instance network, meaning a list of all instances + currently registered at the registry. operationId: network responses: '200': description: The instance network schema: - $ref: '#/definitions/InstanceNetwork' - /addLabel: + type: array + items: + $ref: '#/definitions/Instance' + '/instances/{Id}/label': post: tags: - Basic Operations + consumes: + - application/json summary: Add a label to the instance with the specified id description: >- This command will add the specified label to the instance with the @@ -339,16 +355,18 @@ paths: operationId: addLabel parameters: - name: Id - in: query + in: path description: Id of the instance required: true type: integer format: int64 - - name: Label - in: query - description: The label to add to the instance + - in: body + name: Label + description: Label added to the instance. required: true - type: string + schema: + type: string + example: private responses: '200': description: Label successfully added @@ -356,7 +374,66 @@ paths: description: 'Bad request, your label exceeded the character limit' '404': description: 'Not found, the id you specified could not be found' - /deploy: + /instances/{Id}/logs: + get: + tags: + - Basic Operations + summary: Retrieve the logging output of the specified instance + description: This command retrieves the docker container logging output for the specified instance, if the instance is in fact running inside a docker container. + operationId: retreiveLogs + parameters: + - name: Id + in: path + description: Id of the instance + required: true + type: integer + format: int64 + - name: StdErr + in: query + description: Switch to select the stderr channel + required: false + type: boolean + responses: + '200': + description: Success, log string is being returned + schema: + type: string + example: "I am logging output .." + '400': + description: Selected instance not running inside docker container + '404': + description: Id not found on the server + '500': + description: Internal Server Error + /instances/{Id}/attach: + get: + tags: + - Basic Operations + summary: Stream logging output from instance + description: 'This command streams the docker container logging output for the specified instance. NOTE: This is a websocket endpoint, so only valid websocket requests will be processed. Swagger does not provide sufficient support for websockets, so this documentation might be confusing as it defines a HTTP method, etc. The names of parameters and response-codes are valid though.' + operationId: streamLogs + parameters: + - name: Id + in: path + description: Id of the instance + required: true + type: integer + format: int64 + - name: StdErr + in: query + description: Switch to select the stderr channel + required: false + type: boolean + responses: + '200': + description: Success, logs are being streamed via websocket connection. + '400': + description: Selected instance not running inside docker container + '404': + description: Id not found on the server + '500': + description: Internal Server Error + /instances/deploy: post: tags: - Docker Operations @@ -369,32 +446,37 @@ paths: itself called /reportStart, which will change the state to 'Running' operationId: deploy parameters: - - name: ComponentType - in: query - description: Type of the instances to be counted + - name: DeploymentData + in: body required: true - type: string - enum: - - Crawler - - WebApi - - WebApp - - DelphiManagement - - ElasticSearch - - name: InstanceName - in: query - description: Name for the newly created instance - required: false - type: string + schema: + type: object + required: + - ComponentType + properties: + ComponentType: + description: Type of the instance to be deployed + enum: + - Crawler + - WebApi + - WebApp + - DelphiManagement + - ElasticSearch + example: Crawler + InstanceName: + description: Name for the new instance + type: string + example: MyCrawler responses: '202': description: Operation accepted '500': description: Internal server error - /reportStart: + '/instances/{Id}/reportStart': post: tags: - Docker Operations - summary: Reports the successful start of an instance to the registry + summary: Reports the successful start of an instances to the registry description: >- This command informs the registry about an instance that successfully reached the state 'Running'. This is only applicable to instances @@ -403,9 +485,9 @@ paths: /deploy is called. operationId: reportStart parameters: - - in: query - name: Id - description: The ID of the instance that was started + - name: Id + in: path + description: The ID of the instance. required: true type: integer format: int64 @@ -420,7 +502,7 @@ paths: description: ID not found on server '500': description: Internal server error - /reportStop: + '/instances/{Id}/reportStop': post: tags: - Docker Operations @@ -432,9 +514,9 @@ paths: non-container instances would deregister themselves when stopped. operationId: reportStop parameters: - - in: query - name: Id - description: The ID of the instance that was stopped + - name: Id + in: path + description: The ID of the instance. required: true type: integer format: int64 @@ -449,7 +531,7 @@ paths: description: ID not found on server '500': description: Internal server error - /reportFailure: + '/instances/{Id}/reportFailure': post: tags: - Docker Operations @@ -464,17 +546,19 @@ paths: restarted. operationId: reportFailure parameters: - - in: query - name: Id - description: The ID of the instance that encountered the failure + - name: Id + in: path + description: The ID of the instance. required: true type: integer format: int64 - - in: query + - in: body name: ErrorLog description: An optional string explaining the failure required: false - type: string + schema: + type: string + example: Something went wrong.. responses: '200': description: Report successfully processed. @@ -486,7 +570,7 @@ paths: description: ID not found on server '500': description: Internal server error - /pause: + '/instances/{Id}/pause': post: tags: - Docker Operations @@ -496,9 +580,9 @@ paths: specified ID. Will change the instance state from 'Running' to 'Paused' operationId: pause parameters: - - in: query - name: Id - description: The ID of the instance to be paused + - name: Id + in: path + description: The ID of the instance. required: true type: integer format: int64 @@ -513,7 +597,7 @@ paths: description: ID not found on server '500': description: Internal server error - /resume: + '/instances/{Id}/resume': post: tags: - Docker Operations @@ -524,9 +608,9 @@ paths: 'Paused' to 'Running'. operationId: resume parameters: - - in: query - name: Id - description: The ID of the instance to be resumed + - name: Id + in: path + description: The ID of the instance. required: true type: integer format: int64 @@ -541,22 +625,20 @@ paths: description: ID not found on server '500': description: Internal server error - /stop: + '/instances/{Id}/stop': post: tags: - Docker Operations summary: Stops the specified instances' docker container description: >- - This command stops the specified instance. If the instance is running - inside a docker container, the container will be stopped. If not, the - instance will be gracefully shut down by calling its /stop endpoint. - Will change the instance state to 'Stopped' for docker containers, will - remove the instance for non-docker instances. + This command stops the docker container of the instance with the + specified ID. The instance will be properly shut down by calling its + /stop command first. Will change the instance state to 'Stopped'. operationId: stop parameters: - - in: query - name: Id - description: The ID of the instance to be stopped + - name: Id + in: path + description: The ID of the instance. required: true type: integer format: int64 @@ -564,12 +646,14 @@ paths: '202': description: 'Accepted, the operation will be completed in the future.' '400': - description: 'Bad request, the instance with the specified ID is already stopped.' + description: >- + Bad request, the instance with the specified ID is either already + stopped or not deployed as a docker container at all. '404': description: ID not found on server '500': description: Internal server error - /start: + '/instances/{Id}/start': post: tags: - Docker Operations @@ -580,9 +664,9 @@ paths: 'Running'. operationId: start parameters: - - in: query - name: Id - description: The ID of the instance to be started + - name: Id + in: path + description: The ID of the instance. required: true type: integer format: int64 @@ -597,7 +681,7 @@ paths: description: ID not found on server '500': description: Internal server error - /delete: + '/instances/{Id}/delete': post: tags: - Docker Operations @@ -608,9 +692,9 @@ paths: any data the instance registry holds about the instance. operationId: delete parameters: - - in: query - name: Id - description: The ID of the instance to be deleted + - name: Id + in: path + description: The ID of the instance. required: true type: integer format: int64 @@ -625,7 +709,7 @@ paths: description: ID not found on server '500': description: Internal server error - /assignInstance: + '/instances/{Id}/assignInstance': post: tags: - Docker Operations @@ -637,31 +721,33 @@ paths: applicable to docker instances. operationId: assignInstance parameters: - - in: query + - in: path name: Id description: The ID of the instance whichs dependency should be updated required: true type: integer format: int64 - - in: query + - in: body name: AssignedInstanceId - description: The ID of the instance that should be assigned as dependency + description: The ID of the instance that should be assigned as dependency. required: true - type: integer - format: int64 + schema: + type: integer + format: int64 + example: 42 responses: '202': description: 'Accepted, the operation will be completed in the future.' '400': description: >- - Bad request, the instance with the specified ID is no running inside - a docker container or the assigned instance is of the wrong + Bad request, the instance with the specified ID is not running + inside a docker container or the assigned instance is of the wrong component type. '404': description: One of the ids was not found on the server '500': description: Internal server error - /command: + /instances/{Id}/command: post: tags: - Docker Operations @@ -670,17 +756,29 @@ paths: This command runs a specified command inside a docker container. operationId: command parameters: - - in: query + - in: path name: Id description: The ID of the instance that is a docker container required: true type: integer format: int64 - - in: query - name: Command - description: The Command thet will run inside a container + - in: body + name: CommandData + description: The data needed to run the command required: true - type: string + schema: + type: object + required: + - Command + properties: + Command: + type: string + example: "rm -rf *" + Privileged: + type: boolean + User: + type: string + example: root responses: '200': description: 'OK' @@ -691,21 +789,8 @@ paths: description: Cannot run command, ID not found. '500': description: Internal server error, unknown operation result DESCRIPTION + definitions: - InstanceNetwork: - type: object - required: - - instances - - links - properties: - instances: - type: array - items: - $ref: '#/definitions/Instance' - links: - type: array - items: - $ref: '#/definitions/InstanceLink' InstanceLink: type: object required: @@ -797,3 +882,11 @@ definitions: example: - private - debug + linksTo: + type: array + items: + $ref: '#/definitions/InstanceLink' + linksFrom: + type: array + items: + $ref: '#/definitions/InstanceLink' diff --git a/src/main/scala/de/upb/cs/swt/delphi/instanceregistry/RequestHandler.scala b/src/main/scala/de/upb/cs/swt/delphi/instanceregistry/RequestHandler.scala index a04962d..b2d0a6a 100644 --- a/src/main/scala/de/upb/cs/swt/delphi/instanceregistry/RequestHandler.scala +++ b/src/main/scala/de/upb/cs/swt/delphi/instanceregistry/RequestHandler.scala @@ -4,11 +4,11 @@ import akka.actor._ import akka.http.scaladsl.model.StatusCodes import akka.http.scaladsl.model.ws.Message import akka.pattern.{AskTimeoutException, ask} +import akka.stream.scaladsl.{Keep, Sink, Source} +import akka.stream.{ActorMaterializer, Materializer, OverflowStrategy} import akka.util.Timeout import de.upb.cs.swt.delphi.instanceregistry.Docker.DockerActor._ import de.upb.cs.swt.delphi.instanceregistry.Docker.{ContainerAlreadyStoppedException, DockerActor, DockerConnection} -import akka.stream.scaladsl.{Keep, Sink, Source} -import akka.stream.{ActorMaterializer, Materializer, OverflowStrategy} import de.upb.cs.swt.delphi.instanceregistry.connection.RestClient import de.upb.cs.swt.delphi.instanceregistry.daos.InstanceDAO import de.upb.cs.swt.delphi.instanceregistry.io.swagger.client.model.InstanceEnums.{ComponentType, InstanceState} @@ -16,7 +16,6 @@ import de.upb.cs.swt.delphi.instanceregistry.io.swagger.client.model.LinkEnums.L import de.upb.cs.swt.delphi.instanceregistry.io.swagger.client.model._ import org.reactivestreams.Publisher -import scala.concurrent.duration._ import scala.concurrent.{Await, ExecutionContext, Future} import scala.language.postfixOps import scala.util.{Failure, Success, Try} @@ -24,9 +23,8 @@ import scala.util.{Failure, Success, Try} class RequestHandler(configuration: Configuration, instanceDao: InstanceDAO, connection: DockerConnection) extends AppLogging { - implicit val system: ActorSystem = Registry.system - implicit val materializer : Materializer = ActorMaterializer() + implicit val materializer: Materializer = ActorMaterializer() implicit val ec: ExecutionContext = system.dispatcher val (eventActor, eventPublisher) = Source.actorRef[RegistryEvent](10, OverflowStrategy.dropNew) @@ -53,7 +51,7 @@ class RequestHandler(configuration: Configuration, instanceDao: InstanceDAO, con log.info("Done initializing request handler.") } - def shutdown() : Unit = { + def shutdown(): Unit = { eventActor ! PoisonPill instanceDao.shutdown() } @@ -111,13 +109,21 @@ class RequestHandler(configuration: Configuration, instanceDao: InstanceDAO, con instanceDao.allInstances().count(i => i.componentType == compType) } - def getEventList(id: Long) : Try[List[RegistryEvent]] = { + def getAllInstancesCount(): Int = { + instanceDao.allInstances().length + } + + def getAllInstancesType(): List[Instance] = { + instanceDao.allInstances() + } + + def getEventList(id: Long): Try[List[RegistryEvent]] = { instanceDao.getEventsFor(id) } def getMatchingInstanceOfType(callerId: Long, compType: ComponentType): (OperationResult.Value, Try[Instance]) = { log.info(s"Started matching: Instance with id $callerId is looking for instance of type $compType.") - if(!instanceDao.hasInstance(callerId)){ + if (!instanceDao.hasInstance(callerId)) { log.warning(s"Matching failed: No instance with id $callerId was found.") (OperationResult.IdUnknown, Failure(new RuntimeException(s"Id $callerId not present."))) } else { @@ -147,11 +153,11 @@ class RequestHandler(configuration: Configuration, instanceDao: InstanceDAO, con } def handleInstanceLinkCreated(instanceIdFrom: Long, instanceIdTo: Long): OperationResult.Value = { - if(!instanceDao.hasInstance(instanceIdFrom) || !instanceDao.hasInstance(instanceIdTo)){ + if (!instanceDao.hasInstance(instanceIdFrom) || !instanceDao.hasInstance(instanceIdTo)) { OperationResult.IdUnknown } else { val (instanceFrom, instanceTo) = (instanceDao.getInstance(instanceIdFrom).get, instanceDao.getInstance(instanceIdTo).get) - if(compatibleTypes(instanceFrom.componentType, instanceTo.componentType)){ + if (compatibleTypes(instanceFrom.componentType, instanceTo.componentType)) { val link = InstanceLink(instanceIdFrom, instanceIdTo, LinkState.Assigned) instanceDao.addLink(link) match { @@ -167,27 +173,28 @@ class RequestHandler(configuration: Configuration, instanceDao: InstanceDAO, con } def handleInstanceAssignment(instanceId: Long, newDependencyId: Long): OperationResult.Value = { - if(!instanceDao.hasInstance(instanceId) || !instanceDao.hasInstance(newDependencyId)){ + if (!instanceDao.hasInstance(instanceId) || !instanceDao.hasInstance(newDependencyId)) { OperationResult.IdUnknown - } else if(!isInstanceDockerContainer(instanceId)){ + } else if (!isInstanceDockerContainer(instanceId)) { OperationResult.NoDockerContainer } else { val instance = instanceDao.getInstance(instanceId).get val dependency = instanceDao.getInstance(newDependencyId).get - if(assignmentAllowed(instance.componentType) && compatibleTypes(instance.componentType, dependency.componentType)){ + if (assignmentAllowed(instance.componentType) && compatibleTypes(instance.componentType, dependency.componentType)) { val link = InstanceLink(instanceId, newDependencyId, LinkState.Assigned) - if(instanceDao.addLink(link).isFailure){ + if (instanceDao.addLink(link).isFailure) { //This should not happen, as ids are being verified above! OperationResult.InternalError } else { fireLinkAddedEvent(link) - implicit val timeout : Timeout = configuration.dockerOperationTimeout + implicit val timeout: Timeout = configuration.dockerOperationTimeout - (dockerActor ? restart(instance.dockerId.get)).map{ - _ => log.info(s"Instance $instanceId restarted.") + (dockerActor ? restart(instance.dockerId.get)).map { + _ => + log.info(s"Instance $instanceId restarted.") instanceDao.setStateFor(instance.id.get, InstanceState.Stopped) //Set to stopped, will report start automatically fireStateChangedEvent(instanceDao.getInstance(instanceId).get) }.recover { @@ -219,12 +226,12 @@ class RequestHandler(configuration: Configuration, instanceDao: InstanceDAO, con fireStateChangedEvent(instanceDao.getInstance(matchedInstanceId).get) //Re-retrieve instance bc reference was invalidated by 'setStateFor' } else if (!matchingSuccess && matchedInstance.instanceState == InstanceState.Running) { instanceDao.setStateFor(matchedInstanceId, InstanceState.NotReachable) - fireStateChangedEvent(instanceDao.getInstance(matchedInstanceId).get)//Re-retrieve instance bc reference was invalidated by 'setStateFor' + fireStateChangedEvent(instanceDao.getInstance(matchedInstanceId).get) //Re-retrieve instance bc reference was invalidated by 'setStateFor' } log.info(s"Applied matching result $matchingSuccess to instance with id $matchedInstanceId.") //Update link state - if(!matchingSuccess){ + if (!matchingSuccess) { val link = InstanceLink(callerId, matchedInstanceId, LinkState.Failed) instanceDao.updateLink(link) match { case Success(_) => @@ -298,10 +305,6 @@ class RequestHandler(configuration: Configuration, instanceDao: InstanceDAO, con } - - - - } /** * @@ -417,10 +420,11 @@ class RequestHandler(configuration: Configuration, instanceDao: InstanceDAO, con val instance = instanceDao.getInstance(id).get if (instance.instanceState == InstanceState.Running) { log.info(s"Handling /pause for instance with id $id...") - implicit val timeout : Timeout = configuration.dockerOperationTimeout + implicit val timeout: Timeout = configuration.dockerOperationTimeout - (dockerActor ? pause(instance.dockerId.get)).map{ - _ => log.info(s"Instance $id paused.") + (dockerActor ? pause(instance.dockerId.get)).map { + _ => + log.info(s"Instance $id paused.") instanceDao.setStateFor(instance.id.get, InstanceState.Paused) fireStateChangedEvent(instanceDao.getInstance(id).get) }.recover { @@ -452,10 +456,11 @@ class RequestHandler(configuration: Configuration, instanceDao: InstanceDAO, con val instance = instanceDao.getInstance(id).get if (instance.instanceState == InstanceState.Paused) { log.info(s"Handling /resume for instance with id $id...") - implicit val timeout : Timeout = configuration.dockerOperationTimeout + implicit val timeout: Timeout = configuration.dockerOperationTimeout - (dockerActor ? unpause(instance.dockerId.get)).map{ - _ => log.info(s"Instance $id resumed.") + (dockerActor ? unpause(instance.dockerId.get)).map { + _ => + log.info(s"Instance $id resumed.") instanceDao.setStateFor(instance.id.get, InstanceState.Running) fireStateChangedEvent(instanceDao.getInstance(id).get) }.recover { @@ -484,27 +489,27 @@ class RequestHandler(configuration: Configuration, instanceDao: InstanceDAO, con } else if (!isInstanceDockerContainer(id)) { val instance = instanceDao.getInstance(id).get - if(instance.componentType == ComponentType.ElasticSearch || instance.componentType == ComponentType.DelphiManagement){ + if (instance.componentType == ComponentType.ElasticSearch || instance.componentType == ComponentType.DelphiManagement) { log.warning(s"Cannot stop instance of type ${instance.componentType}.") OperationResult.InvalidTypeForOperation } else { log.info(s"Calling /stop on non-docker instance $instance..") - RestClient.executePost(RestClient.getUri(instance) + "/stop").map{ + RestClient.executePost(RestClient.getUri(instance) + "/stop").map { response => log.info(s"Request to /stop returned $response") - if (response.status == StatusCodes.OK){ + if (response.status == StatusCodes.OK) { log.info(s"Instance with id $id has been shut down successfully.") } else { log.warning(s"Failed to shut down instance with id $id. Status code was: ${response.status}") } - }.recover{ + }.recover { case ex: Exception => log.warning(s"Failed to shut down instance with id $id. Message is: ${ex.getMessage}") } handleDeregister(id) OperationResult.Ok } - } else if(instanceDao.getInstance(id).get.instanceState != InstanceState.Paused){ + } else if (instanceDao.getInstance(id).get.instanceState != InstanceState.Paused) { log.info(s"Handling /stop for instance with id $id...") val instance = instanceDao.getInstance(id).get @@ -512,8 +517,9 @@ class RequestHandler(configuration: Configuration, instanceDao: InstanceDAO, con log.info("Stopping container...") implicit val timeout: Timeout = configuration.dockerOperationTimeout - (dockerActor ? stop(instance.dockerId.get)).map{ - _ => log.info(s"Instance $id stopped.") + (dockerActor ? stop(instance.dockerId.get)).map { + _ => + log.info(s"Instance $id stopped.") instanceDao.setStateFor(instance.id.get, InstanceState.Stopped) fireStateChangedEvent(instanceDao.getInstance(instance.id.get).get) }.recover { @@ -557,7 +563,7 @@ class RequestHandler(configuration: Configuration, instanceDao: InstanceDAO, con log.info("Starting container...") implicit val timeout: Timeout = configuration.dockerOperationTimeout - (dockerActor ? start(instance.dockerId.get)).map{ + (dockerActor ? start(instance.dockerId.get)).map { _ => log.info(s"Instance $id started.") }.recover { case ex: Exception => @@ -591,14 +597,14 @@ class RequestHandler(configuration: Configuration, instanceDao: InstanceDAO, con //It is not safe to delete instances when other running instances depend on it! val usedBy = instance.linksTo.find(link => link.linkState == LinkState.Assigned) val notSafeToDelete = usedBy.isDefined && instanceDao.getInstance(usedBy.get.idFrom).get.instanceState == InstanceState.Running - if(notSafeToDelete){ + if (notSafeToDelete) { OperationResult.BlockingDependency } else if (instance.instanceState != InstanceState.Running) { log.info("Deleting container...") implicit val timeout: Timeout = configuration.dockerOperationTimeout - (dockerActor ? delete(instance.dockerId.get)).map{ + (dockerActor ? delete(instance.dockerId.get)).map { _ => log.info(s"Container for instance $id deleted.") }.recover { case ex: Exception => @@ -622,11 +628,12 @@ class RequestHandler(configuration: Configuration, instanceDao: InstanceDAO, con /** * Retrieves links from the instance with the specified id. + * * @param id Id of the specified instance * @return Success(listOfLinks) if id is present, Failure otherwise */ - def handleGetLinksFrom(id: Long) : Try[List[InstanceLink]] = { - if(!instanceDao.hasInstance(id)){ + def handleGetLinksFrom(id: Long): Try[List[InstanceLink]] = { + if (!instanceDao.hasInstance(id)) { Failure(new RuntimeException(s"Cannot get links from $id, that id is unknown.")) } else { Success(instanceDao.getLinksFrom(id)) @@ -635,11 +642,12 @@ class RequestHandler(configuration: Configuration, instanceDao: InstanceDAO, con /** * Retrieves links to the instance with the specified id. + * * @param id Id of the specified instance * @return Success(listOfLinks) if id is present, Failure otherwise */ - def handleGetLinksTo(id: Long) : Try[List[InstanceLink]] = { - if(!instanceDao.hasInstance(id)){ + def handleGetLinksTo(id: Long): Try[List[InstanceLink]] = { + if (!instanceDao.hasInstance(id)) { Failure(new RuntimeException(s"Cannot get links to $id, that id is unknown.")) } else { Success(instanceDao.getLinksTo(id)) @@ -648,20 +656,22 @@ class RequestHandler(configuration: Configuration, instanceDao: InstanceDAO, con /** * Retrieves the current instance network, containing all instances and instance links. + * * @return InstanceNetwork */ - def handleGetNetwork() : List[Instance] = { + def handleGetNetwork(): List[Instance] = { instanceDao.allInstances() } /** * Add label to instance with specified id - * @param id Instance id + * + * @param id Instance id * @param label Label to add * @return OperationResult */ - def handleAddLabel(id: Long, label: String) : OperationResult.Value = { - if(!instanceDao.hasInstance(id)){ + def handleAddLabel(id: Long, label: String): OperationResult.Value = { + if (!instanceDao.hasInstance(id)) { OperationResult.IdUnknown } else { instanceDao.addLabelFor(id, label) match { @@ -674,18 +684,19 @@ class RequestHandler(configuration: Configuration, instanceDao: InstanceDAO, con /** * * Returns a source streaming the container logs of the instance with the specified id + * * @param id Id of the instance * @return Tuple of OperationResult and Option[Source[...] ] */ - def handleGetLogs(id: Long, stdErrSelected: Boolean) : (OperationResult.Value, Option[String]) = { - if(!instanceDao.hasInstance(id)){ + def handleGetLogs(id: Long, stdErrSelected: Boolean): (OperationResult.Value, Option[String]) = { + if (!instanceDao.hasInstance(id)) { (OperationResult.IdUnknown, None) - } else if(!isInstanceDockerContainer(id)){ + } else if (!isInstanceDockerContainer(id)) { (OperationResult.NoDockerContainer, None) } else { val instance = instanceDao.getInstance(id).get - val f : Future[(OperationResult.Value, Option[String])] = (dockerActor ? logs(instance.dockerId.get, stdErrSelected, stream = false))(configuration.dockerOperationTimeout).map{ + val f: Future[(OperationResult.Value, Option[String])] = (dockerActor ? logs(instance.dockerId.get, stdErrSelected, stream = false)) (configuration.dockerOperationTimeout).map { logVal: Any => val logResult = logVal.asInstanceOf[Try[String]] logResult match { @@ -696,7 +707,7 @@ class RequestHandler(configuration: Configuration, instanceDao: InstanceDAO, con (OperationResult.InternalError, None) } - }.recover{ + }.recover { case ex: Exception => fireDockerOperationErrorEvent(Some(instance), errorMessage = s"Failed to get logs with message: ${ex.getMessage}") (OperationResult.InternalError, None) @@ -705,15 +716,15 @@ class RequestHandler(configuration: Configuration, instanceDao: InstanceDAO, con } } - def handleStreamLogs(id: Long, stdErrSelected: Boolean) : (OperationResult.Value, Option[Publisher[Message]]) = { - if(!instanceDao.hasInstance(id)){ + def handleStreamLogs(id: Long, stdErrSelected: Boolean): (OperationResult.Value, Option[Publisher[Message]]) = { + if (!instanceDao.hasInstance(id)) { (OperationResult.IdUnknown, None) - } else if(!isInstanceDockerContainer(id)){ + } else if (!isInstanceDockerContainer(id)) { (OperationResult.NoDockerContainer, None) } else { val instance = instanceDao.getInstance(id).get - val f : Future[(OperationResult.Value, Option[Publisher[Message]])] = (dockerActor ? logs(instance.dockerId.get, stdErrSelected, stream = true))(configuration.dockerOperationTimeout).map{ + val f: Future[(OperationResult.Value, Option[Publisher[Message]])] = (dockerActor ? logs(instance.dockerId.get, stdErrSelected, stream = true)) (configuration.dockerOperationTimeout).map { publisherVal: Any => val publisherResult = publisherVal.asInstanceOf[Try[Publisher[Message]]] publisherResult match { @@ -724,7 +735,7 @@ class RequestHandler(configuration: Configuration, instanceDao: InstanceDAO, con (OperationResult.InternalError, None) } - }.recover{ + }.recover { case ex: Exception => fireDockerOperationErrorEvent(Some(instance), errorMessage = s"Failed to stream logs with message: ${ex.getMessage}") (OperationResult.InternalError, None) @@ -738,11 +749,12 @@ class RequestHandler(configuration: Configuration, instanceDao: InstanceDAO, con * be selected regardless of its state. If multiple links are present, the assigned link will be returned. If none of * the links is assigned, matching will fail. If the component types stored in the links do not match the required * component type, matching will fail. - * @param callerId Id of the calling instance + * + * @param callerId Id of the calling instance * @param componentType ComponentType to look for * @return Try[Instance], Success if matching was successful, Failure otherwise */ - private def tryLinkMatching(callerId: Long, componentType: ComponentType) : Try[Instance] = { + private def tryLinkMatching(callerId: Long, componentType: ComponentType): Try[Instance] = { log.info(s"Matching first try: Analyzing links for $callerId...") val links = instanceDao.getLinksFrom(callerId).filter(link => link.linkState == LinkState.Assigned) @@ -754,10 +766,10 @@ class RequestHandler(configuration: Configuration, instanceDao: InstanceDAO, con case 1 => val instanceAssigned = instanceDao.getInstance(links.head.idTo) - if(instanceAssigned.isDefined && instanceAssigned.get.componentType == componentType){ + if (instanceAssigned.isDefined && instanceAssigned.get.componentType == componentType) { log.info(s"Finished matching first try: Successfully matched based on 1 link found. Target is ${instanceAssigned.get}.") Success(instanceAssigned.get) - } else if(instanceAssigned.isDefined && instanceAssigned.get.componentType != componentType){ + } else if (instanceAssigned.isDefined && instanceAssigned.get.componentType != componentType) { log.error(s"Matching first try failed: There was one link present, but the target type ${instanceAssigned.get.componentType} did not match expected type $componentType") val link = InstanceLink(links.head.idFrom, links.head.idTo, LinkState.Outdated) instanceDao.updateLink(link) @@ -776,10 +788,10 @@ class RequestHandler(configuration: Configuration, instanceDao: InstanceDAO, con case Some(instanceLink) => val instanceAssigned = instanceDao.getInstance(instanceLink.idTo) - if(instanceAssigned.isDefined && instanceAssigned.get.componentType == componentType){ + if (instanceAssigned.isDefined && instanceAssigned.get.componentType == componentType) { log.info(s"Finished matching first try: Successfully matched based on one assigned link found out of $x total links. Target is ${instanceAssigned.get}.") Success(instanceAssigned.get) - } else if(instanceAssigned.isDefined && instanceAssigned.get.componentType != componentType){ + } else if (instanceAssigned.isDefined && instanceAssigned.get.componentType != componentType) { log.error(s"Matching first try failed: There was one assigned link present, but the target type ${instanceAssigned.get.componentType} did not match expected type $componentType") val link = InstanceLink(links.head.idFrom, links.head.idTo, LinkState.Outdated) instanceDao.updateLink(link) @@ -803,11 +815,12 @@ class RequestHandler(configuration: Configuration, instanceDao: InstanceDAO, con /** * Tries to match caller to instance of the specified type based on which instance has the most labels in common with * the caller. Will fail if no such instance is found. - * @param callerId Id of the calling instance + * + * @param callerId Id of the calling instance * @param componentType ComponentType to match to * @return Success(Instance) if successful, Failure otherwise. */ - private def tryLabelMatching(callerId: Long, componentType: ComponentType) : Try[Instance] = { + private def tryLabelMatching(callerId: Long, componentType: ComponentType): Try[Instance] = { log.info(s"Matching second try: Analyzing labels for $callerId...") val possibleMatches = instanceDao.getInstancesOfType(componentType) @@ -820,11 +833,11 @@ class RequestHandler(configuration: Configuration, instanceDao: InstanceDAO, con val labels = instanceDao.getInstance(callerId).get.labels val intersectionList = possibleMatches - .filter(instance => instance.labels.intersect(labels).nonEmpty) - .sortBy(instance => instance.labels.intersect(labels).size) - .reverse + .filter(instance => instance.labels.intersect(labels).nonEmpty) + .sortBy(instance => instance.labels.intersect(labels).size) + .reverse - if(intersectionList.nonEmpty){ + if (intersectionList.nonEmpty) { val result = intersectionList.head val noOfSharedLabels = result.labels.intersect(labels).size log.info(s"Finished matching second try: Successfully matched to $result based on $noOfSharedLabels shared labels.") @@ -836,7 +849,7 @@ class RequestHandler(configuration: Configuration, instanceDao: InstanceDAO, con } } - private def tryDefaultMatching(componentType: ComponentType) : Try[Instance] = { + private def tryDefaultMatching(componentType: ComponentType): Try[Instance] = { log.info(s"Matching fallback: Searching for instances of type $componentType ...") getNumberOfInstances(componentType) match { case 0 => @@ -883,17 +896,17 @@ class RequestHandler(configuration: Configuration, instanceDao: InstanceDAO, con * Handles a call to /command. container id and command must be present, * Will run the command into the container with provide parameters * - * @param id container id the command will run on - * @param command the command to run - * @param attachStdin attaches to stdin of the command + * @param id container id the command will run on + * @param command the command to run + * @param attachStdin attaches to stdin of the command * @param attachStdout attaches to stdout of the command * @param attachStderr attaches to stderr of the command - * @param detachKeys Override the key sequence for detaching a container. - * Format is a single character [a-Z] or ctrl-<@value> where is one of: a-z, @, [, , or _ - * @param privileged runs the process with extended privileges - * @param tty allocate a pseudo-TTY - * @param user A string value specifying the user, and optionally, group to run the process inside the container, - * Format is one of: "user", "user:group", "uid", or "uid:gid". + * @param detachKeys Override the key sequence for detaching a container. + * Format is a single character [a-Z] or ctrl-<@value> where is one of: a-z, @, [, , or _ + * @param privileged runs the process with extended privileges + * @param tty allocate a pseudo-TTY + * @param user A string value specifying the user, and optionally, group to run the process inside the container, + * Format is one of: "user", "user:group", "uid", or "uid:gid". * @return */ def handleCommand(id: Long, command: String, attachStdin: Option[Boolean], @@ -910,9 +923,9 @@ class RequestHandler(configuration: Configuration, instanceDao: InstanceDAO, con } else { val instance = instanceDao.getInstance(id).get log.info(s"Handling /command for instance with id $id...") - implicit val timeout : Timeout = configuration.dockerOperationTimeout + implicit val timeout: Timeout = configuration.dockerOperationTimeout - (dockerActor ? runCommand(instance.dockerId.get, command, attachStdin, attachStdout, attachStderr, detachKeys, privileged, tty, user)).map{ + (dockerActor ? runCommand(instance.dockerId.get, command, attachStdin, attachStdout, attachStderr, detachKeys, privileged, tty, user)).map { _ => log.info(s"Command '$command' ran successfully in container with id $id.") }.recover { case ex: Exception => @@ -970,7 +983,7 @@ class RequestHandler(configuration: Configuration, instanceDao: InstanceDAO, con private def fireDockerOperationErrorEvent(affectedInstance: Option[Instance], errorMessage: String): Unit = { val event = RegistryEventFactory.createDockerOperationErrorEvent(affectedInstance, errorMessage) eventActor ! event - if(affectedInstance.isDefined){ + if (affectedInstance.isDefined) { instanceDao.addEventFor(affectedInstance.get.id.get, event) } } @@ -1018,11 +1031,11 @@ class RequestHandler(configuration: Configuration, instanceDao: InstanceDAO, con } - private def assignmentAllowed(instanceType: ComponentType) : Boolean = { + private def assignmentAllowed(instanceType: ComponentType): Boolean = { instanceType == ComponentType.Crawler || instanceType == ComponentType.WebApi || instanceType == ComponentType.WebApp } - private def compatibleTypes(instanceType: ComponentType, dependencyType: ComponentType) : Boolean = { + private def compatibleTypes(instanceType: ComponentType, dependencyType: ComponentType): Boolean = { instanceType match { case ComponentType.Crawler => dependencyType == ComponentType.ElasticSearch case ComponentType.WebApi => dependencyType == ComponentType.ElasticSearch diff --git a/src/main/scala/de/upb/cs/swt/delphi/instanceregistry/connection/Server.scala b/src/main/scala/de/upb/cs/swt/delphi/instanceregistry/connection/Server.scala index ac73c12..6a06262 100644 --- a/src/main/scala/de/upb/cs/swt/delphi/instanceregistry/connection/Server.scala +++ b/src/main/scala/de/upb/cs/swt/delphi/instanceregistry/connection/Server.scala @@ -10,27 +10,27 @@ import akka.stream.scaladsl.{Flow, Sink, Source} import de.upb.cs.swt.delphi.instanceregistry.authorization.AccessTokenEnums.UserType import de.upb.cs.swt.delphi.instanceregistry.authorization.{AccessToken, AuthProvider} import de.upb.cs.swt.delphi.instanceregistry.io.swagger.client.model.InstanceEnums.ComponentType -import de.upb.cs.swt.delphi.instanceregistry.io.swagger.client.model.{EventJsonSupport, InstanceJsonSupport, InstanceLinkJsonSupport, Instance} +import de.upb.cs.swt.delphi.instanceregistry.io.swagger.client.model.{EventJsonSupport, Instance, InstanceJsonSupport, InstanceLinkJsonSupport} +import de.upb.cs.swt.delphi.instanceregistry.requestLimiter.{IpLogActor, RequestLimitScheduler} import de.upb.cs.swt.delphi.instanceregistry.{AppLogging, Registry, RequestHandler} import spray.json.JsonParser.ParsingException import spray.json._ import scala.concurrent.ExecutionContext -import scala.util.{Failure, Success} -import de.upb.cs.swt.delphi.instanceregistry.requestLimiter.{IpLogActor, RequestLimitScheduler} +import scala.util.{Failure, Success, Try} /** * Web server configuration for Instance Registry API. */ -class Server (handler: RequestHandler) extends HttpApp +class Server(handler: RequestHandler) extends HttpApp with InstanceJsonSupport with EventJsonSupport with InstanceLinkJsonSupport with AppLogging { - implicit val system : ActorSystem = Registry.system - implicit val materializer : ActorMaterializer = ActorMaterializer() - implicit val ec : ExecutionContext = system.dispatcher + implicit val system: ActorSystem = Registry.system + implicit val materializer: ActorMaterializer = ActorMaterializer() + implicit val ec: ExecutionContext = system.dispatcher private val ipLogActor = system.actorOf(IpLogActor.props) private val requestLimiter = new RequestLimitScheduler(ipLogActor) @@ -40,70 +40,132 @@ class Server (handler: RequestHandler) extends HttpApp apiRoutes } } + //Routes that map http endpoints to methods in this object - def apiRoutes : server.Route = - /****************BASIC OPERATIONS****************/ - path("register") {entity(as[String]) { jsonString => register(jsonString) }} ~ - path("deregister") { deregister() } ~ - path("instances") { fetchInstancesOfType() } ~ - path("instance") { retrieveInstance() } ~ - path("numberOfInstances") { numberOfInstances() } ~ - path("matchingInstance") { matchingInstance()} ~ - path("matchingResult") { matchInstance()} ~ - path("eventList") { eventList()} ~ - path("linksFrom") { linksFrom()} ~ - path("linksTo") { linksTo()} ~ - path("network") { network()} ~ - path("addLabel") { addLabel()} ~ - /****************DOCKER OPERATIONS****************/ - path("deploy") { deployContainer()} ~ - path("reportStart") { reportStart()} ~ - path("reportStop") { reportStop()} ~ - path("reportFailure") { reportFailure()} ~ - path("pause") { pause()} ~ - path("resume") { resume()} ~ - path("stop") { stop()} ~ - path("start") { start()} ~ - path("delete") { deleteContainer()} ~ - path("assignInstance") { assignInstance()} ~ - path("command") { runCommandInContainer()} ~ - path("logs") { retrieveLogs()} ~ - path("attach") { streamLogs()} ~ - /****************EVENT OPERATIONS****************/ - path("events") { streamEvents()} + def apiRoutes: server.Route = + + /** **************BASIC OPERATIONS ****************/ + pathPrefix("instances") { + pathEnd { + fetchInstancesOfType() + } ~ + path("register") { + entity(as[String]) { + jsonString => register(jsonString) + } + } ~ + path("network") { + network() + } ~ + path("deploy") { + entity(as[JsValue]) { json => deployContainer(json.asJsObject)} + } ~ + path("count") { + numberOfInstances() + } ~ + path(LongNumber) { Id => retrieveInstance(Id) } ~ + pathPrefix(LongNumber) { Id => + path("deregister") { + deregister(Id) + } ~ + path("matchingInstance") { + matchingInstance(Id) + } ~ + path("matchingResult") { + entity(as[JsValue]) { + json => matchInstance(Id, json.asJsObject) + } + } ~ + path("eventList") { + eventList(Id) + } ~ + path("linksFrom") { + linksFrom(Id) + } ~ + path("linksTo") { + linksTo(Id) + } ~ + path("reportStart") { + reportStart(Id) + } ~ + path("reportStop") { + reportStop(Id) + } ~ + path("reportFailure") { + reportFailure(Id) + } ~ + path("pause") { + pause(Id) + } ~ + path("resume") { + resume(Id) + } ~ + path("stop") { + stop(Id) + } ~ + path("start") { + start(Id) + } ~ + path("delete") { + deleteContainer(Id) + } ~ + path("assignInstance") { + entity(as[JsValue]) { + json => assignInstance(Id, json.asJsObject) + } + } ~ + path("label") { + entity(as[JsValue]) { json => addLabel(Id, json.asJsObject) } + } ~ + path("logs") { + retrieveLogs(Id) + } ~ + path("attach") { + streamLogs(Id) + } ~ + path("command") { + entity(as[JsValue]) { json => runCommandInContainer(Id, json.asJsObject) } + } + } + } ~ + path("events") { + streamEvents() + } /** * Registers a new instance at the registry. This endpoint is intended for instances that are not running inside * a docker container, as the Id, DockerId and InstanceState are being ignored. + * * @param InstanceString String containing the serialized instance that is registering * @return Server route that either maps to a 200 OK response if successful, or to the respective error codes */ - def register(InstanceString: String) : server.Route = Route.seal { + def register(InstanceString: String): server.Route = Route.seal { authenticateOAuth2[AccessToken]("Secure Site", AuthProvider.authenticateOAuthRequire(_, userType = UserType.Component)) { token => - post - { - log.debug(s"POST /register has been called, parameter is: $InstanceString") + post { + log.debug(s"POST /instances/register has been called, parameter is: $InstanceString") try { - val paramInstance : Instance = InstanceString.parseJson.convertTo[Instance](instanceFormat) + val paramInstance: Instance = InstanceString.parseJson.convertTo[Instance](instanceFormat) handler.handleRegister(paramInstance) match { case Success(id) => - complete{id.toString} + complete { + id.toString + } case Failure(ex) => log.error(ex, "Failed to handle registration of instance.") complete(HttpResponse(StatusCodes.InternalServerError, entity = "An internal server error occurred.")) } } catch { - case dx : DeserializationException => + case dx: DeserializationException => log.error(dx, "Deserialization exception") complete(HttpResponse(StatusCodes.BadRequest, entity = s"Could not deserialize parameter instance with message ${dx.getMessage}.")) - case px : ParsingException => + case px: ParsingException => log.error(px, "Failed to parse JSON while registering") complete(HttpResponse(StatusCodes.BadRequest, entity = s"Failed to parse JSON entity with message ${px.getMessage}")) - case x : Exception => + case x: Exception => log.error(x, "Uncaught exception while deserializing.") complete(HttpResponse(StatusCodes.InternalServerError, entity = "An internal server error occurred.")) } @@ -116,24 +178,31 @@ class Server (handler: RequestHandler) extends HttpApp * Removes an instance. The id of the instance that is calling deregister must be passed as an query argument named * 'Id' (so the call is /deregister?Id=42). This endpoint is intended for instances that are not running inside * a docker container, as the respective instance will be permanently deleted from the registry. + * * @return Server route that either maps to a 200 OK response if successful, or to the respective error codes. */ - def deregister() : server.Route = parameters('Id.as[Long]){ Id => + def deregister(id: Long): server.Route = { authenticateOAuth2[AccessToken]("Secure Site", AuthProvider.authenticateOAuthRequire(_, userType = UserType.Component)) { token => post { - log.debug(s"POST /deregister?Id=$Id has been called") + log.debug(s"POST instance/$id/deregister has been called") - handler.handleDeregister(Id) match { - case handler.OperationResult.IdUnknown => - log.warning(s"Cannot remove instance with id $Id, that id is not known to the server.") - complete{HttpResponse(StatusCodes.NotFound, entity = s"Id $Id not known to the server")} + handler.handleDeregister(id) match { + case handler.OperationResult.IdUnknown => + log.warning(s"Cannot remove instance with id $id, that id is not known to the server.") + complete { + HttpResponse(StatusCodes.NotFound, entity = s"Id $id not known to the server") + } case handler.OperationResult.IsDockerContainer => - log.warning(s"Cannot remove instance with id $Id, this instance is running inside a docker container") - complete{HttpResponse(StatusCodes.BadRequest, entity = s"Cannot remove instance with id $Id, this instance is " + - s"running inside a docker container. Call /delete to remove it from the server and delete the container.")} + log.warning(s"Cannot remove instance with id $id, this instance is running inside a docker container") + complete { + HttpResponse(StatusCodes.BadRequest, entity = s"Cannot remove instance with id $id, this instance is " + + s"running inside a docker container. Call /delete to remove it from the server and delete the container.") + } case handler.OperationResult.Ok => - log.info(s"Successfully removed instance with id $Id") - complete {s"Successfully removed instance with id $Id"} + log.info(s"Successfully removed instance with id $id") + complete { + s"Successfully removed instance with id $id" + } } } } @@ -142,18 +211,30 @@ class Server (handler: RequestHandler) extends HttpApp /** * Returns a list of instances with the specified ComponentType. The ComponentType must be passed as an query argument * named 'ComponentType' (so the call is /instances?ComponentType=Crawler). + * * @return Server route that either maps to a 200 OK response containing the list of instances, or the resp. error codes. */ - def fetchInstancesOfType () : server.Route = parameters('ComponentType.as[String]) { compTypeString => + def fetchInstancesOfType(): server.Route = parameters('ComponentType.as[String].?) { compTypeString => authenticateOAuth2[AccessToken]("Secure Site", AuthProvider.authenticateOAuthRequire(_, userType = UserType.User)) { token => get { log.debug(s"GET /instances?ComponentType=$compTypeString has been called") - val compType : ComponentType = ComponentType.values.find(v => v.toString == compTypeString).orNull + val noValue = "" - if(compType != null) { - complete{handler.getAllInstancesOfType(compType)} - } else { + val compTypeStr = compTypeString.getOrElse(noValue) + + val compType: ComponentType = ComponentType.values.find(v => v.toString == compTypeStr).orNull + + if (compType != null) { + complete { + handler.getAllInstancesOfType(compType) + } + } else if (compTypeStr == noValue) { + complete { + handler.getAllInstancesType().toList + } + } + else { log.error(s"Failed to deserialize parameter string $compTypeString to ComponentType.") complete(HttpResponse(StatusCodes.BadRequest, entity = s"Could not deserialize parameter string $compTypeString to ComponentType")) } @@ -164,18 +245,30 @@ class Server (handler: RequestHandler) extends HttpApp /** * Returns the number of instances for the specified ComponentType. The ComponentType must be passed as an query * argument named 'ComponentType' (so the call is /numberOfInstances?ComponentType=Crawler). + * * @return Server route that either maps to a 200 OK response containing the number of instance, or the resp. error codes. */ - def numberOfInstances() : server.Route = parameters('ComponentType.as[String]) { compTypeString => + def numberOfInstances(): server.Route = parameters('ComponentType.as[String].?) { compTypeString => authenticateOAuth2[AccessToken]("Secure Site", AuthProvider.authenticateOAuthRequire(_, userType = UserType.User)) { token => get { - log.debug(s"GET /numberOfInstances?ComponentType=$compTypeString has been called") + log.debug(s"GET instances/count?ComponentType=$compTypeString has been called") - val compType : ComponentType = ComponentType.values.find(v => v.toString == compTypeString).orNull + val noValue = "" - if(compType != null) { - complete{handler.getNumberOfInstances(compType).toString()} - } else { + val compTypeStr = compTypeString.getOrElse(noValue) + + val compType: ComponentType = ComponentType.values.find(v => v.toString == compTypeStr).orNull + + if (compType != null) { + complete { + handler.getNumberOfInstances(compType).toString() + } + } else if (compTypeStr == noValue) { + complete { + handler.getAllInstancesCount().toString() + } + } + else { log.error(s"Failed to deserialize parameter string $compTypeString to ComponentType.") complete(HttpResponse(StatusCodes.BadRequest, entity = s"Could not deserialize parameter string $compTypeString to ComponentType")) } @@ -186,19 +279,22 @@ class Server (handler: RequestHandler) extends HttpApp /** * Returns an instance with the specified id. Id is passed as query argument named 'Id' (so the resulting call is * /instance?Id=42) + * * @return Server route that either maps to 200 OK and the respective instance as entity, or 404. */ - def retrieveInstance() : server.Route = parameters('Id.as[Long]) { id => - authenticateOAuth2[AccessToken]("Secure Site", AuthProvider.authenticateOAuthRequire(_, userType = UserType.User)){ token => + def retrieveInstance(id: Long): server.Route = { + authenticateOAuth2[AccessToken]("Secure Site", AuthProvider.authenticateOAuthRequire(_, userType = UserType.User)) { token => get { - log.debug(s"GET /instance?Id=$id has been called") + log.debug(s"GET /instances/$id has been called") val instanceOption = handler.getInstance(id) - if(instanceOption.isDefined){ + if (instanceOption.isDefined) { complete(instanceOption.get.toJson(instanceFormat)) } else { - complete{HttpResponse(StatusCodes.NotFound, entity = s"Id $id was not found on the server.")} + complete { + HttpResponse(StatusCodes.NotFound, entity = s"Id $id was not found on the server.") + } } } } @@ -207,17 +303,18 @@ class Server (handler: RequestHandler) extends HttpApp /** * Returns an instance of the specified ComponentType that can be used to resolve dependencies. The ComponentType must * be passed as an query argument named 'ComponentType' (so the call is /matchingInstance?ComponentType=Crawler). + * * @return Server route that either maps to 200 OK response containing the instance, or the resp. error codes. */ - def matchingInstance() : server.Route = parameters('Id.as[Long], 'ComponentType.as[String]){ (id, compTypeString) => + def matchingInstance(id: Long): server.Route = parameters('ComponentType.as[String]) { (compTypeString) => authenticateOAuth2[AccessToken]("Secure Site", AuthProvider.authenticateOAuthRequire(_, userType = UserType.Component)) { token => - get{ - log.debug(s"GET /matchingInstance?Id=$id&ComponentType=$compTypeString has been called") + get { + log.debug(s"GET instance/$id/matchingInstance?ComponentType=$compTypeString has been called") - val compType : ComponentType = ComponentType.values.find(v => v.toString == compTypeString).orNull + val compType: ComponentType = ComponentType.values.find(v => v.toString == compTypeString).orNull log.info(s"Looking for instance of type $compType ...") - if(compType != null){ + if (compType != null) { handler.getMatchingInstanceOfType(id, compType) match { case (_, Success(matchedInstance)) => log.info(s"Matched request from $id to $matchedInstance.") @@ -227,11 +324,15 @@ class Server (handler: RequestHandler) extends HttpApp complete(HttpResponse(StatusCodes.NotFound, entity = s"Could not find instance with id $id.")) case handler.OperationResult.InvalidTypeForOperation => log.warning(s"Could not handle the creation of instance link, incompatible types found.") - complete{HttpResponse(StatusCodes.BadRequest, entity = s"Invalid dependency type $compType")} + complete { + HttpResponse(StatusCodes.BadRequest, entity = s"Invalid dependency type $compType") + } case handler.OperationResult.Ok => complete(matchedInstance.toJson(instanceFormat)) case handler.OperationResult.InternalError => - complete{HttpResponse(StatusCodes.InternalServerError, entity = s"An internal error occurred")} + complete { + HttpResponse(StatusCodes.InternalServerError, entity = s"An internal error occurred") + } } case (handler.OperationResult.IdUnknown, _) => log.warning(s"Cannot match to instance of type $compType, id $id was not found.") @@ -251,20 +352,39 @@ class Server (handler: RequestHandler) extends HttpApp /** * Applies a matching result to the instance with the specified id. The matching result and id are passed as query * parameters named 'Id' and 'MatchingSuccessful' (so the call is /matchingResult?Id=42&MatchingSuccessful=True). + * * @return Server route that either maps to 200 OK or to the respective error codes */ - def matchInstance() : server.Route = parameters('CallerId.as[Long], 'MatchedInstanceId.as[Long], 'MatchingSuccessful.as[Boolean]){ (callerId, matchedInstanceId, matchingResult) => + def matchInstance(affectedInstanceId: Long, json: JsObject): server.Route = { authenticateOAuth2[AccessToken]("Secure Site", AuthProvider.authenticateOAuthRequire(_, userType = UserType.Component)) { token => + post { - log.debug(s"POST /matchingResult?callerId=$callerId&matchedInstanceId=$matchedInstanceId&MatchingSuccessful=$matchingResult has been called") + log.debug(s"POST /instances/$affectedInstanceId/matchingResult has been called with entity $json") + + Try[(Boolean, Long)] { + val callerId = json.fields("SenderId").toString.toLong + val result = json.fields("MatchingSuccessful").toString.toBoolean + (result, callerId) + } match { + case Success((result, callerId)) => + + handler.handleMatchingResult(callerId, affectedInstanceId, result) match { + case handler.OperationResult.IdUnknown => + log.warning(s"Cannot apply matching result for id $callerId to id $affectedInstanceId, at least one id could not be found") + complete { + HttpResponse(StatusCodes.NotFound, entity = s"One of the ids $callerId and $affectedInstanceId was not found.") + } + case handler.OperationResult.Ok => + complete { + s"Matching result $result processed." + } + } - handler.handleMatchingResult(callerId, matchedInstanceId, matchingResult) match { - case handler.OperationResult.IdUnknown => - log.warning(s"Cannot apply matching result for id $callerId to id $matchedInstanceId, at least one id could not be found") - complete{HttpResponse(StatusCodes.NotFound, entity = s"One of the ids $callerId and $matchedInstanceId was not found.")} - case handler.OperationResult.Ok => - complete{s"Matching result $matchingResult processed."} + case Failure(ex) => + log.warning(s"Failed to unmarshal parameters with message ${ex.getMessage}. Data: $json") + complete{HttpResponse(StatusCodes.BadRequest, entity = "Wrong data format supplied.")} } + } } } @@ -272,16 +392,21 @@ class Server (handler: RequestHandler) extends HttpApp /** * Returns a list of registry events that are associated to the instance with the specified id. The id is passed as * query argument named 'Id' (so the resulting call is /eventList?Id=42). + * * @return Server route mapping to either 200 OK and the list of event, or the resp. error codes. */ - def eventList() : server.Route = parameters('Id.as[Long]){id => - authenticateOAuth2[AccessToken]("Secure Site", AuthProvider.authenticateOAuthRequire(_, userType = UserType.User)){ token => + def eventList(id: Long): server.Route = { + authenticateOAuth2[AccessToken]("Secure Site", AuthProvider.authenticateOAuthRequire(_, userType = UserType.User)) { token => get { - log.debug(s"GET /eventList?Id=$id has been called") + log.debug(s"GET instances/$id//eventList has been called") handler.getEventList(id) match { - case Success(list) => complete{list} - case Failure(_) => complete{HttpResponse(StatusCodes.NotFound, entity = s"Id $id not found.")} + case Success(list) => complete { + list + } + case Failure(_) => complete { + HttpResponse(StatusCodes.NotFound, entity = s"Id $id not found.") + } } } } @@ -291,30 +416,45 @@ class Server (handler: RequestHandler) extends HttpApp * Deploys a new container of the specified type. Also adds the resulting instance to the database. The mandatory * parameter 'ComponentType' is passed as a query argument. The optional parameter 'InstanceName' may also be passed as * query argument (so the resulting call may be /deploy?ComponentType=Crawler&InstanceName=MyCrawler). + * * @return Server route that either maps to 202 ACCEPTED and the generated id of the instance, or the resp. error codes. */ - def deployContainer() : server.Route = parameters('ComponentType.as[String], 'InstanceName.as[String].?) { (compTypeString, name) => - authenticateOAuth2[AccessToken]("Secure Site", AuthProvider.authenticateOAuthRequire(_, userType = UserType.Admin)){ token => + def deployContainer(json: JsObject): server.Route = { + authenticateOAuth2[AccessToken]("Secure Site", AuthProvider.authenticateOAuthRequire(_, userType = UserType.Admin)) { token => post { - if(name.isEmpty){ - log.debug(s"POST /deploy?ComponentType=$compTypeString has been called") - } else { - log.debug(s"POST /deploy?ComponentType=$compTypeString&name=${name.get} has been called") - } - val compType: ComponentType = ComponentType.values.find(v => v.toString == compTypeString).orNull - if (compType != null) { - log.info(s"Trying to deploy container of type $compType" + (if(name.isDefined){s" with name ${name.get}..."}else {"..."})) - handler.handleDeploy(compType, name) match { - case Success(id) => - complete{HttpResponse(StatusCodes.Accepted, entity = id.toString)} - case Failure(x) => - complete{HttpResponse(StatusCodes.InternalServerError, entity = s"Internal server error. Message: ${x.getMessage}")} - } + log.debug(s"POST /instances/deploy has been called with data: $json") + + val name = Try(json.fields("InstanceName").toString.replace("\"", "")).toOption + + Try(json.fields("ComponentType").toString.replace("\"", "")) match { + case Success(compTypeString) => + val compType: ComponentType = ComponentType.values.find(v => v.toString == compTypeString).orNull + + if (compType != null) { + log.info(s"Trying to deploy container of type $compType" + (if (name.isDefined) { + s" with name ${name.get}..." + } else { + "..." + })) + handler.handleDeploy(compType, name) match { + case Success(id) => + complete { + HttpResponse(StatusCodes.Accepted, entity = id.toString) + } + case Failure(x) => + complete { + HttpResponse(StatusCodes.InternalServerError, entity = s"Internal server error. Message: ${x.getMessage}") + } + } - } else { - log.error(s"Failed to deserialize parameter string $compTypeString to ComponentType.") - complete(HttpResponse(StatusCodes.BadRequest, entity = s"Could not deserialize parameter string $compTypeString to ComponentType")) + } else { + log.error(s"Failed to deserialize parameter string $compTypeString to ComponentType.") + complete(HttpResponse(StatusCodes.BadRequest, entity = s"Could not deserialize parameter string $compTypeString to ComponentType")) + } + case Failure(ex) => + log.warning(s"Failed to unmarshal parameters with message ${ex.getMessage}. Data: $json") + complete{HttpResponse(StatusCodes.BadRequest, entity = "Wrong data format supplied.")} } } } @@ -323,22 +463,31 @@ class Server (handler: RequestHandler) extends HttpApp /** * Called to report that the instance with the specified id was started successfully. The Id is passed as query * parameter named 'Id' (so the resulting call is /reportStart?Id=42) + * * @return Server route that either maps to 200 OK or the respective error codes */ - def reportStart() : server.Route = parameters('Id.as[Long]) {id => + def reportStart(id: Long): server.Route = { authenticateOAuth2[AccessToken]("Secure Site", AuthProvider.authenticateOAuthRequire(_, userType = UserType.Component)) { token => post { handler.handleReportStart(id) match { case handler.OperationResult.IdUnknown => log.warning(s"Cannot report start for id $id, that id was not found.") - complete{HttpResponse(StatusCodes.NotFound, entity = s"Id $id not found.")} + complete { + HttpResponse(StatusCodes.NotFound, entity = s"Id $id not found.") + } case handler.OperationResult.NoDockerContainer => log.warning(s"Cannot report start for id $id, that instance is not running in a docker container.") - complete{HttpResponse(StatusCodes.BadRequest, entity = s"Id $id is not running in a docker container.")} + complete { + HttpResponse(StatusCodes.BadRequest, entity = s"Id $id is not running in a docker container.") + } case handler.OperationResult.Ok => - complete{"Report successfully processed."} + complete { + "Report successfully processed." + } case r => - complete{HttpResponse(StatusCodes.InternalServerError, entity = s"Internal server error, unknown operation result $r")} + complete { + HttpResponse(StatusCodes.InternalServerError, entity = s"Internal server error, unknown operation result $r") + } } } } @@ -347,22 +496,31 @@ class Server (handler: RequestHandler) extends HttpApp /** * Called to report that the instance with the specified id was stopped successfully. The Id is passed as query * parameter named 'Id' (so the resulting call is /reportStop?Id=42) + * * @return Server route that either maps to 200 OK or the respective error codes */ - def reportStop() : server.Route = parameters('Id.as[Long]) {id => + def reportStop(id: Long): server.Route = { authenticateOAuth2[AccessToken]("Secure Site", AuthProvider.authenticateOAuthRequire(_, userType = UserType.Component)) { token => post { handler.handleReportStop(id) match { case handler.OperationResult.IdUnknown => log.warning(s"Cannot report start for id $id, that id was not found.") - complete{HttpResponse(StatusCodes.NotFound, entity = s"Id $id not found.")} + complete { + HttpResponse(StatusCodes.NotFound, entity = s"Id $id not found.") + } case handler.OperationResult.NoDockerContainer => log.warning(s"Cannot report start for id $id, that instance is not running in a docker container.") - complete{HttpResponse(StatusCodes.BadRequest, entity = s"Id $id is not running in a docker container.")} + complete { + HttpResponse(StatusCodes.BadRequest, entity = s"Id $id is not running in a docker container.") + } case handler.OperationResult.Ok => - complete{"Report successfully processed."} + complete { + "Report successfully processed." + } case r => - complete{HttpResponse(StatusCodes.InternalServerError, entity = s"Internal server error, unknown operation result $r")} + complete { + HttpResponse(StatusCodes.InternalServerError, entity = s"Internal server error, unknown operation result $r") + } } } } @@ -371,28 +529,37 @@ class Server (handler: RequestHandler) extends HttpApp /** * Called to report that the instance with the specified id encountered a failure. The Id is passed as query * parameter named 'Id' (so the resulting call is /reportFailure?Id=42) + * * @return Server route that either maps to 200 OK or the respective error codes */ - def reportFailure() : server.Route = parameters('Id.as[Long], 'ErrorLog.as[String].?) {(id, errorLog) => + def reportFailure(id: Long): server.Route = parameters('ErrorLog.as[String].?) { (errorLog) => authenticateOAuth2[AccessToken]("Secure Site", AuthProvider.authenticateOAuthRequire(_, userType = UserType.Component)) { token => - post{ - if(errorLog.isEmpty){ - log.debug(s"POST /reportFailure?Id=$id has been called") + post { + if (errorLog.isEmpty) { + log.debug(s"POST /instances/$id/reportFailure has been called") } else { - log.debug(s"POST /reportFailure?Id=$id&ErrorLog=${errorLog.get} has been called") + log.debug(s"POST /instances/$id/reportFailure&ErrorLog=${errorLog.get} has been called") } handler.handleReportFailure(id, errorLog) match { case handler.OperationResult.IdUnknown => log.warning(s"Cannot report failure for id $id, that id was not found.") - complete{HttpResponse(StatusCodes.NotFound, entity = s"Id $id not found.")} + complete { + HttpResponse(StatusCodes.NotFound, entity = s"Id $id not found.") + } case handler.OperationResult.NoDockerContainer => log.warning(s"Cannot report failure for id $id, that instance is not running in a docker container.") - complete{HttpResponse(StatusCodes.BadRequest, entity = s"Id $id is not running in a docker container.")} + complete { + HttpResponse(StatusCodes.BadRequest, entity = s"Id $id is not running in a docker container.") + } case handler.OperationResult.Ok => - complete{"Report successfully processed."} + complete { + "Report successfully processed." + } case r => - complete{HttpResponse(StatusCodes.InternalServerError, entity = s"Internal server error, unknown operation result $r")} + complete { + HttpResponse(StatusCodes.InternalServerError, entity = s"Internal server error, unknown operation result $r") + } } } } @@ -401,26 +568,37 @@ class Server (handler: RequestHandler) extends HttpApp /** * Called to pause the instance with the specified id. The associated docker container is paused. The Id is passed * as a query argument named 'Id' (so the resulting call is /pause?Id=42). + * * @return Server route that either maps to 202 ACCEPTED or the expected error codes. */ - def pause() : server.Route = parameters('Id.as[Long]) { id => - authenticateOAuth2[AccessToken]("Secure Site", AuthProvider.authenticateOAuthRequire(_, userType = UserType.Admin)) {token => - post{ - log.debug(s"POST /pause?Id=$id has been called") + def pause(id: Long): server.Route = { + authenticateOAuth2[AccessToken]("Secure Site", AuthProvider.authenticateOAuthRequire(_, userType = UserType.Admin)) { token => + post { + log.debug(s"POST /instances/$id/pause has been called") handler.handlePause(id) match { case handler.OperationResult.IdUnknown => log.warning(s"Cannot pause id $id, that id was not found.") - complete{HttpResponse(StatusCodes.NotFound, entity = s"Id $id not found.")} + complete { + HttpResponse(StatusCodes.NotFound, entity = s"Id $id not found.") + } case handler.OperationResult.NoDockerContainer => log.warning(s"Cannot pause id $id, that instance is not running in a docker container.") - complete{HttpResponse(StatusCodes.BadRequest, entity = s"Id $id is not running in a docker container.")} + complete { + HttpResponse(StatusCodes.BadRequest, entity = s"Id $id is not running in a docker container.") + } case handler.OperationResult.InvalidStateForOperation => log.warning(s"Cannot pause id $id, that instance is not running.") - complete{HttpResponse(StatusCodes.BadRequest, entity = s"Id $id is not running .")} + complete { + HttpResponse(StatusCodes.BadRequest, entity = s"Id $id is not running .") + } case handler.OperationResult.Ok => - complete{HttpResponse(StatusCodes.Accepted, entity = "Operation accepted.")} + complete { + HttpResponse(StatusCodes.Accepted, entity = "Operation accepted.") + } case r => - complete{HttpResponse(StatusCodes.InternalServerError, entity = s"Internal server error, unknown operation result $r")} + complete { + HttpResponse(StatusCodes.InternalServerError, entity = s"Internal server error, unknown operation result $r") + } } } } @@ -429,26 +607,37 @@ class Server (handler: RequestHandler) extends HttpApp /** * Called to resume the instance with the specified id. The associated docker container is resumed. The Id is passed * as a query argument named 'Id' (so the resulting call is /resume?Id=42). + * * @return Server route that either maps to 202 ACCEPTED or the expected error codes. */ - def resume() : server.Route = parameters('Id.as[Long]) { id => - authenticateOAuth2[AccessToken]("Secure Site", AuthProvider.authenticateOAuthRequire(_, userType = UserType.Admin)){ token => + def resume(id: Long): server.Route = { + authenticateOAuth2[AccessToken]("Secure Site", AuthProvider.authenticateOAuthRequire(_, userType = UserType.Admin)) { token => post { - log.debug(s"POST /resume?Id=$id has been called") + log.debug(s"POST /instances/$id/resume has been called") handler.handleResume(id) match { case handler.OperationResult.IdUnknown => log.warning(s"Cannot resume id $id, that id was not found.") - complete{HttpResponse(StatusCodes.NotFound, entity = s"Id $id not found.")} + complete { + HttpResponse(StatusCodes.NotFound, entity = s"Id $id not found.") + } case handler.OperationResult.NoDockerContainer => log.warning(s"Cannot resume id $id, that instance is not running in a docker container.") - complete{HttpResponse(StatusCodes.BadRequest, entity = s"Id $id is not running in a docker container.")} + complete { + HttpResponse(StatusCodes.BadRequest, entity = s"Id $id is not running in a docker container.") + } case handler.OperationResult.InvalidStateForOperation => log.warning(s"Cannot resume id $id, that instance is not paused.") - complete {HttpResponse(StatusCodes.BadRequest, entity = s"Id $id is not paused.")} + complete { + HttpResponse(StatusCodes.BadRequest, entity = s"Id $id is not paused.") + } case handler.OperationResult.Ok => - complete{HttpResponse(StatusCodes.Accepted, entity = "Operation accepted.")} + complete { + HttpResponse(StatusCodes.Accepted, entity = "Operation accepted.") + } case r => - complete{HttpResponse(StatusCodes.InternalServerError, entity = s"Internal server error, unknown operation result $r")} + complete { + HttpResponse(StatusCodes.InternalServerError, entity = s"Internal server error, unknown operation result $r") + } } } } @@ -457,26 +646,37 @@ class Server (handler: RequestHandler) extends HttpApp /** * Called to stop the instance with the specified id. The associated docker container is stopped. The Id is passed * as a query argument named 'Id' (so the resulting call is /stop?Id=42). + * * @return Server route that either maps to 202 ACCEPTED or the expected error codes. */ - def stop() : server.Route = parameters('Id.as[Long]) { id => - authenticateOAuth2[AccessToken]("Secure Site", AuthProvider.authenticateOAuthRequire(_, userType = UserType.Admin)){ token => + def stop(id: Long): server.Route = { + authenticateOAuth2[AccessToken]("Secure Site", AuthProvider.authenticateOAuthRequire(_, userType = UserType.Admin)) { token => post { - log.debug(s"POST /stop?Id=$id has been called") + log.debug(s"POST /instances/$id/stop has been called") handler.handleStop(id) match { case handler.OperationResult.IdUnknown => log.warning(s"Cannot stop id $id, that id was not found.") - complete{HttpResponse(StatusCodes.NotFound, entity = s"Id $id not found.")} + complete { + HttpResponse(StatusCodes.NotFound, entity = s"Id $id not found.") + } case handler.OperationResult.InvalidTypeForOperation => log.warning(s"Cannot stop id $id, this component type cannot be stopped.") - complete{HttpResponse(StatusCodes.BadRequest, entity = s"Cannot stop instance of this type.")} + complete { + HttpResponse(StatusCodes.BadRequest, entity = s"Cannot stop instance of this type.") + } case handler.OperationResult.InvalidStateForOperation => log.warning(s"Cannot stop id $id, the associated container is paused.") - complete{HttpResponse(StatusCodes.BadRequest, entity = s"Cannot stop instance while it is paused.")} + complete { + HttpResponse(StatusCodes.BadRequest, entity = s"Cannot stop instance while it is paused.") + } case handler.OperationResult.Ok => - complete{HttpResponse(StatusCodes.Accepted, entity = "Operation accepted.")} + complete { + HttpResponse(StatusCodes.Accepted, entity = "Operation accepted.") + } case r => - complete{HttpResponse(StatusCodes.InternalServerError, entity = s"Internal server error, unknown operation result $r")} + complete { + HttpResponse(StatusCodes.InternalServerError, entity = s"Internal server error, unknown operation result $r") + } } } } @@ -485,26 +685,37 @@ class Server (handler: RequestHandler) extends HttpApp /** * Called to start the instance with the specified id. The associated docker container is started. The Id is passed * as a query argument named 'Id' (so the resulting call is /start?Id=42). + * * @return Server route that either maps to 202 ACCEPTED or the expected error codes. */ - def start() : server.Route = parameters('Id.as[Long]) { id => - authenticateOAuth2[AccessToken]("Secure Site", AuthProvider.authenticateOAuthRequire(_, userType = UserType.Admin)){ token => - post{ - log.debug(s"POST /start?Id=$id has been called") + def start(id: Long): server.Route = { + authenticateOAuth2[AccessToken]("Secure Site", AuthProvider.authenticateOAuthRequire(_, userType = UserType.Admin)) { token => + post { + log.debug(s"POST /instances/$id/start has been called") handler.handleStart(id) match { case handler.OperationResult.IdUnknown => log.warning(s"Cannot start id $id, that id was not found.") - complete{HttpResponse(StatusCodes.NotFound, entity = s"Id $id not found.")} + complete { + HttpResponse(StatusCodes.NotFound, entity = s"Id $id not found.") + } case handler.OperationResult.NoDockerContainer => log.warning(s"Cannot start id $id, that instance is not running in a docker container.") - complete{HttpResponse(StatusCodes.BadRequest, entity = s"Id $id is not running in a docker container.")} + complete { + HttpResponse(StatusCodes.BadRequest, entity = s"Id $id is not running in a docker container.") + } case handler.OperationResult.InvalidStateForOperation => log.warning(s"Cannot start id $id, that instance is not stopped.") - complete {HttpResponse(StatusCodes.BadRequest, entity = s"Id $id is not stopped.")} + complete { + HttpResponse(StatusCodes.BadRequest, entity = s"Id $id is not stopped.") + } case handler.OperationResult.Ok => - complete{HttpResponse(StatusCodes.Accepted, entity = "Operation accepted.")} + complete { + HttpResponse(StatusCodes.Accepted, entity = "Operation accepted.") + } case r => - complete{HttpResponse(StatusCodes.InternalServerError, entity = s"Internal server error, unknown operation result $r")} + complete { + HttpResponse(StatusCodes.InternalServerError, entity = s"Internal server error, unknown operation result $r") + } } } } @@ -513,28 +724,41 @@ class Server (handler: RequestHandler) extends HttpApp /** * Called to delete the instance with the specified id as well as the associated docker container. The Id is passed * as a query argument named 'Id' (so the resulting call is /delete?Id=42). + * * @return Server route that either maps to 202 ACCEPTED or the respective error codes. */ - def deleteContainer() : server.Route = parameters('Id.as[Long]) { id => - authenticateOAuth2[AccessToken]("Secure Site", AuthProvider.authenticateOAuthRequire(_, userType = UserType.Admin)){ token => - post{ + def deleteContainer(id: Long): server.Route = { + authenticateOAuth2[AccessToken]("Secure Site", AuthProvider.authenticateOAuthRequire(_, userType = UserType.Admin)) { token => + post { log.debug(s"POST /delete?Id=$id has been called") handler.handleDeleteContainer(id) match { case handler.OperationResult.IdUnknown => log.warning(s"Cannot delete id $id, that id was not found.") - complete{HttpResponse(StatusCodes.NotFound, entity = s"Id $id not found.")} + complete { + HttpResponse(StatusCodes.NotFound, entity = s"Id $id not found.") + } case handler.OperationResult.NoDockerContainer => log.warning(s"Cannot delete id $id, that instance is not running in a docker container.") - complete{HttpResponse(StatusCodes.BadRequest, entity = s"Id $id is not running in a docker container.")} + complete { + HttpResponse(StatusCodes.BadRequest, entity = s"Id $id is not running in a docker container.") + } case handler.OperationResult.InvalidStateForOperation => log.warning(s"Cannot delete id $id, that instance is still running.") - complete {HttpResponse(StatusCodes.BadRequest, entity = s"Id $id is not stopped.")} + complete { + HttpResponse(StatusCodes.BadRequest, entity = s"Id $id is not stopped.") + } case handler.OperationResult.Ok => - complete{HttpResponse(StatusCodes.Accepted, entity = "Operation accepted.")} + complete { + HttpResponse(StatusCodes.Accepted, entity = "Operation accepted.") + } case handler.OperationResult.InternalError => - complete{HttpResponse(StatusCodes.InternalServerError, entity = s"Internal server error")} + complete { + HttpResponse(StatusCodes.InternalServerError, entity = s"Internal server error") + } case handler.OperationResult.BlockingDependency => - complete{HttpResponse(StatusCodes.BadRequest, entity = s"Cannot delete this instance, other running instances are depending on it.")} + complete { + HttpResponse(StatusCodes.BadRequest, entity = s"Cannot delete this instance, other running instances are depending on it.") + } } } } @@ -544,27 +768,47 @@ class Server (handler: RequestHandler) extends HttpApp * Called to assign a new instance dependency to the instance with the specified id. Both the ids of the instance and * the specified dependency are passed as query arguments named 'Id' and 'assignedInstanceId' resp. (so the resulting * call is /assignInstance?Id=42&assignedInstanceId=43). Will update the dependency in DB and than restart the container. + * * @return Server route that either maps to 202 ACCEPTED or the respective error codes */ - def assignInstance() : server.Route = parameters('Id.as[Long], 'AssignedInstanceId.as[Long]) { (id, assignedInstanceId) => - authenticateOAuth2[AccessToken]("Secure Site", AuthProvider.authenticateOAuthRequire(_, userType = UserType.Admin)){ token => - post { - log.debug(s"POST /assignInstance?Id=$id&assignedInstanceId=$assignedInstanceId has been called") + def assignInstance(id: Long, json: JsObject): server.Route = { + authenticateOAuth2[AccessToken]("Secure Site", AuthProvider.authenticateOAuthRequire(_, userType = UserType.Admin)) { token => - handler.handleInstanceAssignment(id, assignedInstanceId) match { - case handler.OperationResult.IdUnknown => - log.warning(s"Cannot assign $assignedInstanceId to $id, one or more ids not found.") - complete{HttpResponse(StatusCodes.NotFound, entity = s"Cannot assign instance, at least one of the ids $id / $assignedInstanceId was not found.")} - case handler.OperationResult.NoDockerContainer => - log.warning(s"Cannot assign $assignedInstanceId to $id, $id is no docker container.") - complete{HttpResponse(StatusCodes.BadRequest,entity = s"Cannot assign instance, $id is no docker container.")} - case handler.OperationResult.InvalidTypeForOperation => - log.warning(s"Cannot assign $assignedInstanceId to $id, incompatible types.") - complete{HttpResponse(StatusCodes.BadRequest,entity = s"Cannot assign $assignedInstanceId to $id, incompatible types.")} - case handler.OperationResult.Ok => - complete{HttpResponse(StatusCodes.Accepted, entity = "Operation accepted.")} - case x => - complete{HttpResponse(StatusCodes.InternalServerError, entity = s"Unexpected operation result $x")} + post { + log.debug(s"POST /instances/$id/assignInstance has been called with data : $json ") + + Try[Long] { + json.fields("AssignedInstanceId").toString.toLong + } match { + case Success(assignedInstanceId) => + handler.handleInstanceAssignment(id, assignedInstanceId) match { + case handler.OperationResult.IdUnknown => + log.warning(s"Cannot assign $assignedInstanceId to $id, one or more ids not found.") + complete { + HttpResponse(StatusCodes.NotFound, entity = s"Cannot assign instance, at least one of the ids $id / $assignedInstanceId was not found.") + } + case handler.OperationResult.NoDockerContainer => + log.warning(s"Cannot assign $assignedInstanceId to $id, $id is no docker container.") + complete { + HttpResponse(StatusCodes.BadRequest, entity = s"Cannot assign instance, $id is no docker container.") + } + case handler.OperationResult.InvalidTypeForOperation => + log.warning(s"Cannot assign $assignedInstanceId to $id, incompatible types.") + complete { + HttpResponse(StatusCodes.BadRequest, entity = s"Cannot assign $assignedInstanceId to $id, incompatible types.") + } + case handler.OperationResult.Ok => + complete { + HttpResponse(StatusCodes.Accepted, entity = "Operation accepted.") + } + case x => + complete { + HttpResponse(StatusCodes.InternalServerError, entity = s"Unexpected operation result $x") + } + } + case Failure(ex) => + log.warning(s"Failed to unmarshal parameters with message ${ex.getMessage}. Data: $json") + complete{HttpResponse(StatusCodes.BadRequest, entity = "Wrong data format supplied.")} } } } @@ -573,19 +817,24 @@ class Server (handler: RequestHandler) extends HttpApp /** * Called to get a list of links from the instance with the specified id. The id is passed as query argument named * 'Id' (so the resulting call is /linksFrom?Id=42). + * * @return Server route that either maps to 200 OK (and the list of links as content), or the respective error code. */ - def linksFrom() : server.Route = parameters('Id.as[Long]) { id => - authenticateOAuth2[AccessToken]("Secure Site", AuthProvider.authenticateOAuthRequire(_, userType = UserType.User)){ token => + def linksFrom(id: Long): server.Route = { + authenticateOAuth2[AccessToken]("Secure Site", AuthProvider.authenticateOAuthRequire(_, userType = UserType.User)) { token => get { - log.debug(s"GET /linksFrom?Id=$id has been called.") + log.debug(s"GET /instances/$id/linksFrom has been called.") handler.handleGetLinksFrom(id) match { case Success(linkList) => - complete{linkList} + complete { + linkList + } case Failure(ex) => log.warning(s"Failed to get links from $id with message: ${ex.getMessage}") - complete{HttpResponse(StatusCodes.NotFound, entity = s"Failed to get links from $id, that id is not known.")} + complete { + HttpResponse(StatusCodes.NotFound, entity = s"Failed to get links from $id, that id is not known.") + } } } } @@ -594,19 +843,24 @@ class Server (handler: RequestHandler) extends HttpApp /** * Called to get a list of links to the instance with the specified id. The id is passed as query argument named * 'Id' (so the resulting call is /linksTo?Id=42). + * * @return Server route that either maps to 200 OK (and the list of links as content), or the respective error code. */ - def linksTo() : server.Route = parameters('Id.as[Long]) {id => - authenticateOAuth2[AccessToken]("Secure Site", AuthProvider.authenticateOAuthRequire(_, userType = UserType.User)){ token => + def linksTo(id: Long): server.Route = { + authenticateOAuth2[AccessToken]("Secure Site", AuthProvider.authenticateOAuthRequire(_, userType = UserType.User)) { token => get { - log.debug(s"GET /linksTo?Id=$id has been called.") + log.debug(s"GET instances/$id/linksTo has been called.") handler.handleGetLinksTo(id) match { case Success(linkList) => - complete{linkList} + complete { + linkList + } case Failure(ex) => log.warning(s"Failed to get links to $id with message: ${ex.getMessage}") - complete{HttpResponse(StatusCodes.NotFound, entity = s"Failed to get links to $id, that id is not known.")} + complete { + HttpResponse(StatusCodes.NotFound, entity = s"Failed to get links to $id, that id is not known.") + } } } @@ -616,13 +870,16 @@ class Server (handler: RequestHandler) extends HttpApp /** * Called to get the whole network graph of the current registry. Contains a list of all instances and all links * currently registered. + * * @return Server route that maps to 200 OK and the current InstanceNetwork as content. */ - def network() : server.Route = { - authenticateOAuth2[AccessToken]("Secure Site", AuthProvider.authenticateOAuthRequire(_, userType = UserType.User)){ token => + def network(): server.Route = { + authenticateOAuth2[AccessToken]("Secure Site", AuthProvider.authenticateOAuthRequire(_, userType = UserType.User)) { token => get { - log.debug(s"GET /network has been called.") - complete{handler.handleGetNetwork().toJson} + log.debug(s"GET /instances/network has been called.") + complete { + handler.handleGetNetwork().toJson + } } } } @@ -630,23 +887,36 @@ class Server (handler: RequestHandler) extends HttpApp /** * Called to add a generic label to the instance with the specified id. The Id and label are passed as query arguments * named 'Id' and 'Label', resp. (so the resulting call is /addLabel?Id=42&Label=private) + * * @return Server route that either maps to 200 OK or the respective error codes. */ - def addLabel() : server.Route = parameters('Id.as[Long], 'Label.as[String]){ (id, label) => - authenticateOAuth2[AccessToken]("Secure Site", AuthProvider.authenticateOAuthRequire(_, userType = UserType.Admin)){ token => + def addLabel(id: Long, json: JsObject): server.Route = { + authenticateOAuth2[AccessToken]("Secure Site", AuthProvider.authenticateOAuthRequire(_, userType = UserType.Admin)) { token => + post { - log.debug(s"POST /addLabel?Id=$id&Label=$label has been called.") - handler.handleAddLabel(id, label) match { - case handler.OperationResult.IdUnknown => - log.warning(s"Cannot add label $label to $id, id not found.") - complete{HttpResponse(StatusCodes.NotFound, entity = s"Cannot add label, id $id not found.")} - case handler.OperationResult.InternalError => - log.warning(s"Error while adding label $label to $id: Label exceeds character limit.") - complete{HttpResponse(StatusCodes.BadRequest, - entity = s"Cannot add label to $id, label exceeds character limit of ${Registry.configuration.maxLabelLength}")} - case handler.OperationResult.Ok => - log.info(s"Successfully added label $label to instance with id $id.") - complete("Successfully added label") + log.debug(s"POST /instances/$id/label has been called with data $json.") + + Try[String](json.fields("Label").toString.replace("\"", "")) match { + case Success(label) => + handler.handleAddLabel(id, label) match { + case handler.OperationResult.IdUnknown => + log.warning(s"Cannot add label $label to $id, id not found.") + complete { + HttpResponse(StatusCodes.NotFound, entity = s"Cannot add label, id $id not found.") + } + case handler.OperationResult.InternalError => + log.warning(s"Error while adding label $label to $id: Label exceeds character limit.") + complete { + HttpResponse(StatusCodes.BadRequest, + entity = s"Cannot add label to $id, label exceeds character limit of ${Registry.configuration.maxLabelLength}") + } + case handler.OperationResult.Ok => + log.info(s"Successfully added label $label to instance with id $id.") + complete("Successfully added label") + } + case Failure(ex) => + log.warning(s"Failed to unmarshal parameters with message ${ex.getMessage}. Data: $json") + complete{HttpResponse(StatusCodes.BadRequest, entity = "Wrong data format supplied.")} } } } @@ -655,33 +925,56 @@ class Server (handler: RequestHandler) extends HttpApp /** * Called to run a command in a docker container. The Id an Command is the required parameter there are other optional parameter can be passed * a query with required parameter Command and Id (so the resulting call is /command?Id=42&Command=ls). + * * @return Server route that either maps to 200 Ok or the respective error codes. */ - def runCommandInContainer() : server.Route = parameters('Id.as[Long], 'Command.as[String], - 'AttachStdin.as[Boolean].?, 'AttachStdout.as[Boolean].?, - 'AttachStderr.as[Boolean].?,'DetachKeys.as[String].?, 'Privileged.as[Boolean].?,'Tty.as[Boolean].?, 'User.as[String].? - ) { (id, command, attachStdin, attachStdout, attachStderr, detachKeys, privileged, tty, user) => - authenticateOAuth2[AccessToken]("Secure Site", AuthProvider.authenticateOAuthRequire(_, userType = UserType.Admin)){ token => + def runCommandInContainer(id:Long, json: JsObject): server.Route = { + authenticateOAuth2[AccessToken]("Secure Site", AuthProvider.authenticateOAuthRequire(_, userType = UserType.Admin)) { token => post { log.debug(s"POST /command has been called") - handler.handleCommand(id, command, attachStdin, attachStdout, attachStderr, detachKeys, privileged, tty, user) match { - case handler.OperationResult.IdUnknown => - log.warning(s"Cannot run command $command to $id, id not found.") - complete{HttpResponse(StatusCodes.NotFound, entity = s"Cannot run command, id $id not found.")} - case handler.OperationResult.NoDockerContainer => - log.warning(s"Cannot run command $command to $id, $id is no docker container.") - complete{HttpResponse(StatusCodes.BadRequest,entity = s"Cannot run command, $id is no docker container.")} - case handler.OperationResult.Ok => - complete{HttpResponse(StatusCodes.OK)} - case r => - complete{HttpResponse(StatusCodes.InternalServerError, entity = s"Internal server error, unknown operation result $r")} + + val privileged = Try(json.fields("Privileged").toString.toBoolean) match { + case Success(res) => Some(res) + case Failure(_) => None + } + + val user = Try(json.fields("User").toString.replace("\"", "")) match { + case Success(res) => Some(res) + case Failure(_) => None + } + + Try(json.fields("Command").toString.replace("\"", "")) match { + case Success(command) => + handler.handleCommand(id, command, None, None, None, None, privileged, None, user) match { + case handler.OperationResult.IdUnknown => + log.warning(s"Cannot run command $command to $id, id not found.") + complete { + HttpResponse(StatusCodes.NotFound, entity = s"Cannot run command, id $id not found.") + } + case handler.OperationResult.NoDockerContainer => + log.warning(s"Cannot run command $command to $id, $id is no docker container.") + complete { + HttpResponse(StatusCodes.BadRequest, entity = s"Cannot run command, $id is no docker container.") + } + case handler.OperationResult.Ok => + complete { + HttpResponse(StatusCodes.OK) + } + case r => + complete { + HttpResponse(StatusCodes.InternalServerError, entity = s"Internal server error, unknown operation result $r") + } + } + case Failure(ex) => + log.warning(s"Failed to unmarshal parameters with message ${ex.getMessage}. Data: $json") + complete{HttpResponse(StatusCodes.BadRequest, entity = "Wrong data format supplied.")} } } } } - def retrieveLogs(): server.Route = parameters('Id.as[Long], 'StdErr.as[Boolean].?) { (id, stdErrOption) => - authenticateOAuth2[AccessToken]("Secure Site", AuthProvider.authenticateOAuthRequire(_, userType = UserType.Admin)){ token => + def retrieveLogs(id: Long): server.Route = parameters('StdErr.as[Boolean].?) { stdErrOption => + authenticateOAuth2[AccessToken]("Secure Site", AuthProvider.authenticateOAuthRequire(_, userType = UserType.Admin)) { token => get { log.debug(s"GET /logs?Id=$id has been called") @@ -690,38 +983,52 @@ class Server (handler: RequestHandler) extends HttpApp handler.handleGetLogs(id, stdErrSelected) match { case (handler.OperationResult.IdUnknown, _) => log.warning(s"Cannot get logs, id $id not found.") - complete{HttpResponse(StatusCodes.NotFound, entity = s"Cannot get logs, id $id not found.")} + complete { + HttpResponse(StatusCodes.NotFound, entity = s"Cannot get logs, id $id not found.") + } case (handler.OperationResult.NoDockerContainer, _) => log.warning(s"Cannot get logs, id $id is no docker container.") - complete{HttpResponse(StatusCodes.BadRequest,entity = s"Cannot get logs, id $id is no docker container.")} + complete { + HttpResponse(StatusCodes.BadRequest, entity = s"Cannot get logs, id $id is no docker container.") + } case (handler.OperationResult.Ok, Some(logString)) => - complete{logString} + complete { + logString + } case (handler.OperationResult.InternalError, _) => - complete{HttpResponse(StatusCodes.InternalServerError, entity = s"Internal server error")} + complete { + HttpResponse(StatusCodes.InternalServerError, entity = s"Internal server error") + } case _ => - complete{HttpResponse(StatusCodes.InternalServerError, entity = s"Internal server error")} + complete { + HttpResponse(StatusCodes.InternalServerError, entity = s"Internal server error") + } } } } } - def streamLogs(): server.Route = parameters('Id.as[Long], 'StdErr.as[Boolean].?) { (id, stdErrOption) => + def streamLogs(id: Long): server.Route = parameters('StdErr.as[Boolean].?) { stdErrOption => val stdErrSelected = stdErrOption.isDefined && stdErrOption.get handler.handleStreamLogs(id, stdErrSelected) match { case (handler.OperationResult.IdUnknown, _) => - complete{HttpResponse(StatusCodes.NotFound, entity = s"Cannot stream logs, id $id not found.")} + complete { + HttpResponse(StatusCodes.NotFound, entity = s"Cannot stream logs, id $id not found.") + } case (handler.OperationResult.NoDockerContainer, _) => - complete{HttpResponse(StatusCodes.BadRequest, entity = s"Cannot stream logs, id $id is no docker container.")} + complete { + HttpResponse(StatusCodes.BadRequest, entity = s"Cannot stream logs, id $id is no docker container.") + } case (handler.OperationResult.Ok, Some(publisher)) => handleWebSocketMessages { Flow[Message] .via( Flow.fromSinkAndSource(Sink.ignore, Source.fromPublisher(publisher)) ) - .watchTermination() {(_, done) => + .watchTermination() { (_, done) => done.onComplete { case Success(_) => log.info("Log stream route completed successfully") @@ -731,40 +1038,45 @@ class Server (handler: RequestHandler) extends HttpApp } } case (handler.OperationResult.InternalError, _) => - complete{HttpResponse(StatusCodes.InternalServerError, entity = s"Internal server error")} + complete { + HttpResponse(StatusCodes.InternalServerError, entity = s"Internal server error") + } case _ => - complete{HttpResponse(StatusCodes.InternalServerError, entity = s"Internal server error")} + complete { + HttpResponse(StatusCodes.InternalServerError, entity = s"Internal server error") + } } } /** * Creates a WebSocketConnection that streams events that are issued by the registry to all connected clients. + * * @return Server route that maps to the WebSocketConnection */ - def streamEvents() : server.Route = { - handleWebSocketMessages{ + def streamEvents(): server.Route = { + handleWebSocketMessages { //Flush pending messages from publisher Source.fromPublisher(handler.eventPublisher).to(Sink.ignore).run() //Create flow from publisher Flow[Message] - .map{ + .map { case TextMessage.Strict(msg: String) => msg case _ => println("Ignored non-text message.") } .via( Flow.fromSinkAndSource(Sink.foreach(println), Source.fromPublisher(handler.eventPublisher) - .map(event => event.toJson(eventFormat).toString)) + .map(event => event.toJson(eventFormat).toString)) ) - .map{msg: String => TextMessage.Strict(msg + "\n")} + .map { msg: String => TextMessage.Strict(msg + "\n") } .watchTermination() { (_, done) => - done.onComplete { - case Success(_) => - log.info("Stream route completed successfully") - case Failure(ex) => - log.error(s"Stream route completed with failure : $ex") + done.onComplete { + case Success(_) => + log.info("Stream route completed successfully") + case Failure(ex) => + log.error(s"Stream route completed with failure : $ex") + } } - } } } diff --git a/src/test/scala/de/upb/cs/swt/delphi/instanceregistry/connection/ServerTest.scala b/src/test/scala/de/upb/cs/swt/delphi/instanceregistry/connection/ServerTest.scala index f0828ec..46bdc56 100644 --- a/src/test/scala/de/upb/cs/swt/delphi/instanceregistry/connection/ServerTest.scala +++ b/src/test/scala/de/upb/cs/swt/delphi/instanceregistry/connection/ServerTest.scala @@ -23,27 +23,27 @@ import akka.http.scaladsl.model._ import akka.http.scaladsl.server.Route import akka.http.scaladsl.testkit.ScalatestRouteTest import de.upb.cs.swt.delphi.instanceregistry.Docker.DockerConnection -import de.upb.cs.swt.delphi.instanceregistry.{Configuration, Registry, RequestHandler} -import org.scalatest.{Matchers, WordSpec} import de.upb.cs.swt.delphi.instanceregistry.daos.{DynamicInstanceDAO, InstanceDAO} import de.upb.cs.swt.delphi.instanceregistry.io.swagger.client.model.EventEnums.EventType -import de.upb.cs.swt.delphi.instanceregistry.io.swagger.client.model._ import de.upb.cs.swt.delphi.instanceregistry.io.swagger.client.model.InstanceEnums.{ComponentType, InstanceState} import de.upb.cs.swt.delphi.instanceregistry.io.swagger.client.model.LinkEnums.LinkState +import de.upb.cs.swt.delphi.instanceregistry.io.swagger.client.model._ +import de.upb.cs.swt.delphi.instanceregistry.{Configuration, Registry, RequestHandler} +import org.scalatest.{Matchers, WordSpec} import pdi.jwt.{Jwt, JwtAlgorithm, JwtClaim} import spray.json._ -import scala.concurrent.duration.Duration import scala.concurrent.Await +import scala.concurrent.duration.Duration import scala.util.{Failure, Success, Try} class ServerTest extends WordSpec - with Matchers - with ScalatestRouteTest - with InstanceJsonSupport - with EventJsonSupport { + with Matchers + with ScalatestRouteTest + with InstanceJsonSupport + with EventJsonSupport { private val configuration: Configuration = new Configuration() private val dao: InstanceDAO = new DynamicInstanceDAO(configuration) @@ -61,8 +61,6 @@ class ServerTest private val invalidJsonInstance = validJsonInstance.replace(""""name":"ValidInstance",""", """"name":Invalid", """) - - /** * Before all tests: Initialize handler and wait for server binding to be ready. */ @@ -92,79 +90,68 @@ class ServerTest //Invalid register "not register when entity is invalid" in { //No entity - Post("/register") ~> addAuthorization("Component") ~> server.routes ~> check { + Post("/instances/register") ~> addAuthorization("Component") ~> server.routes ~> check { assert(status === StatusCodes.BAD_REQUEST) responseAs[String].toLowerCase should include("failed to parse json") } //Wrong JSON syntax - Post("/register", HttpEntity(ContentTypes.`application/json`, invalidJsonInstance.stripMargin)) ~> addAuthorization("Component") ~> server.routes ~> check { + Post("/instances/register", HttpEntity(ContentTypes.`application/json`, invalidJsonInstance.stripMargin)) ~> addAuthorization("Component") ~> server.routes ~> check { assert(status === StatusCodes.BAD_REQUEST) responseAs[String].toLowerCase should include("failed to parse json") } //Missing required JSON members - Post("/register", HttpEntity(ContentTypes.`application/json`, validJsonInstanceMissingRequiredMember.stripMargin)) ~> addAuthorization("Component") ~> server.routes ~> check { + Post("/instances/register", HttpEntity(ContentTypes.`application/json`, validJsonInstanceMissingRequiredMember.stripMargin)) ~> addAuthorization("Component") ~> server.routes ~> check { assert(status === StatusCodes.BAD_REQUEST) responseAs[String].toLowerCase should include("could not deserialize parameter instance") } //Invalid HTTP method - Get("/register?InstanceString=25") ~> addAuthorization("Component") ~> Route.seal(server.routes) ~> check { + Get("/instances/register?InstanceString=25") ~> addAuthorization("Component") ~> Route.seal(server.routes) ~> check { assert(status === StatusCodes.METHOD_NOT_ALLOWED) responseAs[String] shouldEqual "HTTP method not allowed, supported methods: POST" } //Wrong user type - Post("/register?InstanceString=25") ~> addAuthorization("User") ~> Route.seal(server.routes) ~> check { + Post("/instances/register?InstanceString=25") ~> addAuthorization("User") ~> Route.seal(server.routes) ~> check { assert(status === StatusCodes.UNAUTHORIZED) responseAs[String] shouldEqual "The supplied authentication is invalid" } //No authorization - Post("/register?InstanceString=25") ~> Route.seal(server.routes) ~> check { + Post("/instances/register?InstanceString=25") ~> Route.seal(server.routes) ~> check { assert(status === StatusCodes.UNAUTHORIZED) - responseAs[String].toLowerCase should include ("not supplied with the request") + responseAs[String].toLowerCase should include("not supplied with the request") } } //Invalid deregister "not deregister if method is invalid, id is missing or invalid" in { - //Id missing - Post("/deregister") ~> addAuthorization("Component") ~> Route.seal(server.routes) ~> check { - assert(status === StatusCodes.NOT_FOUND) - responseAs[String].toLowerCase should include("missing required query parameter") - } - - //Id wrong type - Post("/deregister?Id=kilo") ~> addAuthorization("Component") ~> Route.seal(server.routes) ~> check { - assert(status === StatusCodes.BAD_REQUEST) - responseAs[String].toLowerCase should include("not a valid 64-bit signed integer value") - } //Id not present - Post(s"/deregister?Id=${Long.MaxValue}") ~> addAuthorization("Component") ~> Route.seal(server.routes) ~> check { + Post(s"/instances/${Long.MaxValue}/deregister") ~> addAuthorization("Component") ~> Route.seal(server.routes) ~> check { assert(status === StatusCodes.NOT_FOUND) responseAs[String].toLowerCase should include("not known to the server") } //Wrong HTTP method - Get("/deregister?Id=0") ~> addAuthorization("Component") ~> Route.seal(server.routes) ~> check { + Get("/instances/0/deregister") ~> addAuthorization("Component") ~> Route.seal(server.routes) ~> check { assert(status === StatusCodes.METHOD_NOT_ALLOWED) responseAs[String] shouldEqual "HTTP method not allowed, supported methods: POST" } //Wrong user type - Post("/deregister?Id=0") ~> addAuthorization("User") ~> Route.seal(server.routes) ~> check { + Post("/instances/0/deregister") ~> addAuthorization("User") ~> Route.seal(server.routes) ~> check { assert(status === StatusCodes.UNAUTHORIZED) responseAs[String] shouldEqual "The supplied authentication is invalid" } //No authorization - Post("/deregister?Id=0") ~> Route.seal(server.routes) ~> check { + Post("/instances/0/deregister") ~> Route.seal(server.routes) ~> check { assert(status === StatusCodes.UNAUTHORIZED) - responseAs[String].toLowerCase should include ("not supplied with the request") + responseAs[String].toLowerCase should include("not supplied with the request") } } @@ -179,8 +166,20 @@ class ServerTest case Failure(ex) => fail(ex) } + } + //Valid get instances with no parameter + Get("/instances") ~> addAuthorization("User") ~> server.routes ~> check { + assert(status === StatusCodes.OK) + Try(responseAs[String].parseJson.convertTo[List[Instance]](listFormat(instanceFormat))) match { + case Success(listOfESInstances) => + listOfESInstances.size shouldEqual 1 + listOfESInstances.exists(instance => instance.name.equals("Default ElasticSearch Instance")) shouldBe true + case Failure(ex) => + fail(ex) + } } + //No instances of that type present, still need to be 200 OK Get("/instances?ComponentType=WebApp") ~> addAuthorization("User") ~> server.routes ~> check { assert(status === StatusCodes.OK) @@ -200,6 +199,11 @@ class ServerTest assert(status === StatusCodes.BAD_REQUEST) responseAs[String].toLowerCase should include("could not deserialize parameter") } + //Missing parameter value + Get("/instances?ComponentType=") ~> addAuthorization("User") ~> server.routes ~> check { + assert(status === StatusCodes.BAD_REQUEST) + responseAs[String].toLowerCase should include("could not deserialize parameter") + } //Wrong user type Get("/instances?ComponentType=Crawler") ~> addAuthorization("Component") ~> Route.seal(server.routes) ~> check { @@ -210,13 +214,13 @@ class ServerTest //No authorization Get("/instances?ComponentType=Crawler") ~> Route.seal(server.routes) ~> check { assert(status === StatusCodes.UNAUTHORIZED) - responseAs[String].toLowerCase should include ("not supplied with the request") + responseAs[String].toLowerCase should include("not supplied with the request") } } //Valid get number of instances "successfully retrieve number of instances if parameter is valid" in { - Get("/numberOfInstances?ComponentType=ElasticSearch") ~> addAuthorization("User") ~> server.routes ~> check { + Get("/instances/count?ComponentType=ElasticSearch") ~> addAuthorization("User") ~> server.routes ~> check { assert(status === StatusCodes.OK) Try(responseAs[String].toLong) match { case Success(numberOfEsInstances) => @@ -227,7 +231,7 @@ class ServerTest } //No instances of that type present, still need to be 200 OK - Get("/numberOfInstances?ComponentType=WebApp") ~> addAuthorization("User") ~>server.routes ~> check { + Get("/instances/count?ComponentType=WebApp") ~> addAuthorization("User") ~> server.routes ~> check { assert(status === StatusCodes.OK) Try(responseAs[String].toLong) match { case Success(numberOfEsInstances) => @@ -236,43 +240,59 @@ class ServerTest fail(ex) } } + + //Return all the instances if ComponentType not provided + Get("/instances/count") ~> addAuthorization("User") ~> server.routes ~> check { + assert(status === StatusCodes.OK) + Try(responseAs[String].toLong) match { + case Success(numberOfEsInstances) => + numberOfEsInstances shouldEqual 1 + case Failure(ex) => + fail(ex) + } + } } //Invalid get number of instances "not retrieve number of instances if method is invalid, ComponentType is missing or invalid" in { //Wrong HTTP method - Post("/numberOfInstances?ComponentType=Crawler") ~> addAuthorization("User") ~> Route.seal(server.routes) ~> check { + Post("/instances/count?ComponentType=Crawler") ~> addAuthorization("User") ~> Route.seal(server.routes) ~> check { assert(status === StatusCodes.METHOD_NOT_ALLOWED) responseAs[String] shouldEqual "HTTP method not allowed, supported methods: GET" } //Wrong parameter value - Get("/numberOfInstances?ComponentType=Car") ~> addAuthorization("User") ~> server.routes ~> check { + Get("/instances/count?ComponentType=Car") ~> addAuthorization("User") ~> server.routes ~> check { assert(status === StatusCodes.BAD_REQUEST) responseAs[String].toLowerCase should include("could not deserialize parameter") } + //Missing Parameter value + Get("/instances/count?ComponentType=") ~> addAuthorization("User") ~> Route.seal(server.routes) ~> check { + assert(status === StatusCodes.BAD_REQUEST) + responseAs[String].toLowerCase should include("could not deserialize parameter string") + } //Wrong user type - Get("/numberOfInstances?ComponentType=Crawler") ~> addAuthorization("Component") ~> Route.seal(server.routes) ~> check { + Get("/instances/count?ComponentType=Crawler") ~> addAuthorization("Component") ~> Route.seal(server.routes) ~> check { assert(status === StatusCodes.UNAUTHORIZED) responseAs[String] shouldEqual "The supplied authentication is invalid" } //No authorization - Get("/numberOfInstances?ComponentType=Crawler") ~> Route.seal(server.routes) ~> check { + Get("/instances/count?ComponentType=Crawler") ~> Route.seal(server.routes) ~> check { assert(status === StatusCodes.UNAUTHORIZED) - responseAs[String].toLowerCase should include ("not supplied with the request") + responseAs[String].toLowerCase should include("not supplied with the request") } } //Valid GET /instance "return an instance if id is valid and instance is present" in { - Get("/instance?Id=0") ~> addAuthorization("User") ~> server.routes ~> check { + Get("/instances/0") ~> addAuthorization("User") ~> server.routes ~> check { assert(status === StatusCodes.OK) Try(responseAs[String].parseJson.convertTo[Instance](instanceFormat)) match { case Success(instance) => instance.id.get shouldEqual 0 - instance.name should include ("Default ElasticSearch") + instance.name should include("Default ElasticSearch") case Failure(ex) => fail(ex) } @@ -281,21 +301,21 @@ class ServerTest //Invalid GET /instance "return 404 if instance id is not known" in { - Get("/instance?Id=45") ~> addAuthorization("User") ~> server.routes ~> check { + Get("/instances/45") ~> addAuthorization("User") ~> server.routes ~> check { assert(status === StatusCodes.NOT_FOUND) responseAs[String] shouldEqual "Id 45 was not found on the server." } //Wrong user type - Get("/instance?Id=0") ~> addAuthorization("Component") ~> Route.seal(server.routes) ~> check { + Get("/instances/0") ~> addAuthorization("Component") ~> Route.seal(server.routes) ~> check { assert(status === StatusCodes.UNAUTHORIZED) responseAs[String] shouldEqual "The supplied authentication is invalid" } //No authorization - Get("/instance?Id=0") ~> Route.seal(server.routes) ~> check { + Get("/instances/0") ~> Route.seal(server.routes) ~> check { assert(status === StatusCodes.UNAUTHORIZED) - responseAs[String].toLowerCase should include ("not supplied with the request") + responseAs[String].toLowerCase should include("not supplied with the request") } } @@ -306,7 +326,7 @@ class ServerTest val id = assertValidRegister(ComponentType.Crawler, dockerId = None) //Actual test - Get(s"/matchingInstance?Id=$id&ComponentType=ElasticSearch") ~> addAuthorization("Component") ~> server.routes ~> check { + Get(s"/instances/$id/matchingInstance?ComponentType=ElasticSearch") ~> addAuthorization("Component") ~> server.routes ~> check { assert(status === StatusCodes.OK) Try(responseAs[String].parseJson.convertTo[Instance](instanceFormat)) match { case Success(esInstance) => @@ -329,45 +349,45 @@ class ServerTest val webAppId = assertValidRegister(ComponentType.WebApp) //Invalid ComponentType - Get(s"/matchingInstance?Id=$webApiId&ComponentType=Search") ~> addAuthorization("Component") ~> Route.seal(server.routes) ~> check { + Get(s"/instances/$webApiId/matchingInstance?ComponentType=Search") ~> addAuthorization("Component") ~> Route.seal(server.routes) ~> check { assert(status === StatusCodes.BAD_REQUEST) } //Unknown callee id, expect 404 - Get("/matchingInstance?Id=45&ComponentType=Crawler") ~> addAuthorization("Component") ~> Route.seal(server.routes) ~> check { + Get("/instances/45/matchingInstance?ComponentType=Crawler") ~> addAuthorization("Component") ~> Route.seal(server.routes) ~> check { assert(status === StatusCodes.NOT_FOUND) - responseAs[String].toLowerCase should include ("id 45 was not found") + responseAs[String].toLowerCase should include("id 45 was not found") } //Method Not allowed - Post(s"/matchingInstance?Id=$webApiId&ComponentType=ElasticSearch") ~> addAuthorization("Component") ~> Route.seal(server.routes) ~> check { + Post(s"/instances/$webApiId/matchingInstance?ComponentType=ElasticSearch") ~> addAuthorization("Component") ~> Route.seal(server.routes) ~> check { assert(status === StatusCodes.METHOD_NOT_ALLOWED) responseAs[String] shouldEqual "HTTP method not allowed, supported methods: GET" } //Incompatible types, api asks for crawler - expect 400 - Get(s"/matchingInstance?Id=$webApiId&ComponentType=Crawler") ~> addAuthorization("Component") ~> Route.seal(server.routes) ~> check { + Get(s"/instances/$webApiId/matchingInstance?ComponentType=Crawler") ~> addAuthorization("Component") ~> Route.seal(server.routes) ~> check { assert(status === StatusCodes.BAD_REQUEST) - responseAs[String].toLowerCase should include ("invalid dependency type") + responseAs[String].toLowerCase should include("invalid dependency type") } //No instance of desired type present - expect 404 assertValidDeregister(webApiId) - Get(s"/matchingInstance?Id=$webAppId&ComponentType=WebApi") ~> addAuthorization("Component") ~> Route.seal(server.routes) ~> check { + Get(s"/instances/$webAppId/matchingInstance?ComponentType=WebApi") ~> addAuthorization("Component") ~> Route.seal(server.routes) ~> check { assert(status === StatusCodes.NOT_FOUND) - responseAs[String].toLowerCase should include ("could not find matching instance") + responseAs[String].toLowerCase should include("could not find matching instance") } //Wrong user type - Get(s"/matchingInstance?Id=$webAppId&ComponentType=WebApi") ~> addAuthorization("User") ~> Route.seal(server.routes) ~> check { + Get(s"/instances/$webApiId/matchingInstance?ComponentType=WebApi") ~> addAuthorization("User") ~> Route.seal(server.routes) ~> check { assert(status === StatusCodes.UNAUTHORIZED) responseAs[String] shouldEqual "The supplied authentication is invalid" } //No authorization - Get(s"/matchingInstance?Id=$webAppId&ComponentType=WebApi") ~> Route.seal(server.routes) ~> check { + Get(s"/instances/$webApiId/matchingInstance?ComponentType=WebApi") ~> Route.seal(server.routes) ~> check { assert(status === StatusCodes.UNAUTHORIZED) - responseAs[String].toLowerCase should include ("not supplied with the request") + responseAs[String].toLowerCase should include("not supplied with the request") } assertValidDeregister(webAppId) @@ -382,44 +402,41 @@ class ServerTest //Add a WebApi instance for testing val id2 = assertValidRegister(ComponentType.WebApi) - Post(s"/matchingResult?CallerId=$id1&MatchedInstanceId=$id2&MatchingSuccessful=1") ~> addAuthorization("Component") ~> Route.seal(server.routes) ~> check { + + Post(s"/instances/$id1/matchingResult", HttpEntity(ContentTypes.`application/json`, s"""{ "MatchingSuccessful": true, "SenderId" : $id2}""")) ~> addAuthorization("Component") ~> Route.seal(server.routes) ~> check { assert(status === StatusCodes.OK) responseAs[String] shouldEqual "Matching result true processed." } - //Remove Instances assertValidDeregister(id1) assertValidDeregister(id2) } + //Invalid POST /matchingResult "not process matching result if method or parameters are invalid" in { //Wrong method - Get("/matchingResult?CallerId=0&MatchedInstanceId=0&MatchingSuccessful=1") ~> addAuthorization("Component") ~> Route.seal(server.routes) ~> check { + + Get(s"/instances/0/matchingResult", HttpEntity(ContentTypes.`application/json`, s"""{ "MatchingSuccessful": true, "SenderId" : 0}""")) ~> addAuthorization("Component") ~> Route.seal(server.routes) ~> check { assert(status === StatusCodes.METHOD_NOT_ALLOWED) responseAs[String] shouldEqual "HTTP method not allowed, supported methods: POST" } //Invalid IDs - expect 404 - Post("/matchingResult?CallerId=1&MatchedInstanceId=2&MatchingSuccessful=0") ~> addAuthorization("Component") ~> server.routes ~> check { + Post(s"/instances/1/matchingResult", HttpEntity(ContentTypes.`application/json`, s"""{ "MatchingSuccessful": false, "SenderId" : 2}""")) ~> addAuthorization("Component") ~> Route.seal(server.routes) ~> check { assert(status === StatusCodes.NOT_FOUND) } - //Wrong parameters, caller is same as callee - expect bad request - Post("/matchingResult?CallerId=0&MatchedInstanceId=0&MatchingSuccessful=O") ~> addAuthorization("Component") ~> Route.seal(server.routes) ~> check { - assert(status === StatusCodes.BAD_REQUEST) - } - //Wrong user type - Post("/matchingResult?CallerId=1&MatchedInstanceId=2&MatchingSuccessful=0") ~> addAuthorization("User") ~> Route.seal(server.routes) ~> check { + Post(s"/instances/1/matchingResult", HttpEntity(ContentTypes.`application/json`, s"""{ "MatchingSuccessful": false, "SenderId" : 2}""")) ~> addAuthorization("User") ~> Route.seal(server.routes) ~> check { assert(status === StatusCodes.UNAUTHORIZED) responseAs[String] shouldEqual "The supplied authentication is invalid" } //No authorization - Post("/matchingResult?CallerId=1&MatchedInstanceId=2&MatchingSuccessful=0") ~> Route.seal(server.routes) ~> check { + Post(s"/instances/1/matchingResult", HttpEntity(ContentTypes.`application/json`, s"""{ "MatchingSuccessful": false, "SenderId" : 2}""")) ~> Route.seal(server.routes) ~> check { assert(status === StatusCodes.UNAUTHORIZED) - responseAs[String].toLowerCase should include ("not supplied with the request") + responseAs[String].toLowerCase should include("not supplied with the request") } } @@ -427,7 +444,7 @@ class ServerTest "returns registry events that are associated to the instance if id is valid" in { val id = assertValidRegister(ComponentType.Crawler) //TestCase - Get(s"/eventList?Id=$id") ~> addAuthorization("User") ~> server.routes ~> check { + Get(s"/instances/$id/eventList") ~> addAuthorization("User") ~> server.routes ~> check { assert(status === StatusCodes.OK) Try(responseAs[String].parseJson.convertTo[List[RegistryEvent]](listFormat(eventFormat))) match { case Success(listOfEvents) => @@ -444,33 +461,33 @@ class ServerTest //Invalid GET /eventList "does not return events if method is invalid or id is not found" in { //Wrong Http method - Post("/eventList?Id=0") ~> addAuthorization("User") ~> Route.seal(server.routes) ~> check { + Post("/instances/0/eventList") ~> addAuthorization("User") ~> Route.seal(server.routes) ~> check { assert(status === StatusCodes.METHOD_NOT_ALLOWED) responseAs[String] shouldEqual "HTTP method not allowed, supported methods: GET" } //Wrong ID - Get("/eventList?Id=45") ~> addAuthorization("User") ~> server.routes ~> check { + Get("/instances/45/eventList") ~> addAuthorization("User") ~> server.routes ~> check { assert(status === StatusCodes.NOT_FOUND) responseAs[String] shouldEqual "Id 45 not found." } //Wrong user type - Get("/eventList?Id=0") ~> addAuthorization("Component") ~> Route.seal(server.routes) ~> check { + Get("/instances/0/eventList") ~> addAuthorization("Component") ~> Route.seal(server.routes) ~> check { assert(status === StatusCodes.UNAUTHORIZED) responseAs[String] shouldEqual "The supplied authentication is invalid" } //No authorization - Get("/eventList?Id=0") ~> Route.seal(server.routes) ~> check { + Get("/instances/0/eventList") ~> Route.seal(server.routes) ~> check { assert(status === StatusCodes.UNAUTHORIZED) - responseAs[String].toLowerCase should include ("not supplied with the request") + responseAs[String].toLowerCase should include("not supplied with the request") } } //Valid GET /network "get the whole network graph of the current registry" in { - Get("/network") ~> addAuthorization("User") ~> server.routes ~> check { + Get("/instances/network") ~> addAuthorization("User") ~> server.routes ~> check { assert(status === StatusCodes.OK) Try(responseAs[String].parseJson.convertTo[List[Instance]](listFormat(instanceFormat))) match { case Success(listOfInstances) => @@ -488,7 +505,7 @@ class ServerTest val id = assertValidRegister(ComponentType.Crawler) //Fake connection from crawler to default ES instance - Get(s"/matchingInstance?Id=$id&ComponentType=ElasticSearch") ~> addAuthorization("Component") ~> server.routes ~> check { + Get(s"/instances/$id/matchingInstance?ComponentType=ElasticSearch") ~> addAuthorization("Component") ~> server.routes ~> check { assert(status === StatusCodes.OK) Try(responseAs[String].parseJson.convertTo[Instance](instanceFormat)) match { case Success(esInstance) => @@ -500,7 +517,7 @@ class ServerTest } //Get links from crawler, should be one link to default ES instance - Get(s"/linksFrom?Id=$id") ~> addAuthorization("User") ~> server.routes ~> check { + Get(s"/instances/$id/linksFrom") ~> addAuthorization("User") ~> server.routes ~> check { assert(status === StatusCodes.OK) Try(responseAs[String].parseJson.convertTo[List[InstanceLink]](listFormat(instanceLinkFormat))) match { case Success(listOfLinks) => @@ -520,20 +537,20 @@ class ServerTest //Invalid GET /linksFrom "return no links found for invalid id" in { - Get("/linksFrom?Id=45") ~> addAuthorization("User") ~> server.routes ~> check { + Get("/instances/45/linksFrom") ~> addAuthorization("User") ~> server.routes ~> check { assert(status === StatusCodes.NOT_FOUND) } //Wrong user type - Get("/linksFrom?Id=0") ~> addAuthorization("Component") ~> Route.seal(server.routes) ~> check { + Get("/instances/0/linksFrom") ~> addAuthorization("Component") ~> Route.seal(server.routes) ~> check { assert(status === StatusCodes.UNAUTHORIZED) responseAs[String] shouldEqual "The supplied authentication is invalid" } //No authorization - Get("/linksFrom?Id=0") ~> Route.seal(server.routes) ~> check { + Get("/instances/0/linksFrom") ~> Route.seal(server.routes) ~> check { assert(status === StatusCodes.UNAUTHORIZED) - responseAs[String].toLowerCase should include ("not supplied with the request") + responseAs[String].toLowerCase should include("not supplied with the request") } } @@ -542,7 +559,7 @@ class ServerTest val id = assertValidRegister(ComponentType.Crawler) //Fake connection from crawler to default ES instance - Get(s"/matchingInstance?Id=$id&ComponentType=ElasticSearch") ~> addAuthorization("Component") ~> server.routes ~> check { + Get(s"/instances/$id/matchingInstance?ComponentType=ElasticSearch") ~> addAuthorization("Component") ~> server.routes ~> check { assert(status === StatusCodes.OK) Try(responseAs[String].parseJson.convertTo[Instance](instanceFormat)) match { case Success(esInstance) => @@ -554,7 +571,7 @@ class ServerTest } //Get links to default ES instance, should be one link from crawler - Get(s"/linksTo?Id=0") ~> addAuthorization("User") ~> server.routes ~> check { + Get(s"/instances/0/linksTo") ~> addAuthorization("User") ~> server.routes ~> check { assert(status === StatusCodes.OK) Try(responseAs[String].parseJson.convertTo[List[InstanceLink]](listFormat(instanceLinkFormat))) match { case Success(listOfLinks) => @@ -574,144 +591,143 @@ class ServerTest //Invalid GET /linksTo "return no links found to specified id" in { - Get("/linksTo?Id=45") ~> addAuthorization("User") ~> server.routes ~> check { + Get("/instances/45/linksTo") ~> addAuthorization("User") ~> server.routes ~> check { assert(status === StatusCodes.NOT_FOUND) } //Wrong user type - Get("/linksTo?Id=0") ~> addAuthorization("Component") ~> Route.seal(server.routes) ~> check { + Get("/instances/0/linksTo") ~> addAuthorization("Component") ~> Route.seal(server.routes) ~> check { assert(status === StatusCodes.UNAUTHORIZED) responseAs[String] shouldEqual "The supplied authentication is invalid" } //No authorization - Get("/linksTo?Id=0") ~> Route.seal(server.routes) ~> check { + Get("/instances/0/linksTo") ~> Route.seal(server.routes) ~> check { assert(status === StatusCodes.UNAUTHORIZED) - responseAs[String].toLowerCase should include ("not supplied with the request") + responseAs[String].toLowerCase should include("not supplied with the request") } } - //Valid POST /addLabel + //Valid POST /instances/{id}/label "add a generic label to an instance is label and id are valid" in { - Post("/addLabel?Id=0&Label=ElasticSearchDefaultLabel") ~> addAuthorization("Admin") ~> server.routes ~> check { + Post("/instances/0/label", HttpEntity(ContentTypes.`application/json`, """{ "Label": "Private"}""")) ~> addAuthorization("Admin") ~> server.routes ~> check { assert(status === StatusCodes.OK) responseAs[String] shouldEqual "Successfully added label" } } - - //Invalid POST /addLabel - "fail to add label if id is invalid or label too long" in{ + //Invalid POST /instances/{id}/label + "fail to add label if id is invalid or label too long" in { //Unknown id - expect 404 - Post("/addLabel?Id=45&Label=Private") ~> addAuthorization("Admin") ~> server.routes ~> check { + Post("/instances/45/label", HttpEntity(ContentTypes.`application/json`, """{ "Label": "Private"}""")) ~> addAuthorization("Admin") ~> server.routes ~> check { assert(status === StatusCodes.NOT_FOUND) responseAs[String] shouldEqual "Cannot add label, id 45 not found." } - - val tooLongLabel = "VeryVeryExtraLongLabelThatDoesNotWorkWhileAddingLabel" //Label out of bounds - expect 400 - Post(s"/addLabel?Id=0&Label=$tooLongLabel") ~> addAuthorization("Admin") ~> server.routes ~> check { + val tooLongLabel = "VeryVeryExtraLongLabelThatDoesNotWorkWhileAddingLabel" + val jsonStr = tooLongLabel.toJson + Post("/instances/0/label", HttpEntity(ContentTypes.`application/json`, s"""{ "Label": $jsonStr}""")) ~> addAuthorization("Admin") ~> server.routes ~> check { assert(status === StatusCodes.BAD_REQUEST) - responseAs[String].toLowerCase should include ("exceeds character limit") + responseAs[String].toLowerCase should include("exceeds character limit") } - //Wrong user type - Post("/addLabel?Id=0&Label=Private") ~> addAuthorization("Component") ~> Route.seal(server.routes) ~> check { + Post("/instances/0/label", HttpEntity(ContentTypes.`application/json`, """{ "Label": "Private"}""")) ~> addAuthorization("Component") ~> Route.seal(server.routes) ~> check { assert(status === StatusCodes.UNAUTHORIZED) responseAs[String] shouldEqual "The supplied authentication is invalid" } //Wrong user type - Post("/addLabel?Id=0&Label=Private") ~> addAuthorization("User") ~> Route.seal(server.routes) ~> check { + Post("/instances/0/label", HttpEntity(ContentTypes.`application/json`, """{ "Label": "Private"}""")) ~> addAuthorization("User") ~> Route.seal(server.routes) ~> check { assert(status === StatusCodes.UNAUTHORIZED) responseAs[String] shouldEqual "The supplied authentication is invalid" } - //No authorization - Post("/addLabel?Id=0&Label=Private") ~> Route.seal(server.routes) ~> check { + Post("/instances/0/label", HttpEntity(ContentTypes.`application/json`, """{ "Label": "Private"}""")) ~> Route.seal(server.routes) ~> check { assert(status === StatusCodes.UNAUTHORIZED) - responseAs[String].toLowerCase should include ("not supplied with the request") + responseAs[String].toLowerCase should include("not supplied with the request") } + } - /**Minimal tests for docker operations**/ + /** Minimal tests for docker operations **/ "fail to deploy if component type is invalid" in { - Post("/deploy?ComponentType=Car") ~> addAuthorization("Admin") ~> server.routes ~> check { + Post("/instances/deploy", HttpEntity(ContentTypes.`application/json`, """{"ComponentType": "Car"}""")) ~> addAuthorization("Admin") ~> server.routes ~> check { status shouldEqual StatusCodes.BAD_REQUEST - responseAs[String].toLowerCase should include ("could not deserialize") + responseAs[String].toLowerCase should include("could not deserialize") } //Wrong user type - Post("/deploy?ComponentType=Crawler") ~> addAuthorization("User") ~> server.routes ~> check { + Post("/instances/deploy", HttpEntity(ContentTypes.`application/json`, """{"ComponentType": "Crawler"}""")) ~> addAuthorization("User") ~> server.routes ~> check { rejection.isInstanceOf[AuthenticationFailedRejection] shouldBe true } //No authorization - Post("/deploy?ComponentType=Crawler") ~> server.routes ~> check { + Post("/instances/deploy", HttpEntity(ContentTypes.`application/json`, """{"ComponentType": "Crawler"}""")) ~> server.routes ~> check { rejection.isInstanceOf[AuthenticationFailedRejection] shouldBe true } } "fail to execute docker operations if id is invalid" in { - Post("/reportStart?Id=42") ~> addAuthorization("Component") ~> server.routes ~> check { + Post("/instances/42/reportStart") ~> addAuthorization("Component") ~> server.routes ~> check { status shouldEqual StatusCodes.NOT_FOUND - responseAs[String].toLowerCase should include ("not found") + responseAs[String].toLowerCase should include("not found") } - Post("/reportStop?Id=42") ~> addAuthorization("Component") ~> server.routes ~> check { + Post("/instances/42/reportStop") ~> addAuthorization("Component") ~> server.routes ~> check { status shouldEqual StatusCodes.NOT_FOUND - responseAs[String].toLowerCase should include ("not found") + responseAs[String].toLowerCase should include("not found") } - Post("/reportFailure?Id=42") ~> addAuthorization("Component") ~> server.routes ~> check { + Post("/instances/42/reportFailure") ~> addAuthorization("Component") ~> server.routes ~> check { status shouldEqual StatusCodes.NOT_FOUND - responseAs[String].toLowerCase should include ("not found") + responseAs[String].toLowerCase should include("not found") } - Post("/pause?Id=42") ~> addAuthorization("Admin") ~> server.routes ~> check { + Post("/instances/42/pause") ~> addAuthorization("Admin") ~> server.routes ~> check { status shouldEqual StatusCodes.NOT_FOUND - responseAs[String].toLowerCase should include ("not found") + responseAs[String].toLowerCase should include("not found") } - Post("/resume?Id=42") ~> addAuthorization("Admin") ~> server.routes ~> check { + Post("/instances/42/resume") ~> addAuthorization("Admin") ~> server.routes ~> check { status shouldEqual StatusCodes.NOT_FOUND - responseAs[String].toLowerCase should include ("not found") + responseAs[String].toLowerCase should include("not found") } - Post("/stop?Id=42") ~> addAuthorization("Admin") ~> server.routes ~> check { + Post("/instances/42/stop") ~> addAuthorization("Admin") ~> server.routes ~> check { status shouldEqual StatusCodes.NOT_FOUND - responseAs[String].toLowerCase should include ("not found") + responseAs[String].toLowerCase should include("not found") } - Post("/start?Id=42") ~> addAuthorization("Admin") ~> server.routes ~> check { + Post("/instances/42/start") ~> addAuthorization("Admin") ~> server.routes ~> check { status shouldEqual StatusCodes.NOT_FOUND - responseAs[String].toLowerCase should include ("not found") + responseAs[String].toLowerCase should include("not found") } - Post("/delete?Id=42") ~> addAuthorization("Admin") ~> server.routes ~> check { + Post("/instances/42/delete") ~> addAuthorization("Admin") ~> server.routes ~> check { status shouldEqual StatusCodes.NOT_FOUND - responseAs[String].toLowerCase should include ("not found") + responseAs[String].toLowerCase should include("not found") } - Post("/assignInstance?Id=42&AssignedInstanceId=43") ~> addAuthorization("Admin") ~> server.routes ~> check { + + Post("/instances/42/assignInstance", HttpEntity(ContentTypes.`application/json`, """{ "AssignedInstanceId": 43}""")) ~> addAuthorization("Admin") ~> server.routes ~> check { status shouldEqual StatusCodes.NOT_FOUND - responseAs[String].toLowerCase should include ("not found") + responseAs[String].toLowerCase should include("not found") } } "fail to execute docker operations if instance is no docker container" in { val id = assertValidRegister(ComponentType.Crawler, dockerId = None) - Post(s"/reportStart?Id=$id") ~> addAuthorization("Component") ~> server.routes ~> check { + Post(s"/instances/$id/reportStart") ~> addAuthorization("Component") ~> server.routes ~> check { status shouldEqual StatusCodes.BAD_REQUEST } - Post(s"/reportStop?Id=$id") ~> addAuthorization("Component") ~> server.routes ~> check { + Post(s"/instances/$id/reportStop") ~> addAuthorization("Component") ~> server.routes ~> check { status shouldEqual StatusCodes.BAD_REQUEST } - Post(s"/reportFailure?Id=$id") ~> addAuthorization("Component") ~> server.routes ~> check { + Post(s"/instances/$id/reportFailure") ~> addAuthorization("Component") ~> server.routes ~> check { status shouldEqual StatusCodes.BAD_REQUEST } - Post(s"/pause?Id=$id") ~> addAuthorization("Admin") ~> server.routes ~> check { + Post(s"/instances/$id/pause") ~> addAuthorization("Admin") ~> server.routes ~> check { status shouldEqual StatusCodes.BAD_REQUEST } - Post(s"/resume?Id=$id") ~> addAuthorization("Admin") ~> server.routes ~> check { + Post(s"/instances/$id/resume") ~> addAuthorization("Admin") ~> server.routes ~> check { status shouldEqual StatusCodes.BAD_REQUEST } - Post(s"/start?Id=$id") ~> addAuthorization("Admin") ~> server.routes ~> check { + Post(s"/instances/$id/start") ~> addAuthorization("Admin") ~> server.routes ~> check { status shouldEqual StatusCodes.BAD_REQUEST } - Post(s"/delete?Id=$id") ~> addAuthorization("Admin") ~> server.routes ~> check { + Post(s"/instances/$id/delete") ~> addAuthorization("Admin") ~> server.routes ~> check { status shouldEqual StatusCodes.BAD_REQUEST } assertValidDeregister(id) @@ -719,25 +735,28 @@ class ServerTest "fail to execute docker operations with wrong authorization supplied" in { val id = assertValidRegister(ComponentType.Crawler, dockerId = None) - Post(s"/reportStart?Id=$id") ~> server.routes ~> check { + Post(s"/instances/$id/reportStart") ~> server.routes ~> check { rejection.isInstanceOf[AuthenticationFailedRejection] shouldBe true } - Post(s"/reportStop?Id=$id") ~> server.routes ~> check { + Post(s"/instances/$id/reportStop") ~> server.routes ~> check { rejection.isInstanceOf[AuthenticationFailedRejection] shouldBe true } - Post(s"/reportFailure?Id=$id") ~> server.routes ~> check { + Post(s"/instances/$id/reportFailure") ~> server.routes ~> check { rejection.isInstanceOf[AuthenticationFailedRejection] shouldBe true } - Post(s"/pause?Id=$id") ~> addAuthorization("User") ~> server.routes ~> check { + Post(s"/instances/$id/pause") ~> addAuthorization("User") ~> server.routes ~> check { rejection.isInstanceOf[AuthenticationFailedRejection] shouldBe true } - Post(s"/resume?Id=$id") ~> addAuthorization("User") ~> server.routes ~> check { + Post(s"/instances/$id/resume") ~> addAuthorization("User") ~> server.routes ~> check { rejection.isInstanceOf[AuthenticationFailedRejection] shouldBe true } - Post(s"/start?Id=$id") ~> addAuthorization("User") ~> server.routes ~> check { + Post(s"/instances/$id/stop") ~> addAuthorization("User") ~> server.routes ~> check { rejection.isInstanceOf[AuthenticationFailedRejection] shouldBe true } - Post(s"/delete?Id=$id") ~> server.routes ~> check { + Post(s"/instances/$id/start") ~> addAuthorization("User") ~> server.routes ~> check { + rejection.isInstanceOf[AuthenticationFailedRejection] shouldBe true + } + Post(s"/instances/$id/delete") ~> server.routes ~> check { rejection.isInstanceOf[AuthenticationFailedRejection] shouldBe true } assertValidDeregister(id) @@ -745,27 +764,28 @@ class ServerTest "Requests" should { "throttle when limit reached" in { - for(i <- 1 to configuration.maxIndividualIpReq){ - Get(s"/linksTo?Id=0")~> server.routes ~> check {} + for (i <- 1 to configuration.maxIndividualIpReq) { + Get(s"/instances/0/linksTo") ~> server.routes ~> check {} } - Get(s"/linksTo?Id=0") ~> server.routes ~> check { + Get(s"/instances/0/linksTo") ~> server.routes ~> check { status shouldEqual StatusCodes.BAD_REQUEST - responseAs[String].toLowerCase should include ("request limit exceeded") + responseAs[String].toLowerCase should include("request limit exceeded") } } } } + private def assertValidRegister(compType: ComponentType, dockerId: Option[String] = Some("randomId"), - labels: List[String] = List("some_label")) : Long = { + labels: List[String] = List("some_label")): Long = { val instanceString = Instance(id = None, host = "http://localhost", portNumber = 4242, name = "ValidInstance", componentType = compType, dockerId = dockerId, instanceState = InstanceState.Running, labels = labels, linksTo = List.empty, linksFrom = List.empty) .toJson(instanceFormat).toString - Post("/register", HttpEntity(ContentTypes.`application/json`, + Post("/instances/register", HttpEntity(ContentTypes.`application/json`, instanceString.stripMargin)) ~> addAuthorization("Component") ~> server.routes ~> check { assert(status === StatusCodes.OK) responseEntity match { @@ -780,13 +800,13 @@ class ServerTest } private def assertValidDeregister(id: Long): Unit = { - Post(s"/deregister?Id=$id") ~> addAuthorization("Component") ~> server.routes ~> check { + Post(s"/instances/$id/deregister") ~> addAuthorization("Component") ~> server.routes ~> check { assert(status === StatusCodes.OK) entityAs[String].toLowerCase should include("successfully removed instance") } } - private def generateValidTestToken(userType: String) : String = { + private def generateValidTestToken(userType: String): String = { val claim = JwtClaim() .issuedNow .expiresIn(5) @@ -797,6 +817,6 @@ class ServerTest Jwt.encode(claim, configuration.jwtSecretKey, JwtAlgorithm.HS256) } - private def addAuthorization(userType: String) : HttpRequest => HttpRequest = addHeader(Authorization.oauth2(generateValidTestToken(userType))) + private def addAuthorization(userType: String): HttpRequest => HttpRequest = addHeader(Authorization.oauth2(generateValidTestToken(userType))) }