From abac46fe742484c6e0b90bebe3c86d88231540b2 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Mon, 7 Mar 2022 17:56:48 +0100 Subject: feat(web/client): Add separate web client implementation This change implements a simple client for the OpenDC REST API into a separate module. This allows other users to use this module as well. --- opendc-web/opendc-web-client/build.gradle.kts | 37 ++++++ .../kotlin/org/opendc/web/client/OpenDCClient.kt | 73 +++++++++++ .../org/opendc/web/client/PortfolioResource.kt | 58 +++++++++ .../org/opendc/web/client/ProjectResource.kt | 52 ++++++++ .../org/opendc/web/client/ScenarioResource.kt | 63 +++++++++ .../org/opendc/web/client/SchedulerResource.kt | 36 +++++ .../org/opendc/web/client/TopologyResource.kt | 66 ++++++++++ .../kotlin/org/opendc/web/client/TraceResource.kt | 42 ++++++ .../org/opendc/web/client/auth/AuthController.kt | 40 ++++++ .../opendc/web/client/auth/OpenIdAuthController.kt | 140 ++++++++++++++++++++ .../org/opendc/web/client/internal/ClientUtils.kt | 54 ++++++++ .../web/client/internal/OAuthTokenRequest.kt | 62 +++++++++ .../web/client/internal/OAuthTokenResponse.kt | 40 ++++++ .../web/client/internal/OpenIdConfiguration.kt | 43 ++++++ .../org/opendc/web/client/runner/JobResource.kt | 47 +++++++ .../opendc/web/client/runner/OpenDCRunnerClient.kt | 59 +++++++++ .../web/client/transport/HttpTransportClient.kt | 145 +++++++++++++++++++++ .../opendc/web/client/transport/TransportClient.kt | 50 +++++++ 18 files changed, 1107 insertions(+) create mode 100644 opendc-web/opendc-web-client/build.gradle.kts create mode 100644 opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/OpenDCClient.kt create mode 100644 opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/PortfolioResource.kt create mode 100644 opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/ProjectResource.kt create mode 100644 opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/ScenarioResource.kt create mode 100644 opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/SchedulerResource.kt create mode 100644 opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/TopologyResource.kt create mode 100644 opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/TraceResource.kt create mode 100644 opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/auth/AuthController.kt create mode 100644 opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/auth/OpenIdAuthController.kt create mode 100644 opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/ClientUtils.kt create mode 100644 opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/OAuthTokenRequest.kt create mode 100644 opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/OAuthTokenResponse.kt create mode 100644 opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/OpenIdConfiguration.kt create mode 100644 opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/runner/JobResource.kt create mode 100644 opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/runner/OpenDCRunnerClient.kt create mode 100644 opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/transport/HttpTransportClient.kt create mode 100644 opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/transport/TransportClient.kt (limited to 'opendc-web') diff --git a/opendc-web/opendc-web-client/build.gradle.kts b/opendc-web/opendc-web-client/build.gradle.kts new file mode 100644 index 00000000..f53b29d8 --- /dev/null +++ b/opendc-web/opendc-web-client/build.gradle.kts @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +description = "Client for the OpenDC web API" + +/* Build configuration */ +plugins { + `kotlin-library-conventions` + `testing-conventions` + `jacoco-conventions` +} + +dependencies { + api(projects.opendcWeb.opendcWebProto) + implementation(libs.jackson.module.kotlin) + implementation(libs.jackson.datatype.jsr310) + implementation(libs.jakarta.validation) +} diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/OpenDCClient.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/OpenDCClient.kt new file mode 100644 index 00000000..33f2b41e --- /dev/null +++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/OpenDCClient.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2022 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.client + +import org.opendc.web.client.auth.AuthController +import org.opendc.web.client.transport.HttpTransportClient +import org.opendc.web.client.transport.TransportClient +import java.net.URI + +/** + * Client implementation for the user-facing OpenDC REST API (version 2). + * + * @param client Low-level client for managing the underlying transport. + */ +public class OpenDCClient(client: TransportClient) { + /** + * Construct a new [OpenDCClient]. + * + * @param baseUrl The base url of the API. + * @param auth Helper class for managing authentication. + */ + public constructor(baseUrl: URI, auth: AuthController) : this(HttpTransportClient(baseUrl, auth)) + + /** + * A resource for the available projects. + */ + public val projects: ProjectResource = ProjectResource(client) + + /** + * A resource for the topologies available to the user. + */ + public val topologies: TopologyResource = TopologyResource(client) + + /** + * A resource for the portfolios available to the user. + */ + public val portfolios: PortfolioResource = PortfolioResource(client) + + /** + * A resource for the scenarios available to the user. + */ + public val scenarios: ScenarioResource = ScenarioResource(client) + + /** + * A resource for the available schedulers. + */ + public val schedulers: SchedulerResource = SchedulerResource(client) + + /** + * A resource for the available workload traces. + */ + public val traces: TraceResource = TraceResource(client) +} diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/PortfolioResource.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/PortfolioResource.kt new file mode 100644 index 00000000..399804e8 --- /dev/null +++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/PortfolioResource.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2022 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.client + +import org.opendc.web.client.internal.delete +import org.opendc.web.client.internal.get +import org.opendc.web.client.internal.post +import org.opendc.web.client.transport.TransportClient +import org.opendc.web.proto.user.Portfolio + +/** + * A resource representing the portfolios available to the user. + */ +public class PortfolioResource internal constructor(private val client: TransportClient) { + /** + * List all portfolios that belong to the specified [project]. + */ + public fun getAll(project: Long): List = client.get("projects/$project/portfolios") ?: emptyList() + + /** + * Obtain the portfolio for [project] with [number]. + */ + public fun get(project: Long, number: Int): Portfolio? = client.get("projects/$project/portfolios/$number") + + /** + * Create a new portfolio for [project] with the specified [request]. + */ + public fun create(project: Long, request: Portfolio.Create): Portfolio { + return checkNotNull(client.post("projects/$project/portfolios", request)) + } + + /** + * Delete the portfolio for [project] with [index]. + */ + public fun delete(project: Long, index: Int): Portfolio { + return requireNotNull(client.delete("projects/$project/portfolios/$index")) { "Unknown portfolio $index" } + } +} diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/ProjectResource.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/ProjectResource.kt new file mode 100644 index 00000000..12635b89 --- /dev/null +++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/ProjectResource.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2022 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.client + +import org.opendc.web.client.internal.* +import org.opendc.web.client.transport.TransportClient +import org.opendc.web.proto.user.Project + +/** + * A resource representing the projects available to the user. + */ +public class ProjectResource internal constructor(private val client: TransportClient) { + /** + * List all projects available to the user. + */ + public fun getAll(): List = client.get("projects") ?: emptyList() + + /** + * Obtain the project with [id]. + */ + public fun get(id: Long): Project? = client.get("projects/$id") + + /** + * Create a new project. + */ + public fun create(name: String): Project = checkNotNull(client.post("projects", Project.Create(name))) + + /** + * Delete the project with the specified [id]. + */ + public fun delete(id: Long): Project = requireNotNull(client.delete("projects/$id")) { "Unknown project $id" } +} diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/ScenarioResource.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/ScenarioResource.kt new file mode 100644 index 00000000..7055e752 --- /dev/null +++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/ScenarioResource.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2022 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.client + +import org.opendc.web.client.internal.delete +import org.opendc.web.client.internal.get +import org.opendc.web.client.internal.post +import org.opendc.web.client.transport.TransportClient +import org.opendc.web.proto.user.Scenario + +/** + * A resource representing the scenarios available to the user. + */ +public class ScenarioResource internal constructor(private val client: TransportClient) { + /** + * List all scenarios that belong to the specified [project]. + */ + public fun getAll(project: Long): List = client.get("projects/$project/scenarios") ?: emptyList() + + /** + * List all scenarios that belong to the specified [portfolioNumber]. + */ + public fun getAll(project: Long, portfolioNumber: Int): List = client.get("projects/$project/portfolios/$portfolioNumber/scenarios") ?: emptyList() + + /** + * Obtain the scenario for [project] with [index]. + */ + public fun get(project: Long, index: Int): Scenario? = client.get("projects/$project/scenarios/$index") + + /** + * Create a new scenario for [portfolio][portfolioNumber] with the specified [request]. + */ + public fun create(project: Long, portfolioNumber: Int, request: Scenario.Create): Scenario { + return checkNotNull(client.post("projects/$project/portfolios/$portfolioNumber", request)) + } + + /** + * Delete the scenario for [project] with [index]. + */ + public fun delete(project: Long, index: Int): Scenario { + return requireNotNull(client.delete("projects/$project/scenarios/$index")) { "Unknown scenario $index" } + } +} diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/SchedulerResource.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/SchedulerResource.kt new file mode 100644 index 00000000..43b72d88 --- /dev/null +++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/SchedulerResource.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.client + +import org.opendc.web.client.internal.get +import org.opendc.web.client.transport.TransportClient + +/** + * A resource representing the schedulers available in the OpenDC instance. + */ +public class SchedulerResource internal constructor(private val client: TransportClient) { + /** + * List all schedulers available. + */ + public fun getAll(): List = client.get("schedulers") ?: emptyList() +} diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/TopologyResource.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/TopologyResource.kt new file mode 100644 index 00000000..c37ae8da --- /dev/null +++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/TopologyResource.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2022 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.client + +import org.opendc.web.client.internal.delete +import org.opendc.web.client.internal.get +import org.opendc.web.client.internal.post +import org.opendc.web.client.internal.put +import org.opendc.web.client.transport.TransportClient +import org.opendc.web.proto.user.Topology + +/** + * A resource representing the topologies available to the user. + */ +public class TopologyResource internal constructor(private val client: TransportClient) { + /** + * List all topologies that belong to the specified [project]. + */ + public fun getAll(project: Long): List = client.get("projects/$project/topologies") ?: emptyList() + + /** + * Obtain the topology for [project] with [index]. + */ + public fun get(project: Long, index: Int): Topology? = client.get("projects/$project/topologies/$index") + + /** + * Create a new topology for [project] with [request]. + */ + public fun create(project: Long, request: Topology.Create): Topology { + return checkNotNull(client.post("projects/$project/topologies", request)) + } + + /** + * Update the topology with [index] for [project] using the specified [request]. + */ + public fun update(project: Long, index: Int, request: Topology.Update): Topology? { + return client.put("projects/$project/topologies/$index", request) + } + + /** + * Delete the topology for [project] with [index]. + */ + public fun delete(project: Long, index: Long): Topology { + return requireNotNull(client.delete("projects/$project/topologies/$index")) { "Unknown topology $index" } + } +} diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/TraceResource.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/TraceResource.kt new file mode 100644 index 00000000..8201c432 --- /dev/null +++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/TraceResource.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2022 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.client + +import org.opendc.web.client.internal.* +import org.opendc.web.client.transport.TransportClient +import org.opendc.web.proto.Trace + +/** + * A resource representing the workload traces available in the OpenDC instance. + */ +public class TraceResource internal constructor(private val client: TransportClient) { + /** + * List all workload traces available. + */ + public fun getAll(): List = client.get("traces") ?: emptyList() + + /** + * Obtain the workload trace with the specified [id]. + */ + public fun get(id: Long): Trace? = client.get("traces/$id") +} diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/auth/AuthController.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/auth/AuthController.kt new file mode 100644 index 00000000..a4c66f55 --- /dev/null +++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/auth/AuthController.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2022 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.client.auth + +import java.net.http.HttpRequest + +/** + * Helper interface for managing API authentication. + */ +public interface AuthController { + /** + * Inject the authorization token into the specified [request]. + */ + public fun injectToken(request: HttpRequest.Builder) + + /** + * Refresh the current auth token. + */ + public fun refreshToken() +} diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/auth/OpenIdAuthController.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/auth/OpenIdAuthController.kt new file mode 100644 index 00000000..7f9cbacd --- /dev/null +++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/auth/OpenIdAuthController.kt @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2022 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.client.auth + +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import org.opendc.web.client.internal.OAuthTokenRequest +import org.opendc.web.client.internal.OAuthTokenResponse +import org.opendc.web.client.internal.OpenIdConfiguration +import java.net.URI +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse + +/** + * An [AuthController] for OpenID Connect protected APIs. + */ +public class OpenIdAuthController( + private val domain: String, + private val clientId: String, + private val clientSecret: String, + private val audience: String = "https://api.opendc.org/v2/", + private val client: HttpClient = HttpClient.newHttpClient() +) : AuthController { + /** + * The Jackson object mapper to convert messages from/to JSON. + */ + private val mapper = jacksonObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + + /** + * The cached [OpenIdConfiguration]. + */ + private val openidConfig: OpenIdConfiguration + get() { + var openidConfig = _openidConfig + if (openidConfig == null) { + openidConfig = requestConfig() + _openidConfig = openidConfig + } + + return openidConfig + } + private var _openidConfig: OpenIdConfiguration? = null + + /** + * The cached OAuth token. + */ + private var _token: OAuthTokenResponse? = null + + override fun injectToken(request: HttpRequest.Builder) { + var token = _token + if (token == null) { + token = requestToken() + _token = token + } + + request.header("Authorization", "Bearer ${token.accessToken}") + } + + /** + * Refresh the current access token. + */ + override fun refreshToken() { + val refreshToken = _token?.refreshToken + if (refreshToken == null) { + requestToken() + return + } + + _token = refreshToken(openidConfig, refreshToken) + } + + /** + * Request the OpenID configuration from the chosen auth domain + */ + private fun requestConfig(): OpenIdConfiguration { + val request = HttpRequest.newBuilder(URI("https://$domain/.well-known/openid-configuration")) + .GET() + .build() + val response = client.send(request, HttpResponse.BodyHandlers.ofInputStream()) + return mapper.readValue(response.body()) + } + + /** + * Request the auth token from the server. + */ + private fun requestToken(openidConfig: OpenIdConfiguration): OAuthTokenResponse { + val body = OAuthTokenRequest.ClientCredentials(audience, clientId, clientSecret) + val request = HttpRequest.newBuilder(openidConfig.tokenEndpoint) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofByteArray(mapper.writeValueAsBytes(body))) + .build() + val response = client.send(request, HttpResponse.BodyHandlers.ofInputStream()) + return mapper.readValue(response.body()) + } + + /** + * Helper method to refresh the auth token. + */ + private fun refreshToken(openidConfig: OpenIdConfiguration, refreshToken: String): OAuthTokenResponse { + val body = OAuthTokenRequest.RefreshToken(refreshToken, clientId, clientSecret) + val request = HttpRequest.newBuilder(openidConfig.tokenEndpoint) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofByteArray(mapper.writeValueAsBytes(body))) + .build() + val response = client.send(request, HttpResponse.BodyHandlers.ofInputStream()) + return mapper.readValue(response.body()) + } + + /** + * Fetch a new access token. + */ + private fun requestToken(): OAuthTokenResponse { + val token = requestToken(openidConfig) + _token = token + return token + } +} diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/ClientUtils.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/ClientUtils.kt new file mode 100644 index 00000000..29cf09dc --- /dev/null +++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/ClientUtils.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2022 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.client.internal + +import com.fasterxml.jackson.core.type.TypeReference +import org.opendc.web.client.transport.TransportClient + +/** + * Perform a GET request for resource at [path] and convert to type [T]. + */ +internal inline fun TransportClient.get(path: String): T? { + return get(path, object : TypeReference() {}) +} + +/** + * Perform a POST request for resource at [path] and convert to type [T]. + */ +internal inline fun TransportClient.post(path: String, body: B): T? { + return post(path, body, object : TypeReference() {}) +} + +/** + * Perform a PUT request for resource at [path] and convert to type [T]. + */ +internal inline fun TransportClient.put(path: String, body: B): T? { + return put(path, body, object : TypeReference() {}) +} + +/** + * Perform a DELETE request for resource at [path] and convert to type [T]. + */ +internal inline fun TransportClient.delete(path: String): T? { + return delete(path, object : TypeReference() {}) +} diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/OAuthTokenRequest.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/OAuthTokenRequest.kt new file mode 100644 index 00000000..25341995 --- /dev/null +++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/OAuthTokenRequest.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2022 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.client.internal + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonSubTypes +import com.fasterxml.jackson.annotation.JsonTypeInfo + +/** + * Token request sent to the OAuth server. + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "grant_type") +@JsonSubTypes( + value = [ + JsonSubTypes.Type(value = OAuthTokenRequest.ClientCredentials::class, name = "client_credentials"), + JsonSubTypes.Type(value = OAuthTokenRequest.RefreshToken::class, name = "refresh_token") + ] +) +internal sealed class OAuthTokenRequest { + /** + * Client credentials grant for OAuth2 + */ + data class ClientCredentials( + val audience: String, + @JsonProperty("client_id") + val clientId: String, + @JsonProperty("client_secret") + val clientSecret: String + ) : OAuthTokenRequest() + + /** + * Refresh token grant for OAuth2. + */ + data class RefreshToken( + @JsonProperty("refresh_token") + val refreshToken: String, + @JsonProperty("client_id") + val clientId: String, + @JsonProperty("client_secret") + val clientSecret: String + ) : OAuthTokenRequest() +} diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/OAuthTokenResponse.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/OAuthTokenResponse.kt new file mode 100644 index 00000000..cd5ccab0 --- /dev/null +++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/OAuthTokenResponse.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2022 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.client.internal + +import com.fasterxml.jackson.annotation.JsonProperty + +/** + * Token response from the OAuth server. + */ +internal data class OAuthTokenResponse( + @JsonProperty("access_token") + val accessToken: String, + @JsonProperty("refresh_token") + val refreshToken: String? = null, + @JsonProperty("token_type") + val tokenType: String, + val scope: String = "", + @JsonProperty("expires_in") + val expiresIn: Long +) diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/OpenIdConfiguration.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/OpenIdConfiguration.kt new file mode 100644 index 00000000..23fbf368 --- /dev/null +++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/internal/OpenIdConfiguration.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2022 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.client.internal + +import com.fasterxml.jackson.annotation.JsonProperty +import java.net.URI + +/** + * OpenID configuration exposed by the auth server. + */ +internal data class OpenIdConfiguration( + val issuer: String, + @JsonProperty("authorization_endpoint") + val authorizationEndpoint: URI, + @JsonProperty("token_endpoint") + val tokenEndpoint: URI, + @JsonProperty("userinfo_endpoint") + val userInfoEndpoint: URI, + @JsonProperty("jwks_uri") + val jwksUri: URI, + @JsonProperty("scopes_supported") + val scopesSupported: Set +) diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/runner/JobResource.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/runner/JobResource.kt new file mode 100644 index 00000000..372a92d7 --- /dev/null +++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/runner/JobResource.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2022 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.client.runner + +import org.opendc.web.client.internal.* +import org.opendc.web.client.transport.TransportClient +import org.opendc.web.proto.runner.Job + +/** + * A resource representing the available simulation jobs for the runner. + */ +public class JobResource internal constructor(private val client: TransportClient) { + /** + * Query the pending jobs. + */ + public fun queryPending(): List = client.get("jobs") ?: emptyList() + + /** + * Obtain the job with [id]. + */ + public fun get(id: Long): Job? = client.get("jobs/$id") + + /** + * Update the job with [id]. + */ + public fun update(id: Long, update: Job.Update): Job? = client.post("jobs/$id", update) +} diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/runner/OpenDCRunnerClient.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/runner/OpenDCRunnerClient.kt new file mode 100644 index 00000000..a3cff6c3 --- /dev/null +++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/runner/OpenDCRunnerClient.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2022 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.client.runner + +import org.opendc.web.client.* +import org.opendc.web.client.auth.AuthController +import org.opendc.web.client.transport.HttpTransportClient +import org.opendc.web.client.transport.TransportClient +import java.net.URI + +/** + * Client implementation for the runner-facing OpenDC REST API (version 2). + * + * @param client Low-level client for managing the underlying transport. + */ +public class OpenDCRunnerClient(client: TransportClient) { + /** + * Construct a new [OpenDCRunnerClient]. + * + * @param baseUrl The base url of the API. + * @param auth Helper class for managing authentication. + */ + public constructor(baseUrl: URI, auth: AuthController) : this(HttpTransportClient(baseUrl, auth)) + + /** + * A resource for the available simulation jobs. + */ + public val jobs: JobResource = JobResource(client) + + /** + * A resource for the available schedulers. + */ + public val schedulers: SchedulerResource = SchedulerResource(client) + + /** + * A resource for the available workload traces. + */ + public val traces: TraceResource = TraceResource(client) +} diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/transport/HttpTransportClient.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/transport/HttpTransportClient.kt new file mode 100644 index 00000000..03b3945f --- /dev/null +++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/transport/HttpTransportClient.kt @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2022 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.client.transport + +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import org.opendc.web.client.auth.AuthController +import java.net.URI +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse +import java.nio.file.Paths + +/** + * A [TransportClient] that accesses the OpenDC API over HTTP. + * + * @param baseUrl The base url of the API. + * @param auth Helper class for managing authentication. + * @param client The HTTP client to use. + */ +public class HttpTransportClient( + private val baseUrl: URI, + private val auth: AuthController, + private val client: HttpClient = HttpClient.newHttpClient() +) : TransportClient { + /** + * The Jackson object mapper to convert messages from/to JSON. + */ + private val mapper = jacksonObjectMapper() + .registerModule(JavaTimeModule()) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + + /** + * Obtain a resource at [path] of [targetType]. + */ + override fun get(path: String, targetType: TypeReference): T? { + val request = HttpRequest.newBuilder(buildUri(path)) + .GET() + .also { auth.injectToken(it) } + .build() + val response = client.send(request, HttpResponse.BodyHandlers.ofInputStream()) + + return when (val code = response.statusCode()) { + in 200..299 -> mapper.readValue(response.body(), targetType) + 401 -> { + auth.refreshToken() + get(path, targetType) + } + 404 -> null + else -> throw IllegalStateException("Invalid response $code") + } + } + + /** + * Update a resource at [path] of [targetType]. + */ + override fun post(path: String, body: B, targetType: TypeReference): T? { + val request = HttpRequest.newBuilder(buildUri(path)) + .POST(HttpRequest.BodyPublishers.ofByteArray(mapper.writeValueAsBytes(body))) + .header("Content-Type", "application/json") + .also { auth.injectToken(it) } + .build() + val response = client.send(request, HttpResponse.BodyHandlers.ofInputStream()) + + return when (val code = response.statusCode()) { + in 200..299 -> mapper.readValue(response.body(), targetType) + 401 -> { + auth.refreshToken() + post(path, body, targetType) + } + 404 -> null + else -> throw IllegalStateException("Invalid response $code") + } + } + + /** + * Replace a resource at [path] of [targetType]. + */ + override fun put(path: String, body: B, targetType: TypeReference): T? { + val request = HttpRequest.newBuilder(buildUri(path)) + .PUT(HttpRequest.BodyPublishers.ofByteArray(mapper.writeValueAsBytes(body))) + .header("Content-Type", "application/json") + .also { auth.injectToken(it) } + .build() + val response = client.send(request, HttpResponse.BodyHandlers.ofInputStream()) + + return when (val code = response.statusCode()) { + in 200..299 -> mapper.readValue(response.body(), targetType) + 401 -> { + auth.refreshToken() + put(path, body, targetType) + } + 404 -> null + else -> throw IllegalStateException("Invalid response $code") + } + } + + /** + * Delete a resource at [path] of [targetType]. + */ + override fun delete(path: String, targetType: TypeReference): T? { + val request = HttpRequest.newBuilder(buildUri(path)) + .DELETE() + .also { auth.injectToken(it) } + .build() + val response = client.send(request, HttpResponse.BodyHandlers.ofInputStream()) + + return when (val code = response.statusCode()) { + in 200..299 -> mapper.readValue(response.body(), targetType) + 401 -> { + auth.refreshToken() + delete(path, targetType) + } + 404 -> null + else -> throw IllegalStateException("Invalid response $code") + } + } + + /** + * Build the absolute [URI] to which the request should be sent. + */ + private fun buildUri(path: String): URI = baseUrl.resolve(Paths.get(baseUrl.path, path).toString()) +} diff --git a/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/transport/TransportClient.kt b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/transport/TransportClient.kt new file mode 100644 index 00000000..af727ca7 --- /dev/null +++ b/opendc-web/opendc-web-client/src/main/kotlin/org/opendc/web/client/transport/TransportClient.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2022 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.client.transport + +import com.fasterxml.jackson.core.type.TypeReference + +/** + * Low-level interface for dealing with the transport layer of the API. + */ +public interface TransportClient { + /** + * Obtain a resource at [path] of [targetType]. + */ + public fun get(path: String, targetType: TypeReference): T? + + /** + * Update a resource at [path] of [targetType]. + */ + public fun post(path: String, body: B, targetType: TypeReference): T? + + /** + * Replace a resource at [path] of [targetType]. + */ + public fun put(path: String, body: B, targetType: TypeReference): T? + + /** + * Delete a resource at [path] of [targetType]. + */ + public fun delete(path: String, targetType: TypeReference): T? +} -- cgit v1.2.3