diff options
| author | Fabian Mastenbroek <mail.fabianm@gmail.com> | 2023-02-03 18:32:34 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-02-03 18:32:34 +0000 |
| commit | bb5e9e7778f6e7cc2161a988fef177d28df8d64f (patch) | |
| tree | 3085bdbc0899c7269e286ce026982c02a3a2864e | |
| parent | dd9b7b1e0c59c010fb191a1ea1d805f2748fb216 (diff) | |
| parent | 49b3015a16287bb4486aa64c5c26f05f7c22089c (diff) | |
merge: Clean up web server (#130)
This pull request cleans up the web server to follow Quarkus' best-practices.
## Implementation Notes :hammer_and_pick:
* Migrate to Hypersistence Utils
* Convert web server utils to Java
* Use Panache for entity modeling
* Convert resources to Java
* Remove unnecessary service indirections
## External Dependencies :four_leaf_clover:
* Panache for modeling database entities
* Hypersistence Utils for storing JSON in database
* Mockito for mocking in the web server tests
## Breaking API Changes :warning:
* All implementation is moved to Java for better compatibility with Quarkus.
* Scenarios can now have multiple jobs (e.g., if retried)
100 files changed, 4921 insertions, 5516 deletions
diff --git a/buildSrc/src/main/kotlin/quarkus-conventions.gradle.kts b/buildSrc/src/main/kotlin/quarkus-conventions.gradle.kts index f1adb182..3bd3d85a 100644 --- a/buildSrc/src/main/kotlin/quarkus-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/quarkus-conventions.gradle.kts @@ -29,25 +29,11 @@ import org.gradle.kotlin.dsl.kotlin import org.gradle.kotlin.dsl.withType plugins { - id("kotlin-conventions") - kotlin("plugin.allopen") - kotlin("plugin.jpa") + id("java-conventions") id("testing-conventions") id("io.quarkus") } -/* Mark necessary classes as open in Kotlin */ -allOpen { - annotation("javax.ws.rs.Path") - annotation("javax.enterprise.context.ApplicationScoped") - annotation("io.quarkus.test.junit.QuarkusTest") - annotation("javax.persistence.Entity") -} - -/* Include metadata for method parameters */ -tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> { - kotlinOptions.javaParameters = true -} /* Launch Quarkus dev mode from project root directory */ tasks.quarkusDev { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 627f1768..2581f1ca 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,6 +5,7 @@ commons-math3 = "3.6.1" dokka = "1.7.10" gradle-node = "3.5.0" hadoop = "3.3.4" +hypersistence-utils = "3.0.1" jackson = "2.14.0" jandex-gradle = "1.0.0" java = "17" @@ -22,8 +23,7 @@ mockk = "1.13.2" node = "18.12.0" parquet = "1.12.3" progressbar = "0.9.3" -quarkus = "2.14.2.Final" -quarkus-junit5-mockk = "1.1.1" +quarkus = "2.16.1.Final" sentry = "6.8.0" slf4j = "2.0.4" spotless = "6.12.0" @@ -78,19 +78,20 @@ quarkus-resteasy-jackson = { module = "io.quarkus:quarkus-resteasy-jackson" } quarkus-smallrye-openapi = { module = "io.quarkus:quarkus-smallrye-openapi" } quarkus-security = { module = "io.quarkus:quarkus-security" } quarkus-oidc = { module = "io.quarkus:quarkus-oidc" } -quarkus-hibernate-orm = { module = "io.quarkus:quarkus-hibernate-orm" } +quarkus-hibernate-orm-core = { module = "io.quarkus:quarkus-hibernate-orm" } +quarkus-hibernate-orm-panache = { module = "io.quarkus:quarkus-hibernate-orm-panache" } quarkus-hibernate-validator = { module = "io.quarkus:quarkus-hibernate-validator" } quarkus-jdbc-h2 = { module = "io.quarkus:quarkus-jdbc-h2" } quarkus-jdbc-postgresql = { module = "io.quarkus:quarkus-jdbc-postgresql" } quarkus-flyway = { module = "io.quarkus:quarkus-flyway" } +hypersistence-utils-hibernate = { module = "io.hypersistence:hypersistence-utils-hibernate-55", version.ref = "hypersistence-utils" } # Quarkus (Testing) quarkus-junit5-core = { module = "io.quarkus:quarkus-junit5" } -quarkus-junit5-mockk = { module = "io.quarkiverse.mockk:quarkus-junit5-mockk", version.ref = "quarkus-junit5-mockk" } quarkus-jacoco = { module = "io.quarkus:quarkus-jacoco" } +quarkus-panache-mock = { module = "io.quarkus:quarkus-panache-mock" } quarkus-test-security = { module = "io.quarkus:quarkus-test-security" } restassured-core = { module = "io.rest-assured:rest-assured" } -restassured-kotlin = { module = "io.rest-assured:kotlin-extensions" } # Calcite (SQL) calcite-core = { module = "org.apache.calcite:calcite-core", version.ref = "calcite" } diff --git a/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/Scenario.kt b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/Scenario.kt index ed77ef08..b9c7a4cf 100644 --- a/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/Scenario.kt +++ b/opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/Scenario.kt @@ -40,7 +40,7 @@ public data class Scenario( val topology: Topology.Summary, val phenomena: OperationalPhenomena, val schedulerName: String, - val job: Job + val jobs: List<Job> ) { /** * Create a new scenario. @@ -81,6 +81,6 @@ public data class Scenario( val topology: Topology.Summary, val phenomena: OperationalPhenomena, val schedulerName: String, - val job: Job + val jobs: List<Job> ) } diff --git a/opendc-web/opendc-web-server/build.gradle.kts b/opendc-web/opendc-web-server/build.gradle.kts index 714ab066..d05210ac 100644 --- a/opendc-web/opendc-web-server/build.gradle.kts +++ b/opendc-web/opendc-web-server/build.gradle.kts @@ -46,17 +46,18 @@ dependencies { implementation(libs.quarkus.security) implementation(libs.quarkus.oidc) - implementation(libs.quarkus.hibernate.orm) + implementation(libs.quarkus.hibernate.orm.core) + implementation(libs.quarkus.hibernate.orm.panache) implementation(libs.quarkus.hibernate.validator) implementation(libs.quarkus.flyway) implementation(libs.quarkus.jdbc.postgresql) implementation(libs.quarkus.jdbc.h2) + implementation(libs.hypersistence.utils.hibernate) testImplementation(libs.quarkus.junit5.core) - testImplementation(libs.quarkus.junit5.mockk) testImplementation(libs.quarkus.jacoco) + testImplementation(libs.quarkus.panache.mock) testImplementation(libs.restassured.core) - testImplementation(libs.restassured.kotlin) testImplementation(libs.quarkus.test.security) testImplementation(libs.quarkus.jdbc.h2) } diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Job.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Job.java new file mode 100644 index 00000000..c5fb208e --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Job.java @@ -0,0 +1,167 @@ +/* + * 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.server.model; + +import io.quarkus.hibernate.orm.panache.Panache; +import io.quarkus.hibernate.orm.panache.PanacheEntity; +import io.quarkus.hibernate.orm.panache.PanacheQuery; +import io.quarkus.panache.common.Parameters; +import java.time.Instant; +import java.util.Map; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.FetchType; +import javax.persistence.ForeignKey; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; +import org.hibernate.annotations.Type; +import org.opendc.web.proto.JobState; + +/** + * A simulation job to be run by the simulator. + */ +@Entity +@Table(name = "jobs") +@NamedQueries({ + @NamedQuery( + name = "Job.updateOne", + query = + """ + UPDATE Job j + SET j.state = :newState, j.updatedAt = :updatedAt, j.runtime = :runtime, j.results = :results + WHERE j.id = :id AND j.state = :oldState + """) +}) +public class Job extends PanacheEntity { + @ManyToOne(optional = false, fetch = FetchType.EAGER) + @JoinColumn(name = "scenario_id", foreignKey = @ForeignKey(name = "fk_jobs_scenario"), nullable = false) + public Scenario scenario; + + @Column(name = "created_by", nullable = false, updatable = false) + public String createdBy; + + @Column(name = "created_at", nullable = false, updatable = false) + public Instant createdAt; + + /** + * The number of simulation runs to perform. + */ + @Column(nullable = false, updatable = false) + public int repeats; + + /** + * The instant at which the job was updated. + */ + @Column(name = "updated_at", nullable = false) + public Instant updatedAt; + + /** + * The state of the job. + */ + @Type(type = "io.hypersistence.utils.hibernate.type.basic.PostgreSQLEnumType") + @Column(nullable = false, columnDefinition = "enum") + @Enumerated(EnumType.STRING) + public JobState state = JobState.PENDING; + + /** + * The runtime of the job (in seconds). + */ + @Column(nullable = false) + public int runtime = 0; + + /** + * Experiment results in JSON + */ + @Type(type = "io.hypersistence.utils.hibernate.type.json.JsonType") + @Column(columnDefinition = "jsonb") + public Map<String, ?> results = null; + + /** + * Construct a {@link Job} instance. + */ + public Job(Scenario scenario, String createdBy, Instant createdAt, int repeats) { + this.createdBy = createdBy; + this.scenario = scenario; + this.createdAt = createdAt; + this.updatedAt = createdAt; + this.repeats = repeats; + } + + /** + * JPA constructor + */ + protected Job() {} + + /** + * Find {@link Job}s in the specified {@link JobState}. + * + * @param state The state of the jobs to find. + * @return A query for jobs that are in the specified state. + */ + public static PanacheQuery<Job> findByState(JobState state) { + return find("state", state); + } + + /** + * Atomically update this job. + * + * @param newState The new state to enter into. + * @param time The time at which the update occurs. + * @param results The results to possible set. + * @return <code>true</code> when the update succeeded`, <code>false</code> when there was a conflict. + */ + public boolean updateAtomically(JobState newState, Instant time, int runtime, Map<String, ?> results) { + long count = update( + "#Job.updateOne", + Parameters.with("id", id) + .and("oldState", state) + .and("newState", newState) + .and("updatedAt", time) + .and("runtime", runtime) + .and("results", results)); + Panache.getEntityManager().refresh(this); + return count > 0; + } + + /** + * Determine whether the job is allowed to transition to <code>newState</code>. + * + * @param newState The new state to transition to. + * @return <code>true</code> if the transition to the new state is legal, <code>false</code> otherwise. + */ + public boolean canTransitionTo(JobState newState) { + // Note that we always allow transitions from the state + return newState == this.state + || switch (this.state) { + case PENDING -> newState == JobState.CLAIMED; + case CLAIMED -> newState == JobState.RUNNING || newState == JobState.FAILED; + case RUNNING -> newState == JobState.FINISHED || newState == JobState.FAILED; + case FINISHED, FAILED -> false; + }; + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Portfolio.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Portfolio.java new file mode 100644 index 00000000..3a406683 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Portfolio.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2023 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.server.model; + +import io.quarkus.hibernate.orm.panache.PanacheEntity; +import io.quarkus.hibernate.orm.panache.PanacheQuery; +import io.quarkus.panache.common.Parameters; +import java.util.HashSet; +import java.util.Set; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.OrderBy; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import org.hibernate.annotations.Type; +import org.opendc.web.proto.Targets; + +/** + * A portfolio is the composition of multiple scenarios. + */ +@Entity +@Table( + name = "portfolios", + uniqueConstraints = { + @UniqueConstraint( + name = "uk_portfolios_number", + columnNames = {"project_id", "number"}) + }, + indexes = {@Index(name = "ux_portfolios_number", columnList = "project_id, number")}) +@NamedQueries({ + @NamedQuery(name = "Portfolio.findByProject", query = "SELECT p FROM Portfolio p WHERE p.project.id = :projectId"), + @NamedQuery( + name = "Portfolio.findOneByProject", + query = "SELECT p FROM Portfolio p WHERE p.project.id = :projectId AND p.number = :number") +}) +public class Portfolio extends PanacheEntity { + /** + * The {@link Project} this portfolio belongs to. + */ + @ManyToOne(optional = false) + @JoinColumn(name = "project_id", nullable = false) + public Project project; + + /** + * Unique number of the portfolio for the project. + */ + @Column(nullable = false) + public int number; + + /** + * The name of this portfolio. + */ + @Column(nullable = false) + public String name; + + /** + * The portfolio targets (metrics, repetitions). + */ + @Type(type = "io.hypersistence.utils.hibernate.type.json.JsonType") + @Column(columnDefinition = "jsonb", nullable = false, updatable = false) + public Targets targets; + + /** + * The scenarios in this portfolio. + */ + @OneToMany( + cascade = {CascadeType.ALL}, + mappedBy = "portfolio", + orphanRemoval = true) + @OrderBy("id ASC") + public Set<Scenario> scenarios = new HashSet<>(); + + /** + * Construct a {@link Portfolio} object. + */ + public Portfolio(Project project, int number, String name, Targets targets) { + this.project = project; + this.number = number; + this.name = name; + this.targets = targets; + } + + /** + * JPA constructor + */ + protected Portfolio() {} + + /** + * Find all {@link Portfolio}s that belong to the specified project + * + * @param projectId The unique identifier of the project. + * @return The query of portfolios that belong to the specified project. + */ + public static PanacheQuery<Portfolio> findByProject(long projectId) { + return find("#Portfolio.findByProject", Parameters.with("projectId", projectId)); + } + + /** + * Find the {@link Portfolio} with the specified <code>number</code> belonging to the specified project. + * + * @param projectId The unique identifier of the project. + * @param number The number of the scenario. + * @return The portfolio or <code>null</code> if it does not exist. + */ + public static Portfolio findByProject(long projectId, int number) { + return find( + "#Portfolio.findOneByProject", + Parameters.with("projectId", projectId).and("number", number)) + .firstResult(); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Project.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Project.java new file mode 100644 index 00000000..5836e33f --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Project.java @@ -0,0 +1,224 @@ +/* + * 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.server.model; + +import io.quarkus.hibernate.orm.panache.Panache; +import io.quarkus.hibernate.orm.panache.PanacheEntity; +import io.quarkus.panache.common.Parameters; +import java.time.Instant; +import java.util.HashSet; +import java.util.Set; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.OrderBy; +import javax.persistence.Table; + +/** + * A project in OpenDC encapsulates all the datacenter designs and simulation runs for a set of users. + */ +@Entity +@Table(name = "projects") +@NamedQueries({ + @NamedQuery( + name = "Project.findByUser", + query = + """ + SELECT a + FROM ProjectAuthorization a + WHERE a.key.userId = :userId + """), + @NamedQuery( + name = "Project.allocatePortfolio", + query = + """ + UPDATE Project p + SET p.portfoliosCreated = :oldState + 1, p.updatedAt = :now + WHERE p.id = :id AND p.portfoliosCreated = :oldState + """), + @NamedQuery( + name = "Project.allocateTopology", + query = + """ + UPDATE Project p + SET p.topologiesCreated = :oldState + 1, p.updatedAt = :now + WHERE p.id = :id AND p.topologiesCreated = :oldState + """), + @NamedQuery( + name = "Project.allocateScenario", + query = + """ + UPDATE Project p + SET p.scenariosCreated = :oldState + 1, p.updatedAt = :now + WHERE p.id = :id AND p.scenariosCreated = :oldState + """) +}) +public class Project extends PanacheEntity { + /** + * The name of the project. + */ + @Column(nullable = false) + public String name; + + /** + * The instant at which the project was created. + */ + @Column(name = "created_at", nullable = false, updatable = false) + public Instant createdAt; + + /** + * The instant at which the project was updated. + */ + @Column(name = "updated_at", nullable = false) + public Instant updatedAt; + + /** + * The portfolios belonging to this project. + */ + @OneToMany( + cascade = {CascadeType.ALL}, + mappedBy = "project", + orphanRemoval = true) + @OrderBy("id ASC") + public Set<Portfolio> portfolios = new HashSet<>(); + + /** + * The number of portfolios created for this project (including deleted portfolios). + */ + @Column(name = "portfolios_created", nullable = false) + public int portfoliosCreated = 0; + + /** + * The topologies belonging to this project. + */ + @OneToMany( + cascade = {CascadeType.ALL}, + mappedBy = "project", + orphanRemoval = true) + @OrderBy("id ASC") + public Set<Topology> topologies = new HashSet<>(); + + /** + * The number of topologies created for this project (including deleted topologies). + */ + @Column(name = "topologies_created", nullable = false) + public int topologiesCreated = 0; + + /** + * The scenarios belonging to this project. + */ + @OneToMany(mappedBy = "project", orphanRemoval = true) + public Set<Scenario> scenarios = new HashSet<>(); + + /** + * The number of scenarios created for this project (including deleted scenarios). + */ + @Column(name = "scenarios_created", nullable = false) + public int scenariosCreated = 0; + + /** + * The users authorized to access the project. + */ + @OneToMany( + cascade = {CascadeType.ALL}, + mappedBy = "project", + orphanRemoval = true) + public Set<ProjectAuthorization> authorizations = new HashSet<>(); + + /** + * Construct a {@link Project} object. + */ + public Project(String name, Instant createdAt) { + this.name = name; + this.createdAt = createdAt; + this.updatedAt = createdAt; + } + + /** + * JPA constructor + */ + protected Project() {} + + /** + * Allocate the next portfolio number for the specified [project]. + * + * @param time The time at which the new portfolio is created. + */ + public int allocatePortfolio(Instant time) { + for (int i = 0; i < 4; i++) { + long count = update( + "#Project.allocatePortfolio", + Parameters.with("id", id).and("oldState", portfoliosCreated).and("now", time)); + if (count > 0) { + return portfoliosCreated + 1; + } else { + Panache.getEntityManager().refresh(this); + } + } + + throw new IllegalStateException("Failed to allocate next portfolio"); + } + + /** + * Allocate the next topology number for the specified [project]. + * + * @param time The time at which the new topology is created. + */ + public int allocateTopology(Instant time) { + for (int i = 0; i < 4; i++) { + long count = update( + "#Project.allocateTopology", + Parameters.with("id", id).and("oldState", topologiesCreated).and("now", time)); + if (count > 0) { + return topologiesCreated + 1; + } else { + Panache.getEntityManager().refresh(this); + } + } + + throw new IllegalStateException("Failed to allocate next topology"); + } + + /** + * Allocate the next scenario number for the specified [project]. + * + * @param time The time at which the new scenario is created. + */ + public int allocateScenario(Instant time) { + for (int i = 0; i < 4; i++) { + long count = update( + "#Project.allocateScenario", + Parameters.with("id", id).and("oldState", scenariosCreated).and("now", time)); + if (count > 0) { + return scenariosCreated + 1; + } else { + Panache.getEntityManager().refresh(this); + } + } + + throw new IllegalStateException("Failed to allocate next scenario"); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/ProjectAuthorization.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/ProjectAuthorization.java new file mode 100644 index 00000000..1238f58d --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/ProjectAuthorization.java @@ -0,0 +1,174 @@ +/* + * 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.server.model; + +import io.quarkus.hibernate.orm.panache.PanacheEntityBase; +import io.quarkus.hibernate.orm.panache.PanacheQuery; +import io.quarkus.panache.common.Parameters; +import java.io.Serializable; +import java.util.Objects; +import javax.persistence.Column; +import javax.persistence.Embeddable; +import javax.persistence.EmbeddedId; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.FetchType; +import javax.persistence.ForeignKey; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.MapsId; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; +import org.hibernate.annotations.Type; +import org.opendc.web.proto.user.ProjectRole; + +/** + * An authorization for some user to participate in a project. + */ +@Entity +@Table(name = "project_authorizations") +@NamedQueries({ + @NamedQuery( + name = "ProjectAuthorization.findByUser", + query = + """ + SELECT a + FROM ProjectAuthorization a + WHERE a.key.userId = :userId + """), +}) +public class ProjectAuthorization extends PanacheEntityBase { + /** + * The user identifier of the authorization. + */ + @EmbeddedId + public ProjectAuthorization.Key key; + + /** + * The project that the user is authorized to participate in. + */ + @ManyToOne(optional = false, fetch = FetchType.LAZY) + @MapsId("projectId") + @JoinColumn( + name = "project_id", + updatable = false, + insertable = false, + nullable = false, + foreignKey = @ForeignKey(name = "fk_project_authorizations")) + public Project project; + + /** + * The role of the user in the project. + */ + @Type(type = "io.hypersistence.utils.hibernate.type.basic.PostgreSQLEnumType") + @Column(nullable = false, columnDefinition = "enum") + @Enumerated(EnumType.STRING) + public ProjectRole role; + + /** + * Construct a {@link ProjectAuthorization} object. + */ + public ProjectAuthorization(Project project, String userId, ProjectRole role) { + this.key = new ProjectAuthorization.Key(project.id, userId); + this.project = project; + this.role = role; + } + + /** + * JPA constructor + */ + protected ProjectAuthorization() {} + + /** + * List all projects for the user with the specified <code>userId</code>. + * + * @param userId The identifier of the user that is requesting the list of projects. + * @return A query returning projects that the user has received authorization for. + */ + public static PanacheQuery<ProjectAuthorization> findByUser(String userId) { + return find("#ProjectAuthorization.findByUser", Parameters.with("userId", userId)); + } + + /** + * Find the project with <code>id</code> for the user with the specified <code>userId</code>. + * + * @param userId The identifier of the user that is requesting the list of projects. + * @param id The unique identifier of the project. + * @return The project with the specified identifier or <code>null</code> if it does not exist or is not accessible + * to the user with the specified identifier. + */ + public static ProjectAuthorization findByUser(String userId, long id) { + return findById(new ProjectAuthorization.Key(id, userId)); + } + + /** + * Determine whether the authorization allows the user to edit the project. + */ + public boolean canEdit() { + return switch (role) { + case OWNER, EDITOR -> true; + case VIEWER -> false; + }; + } + + /** + * Determine whether the authorization allows the user to delete the project. + */ + public boolean canDelete() { + return role == ProjectRole.OWNER; + } + + /** + * Key for representing a {@link ProjectAuthorization} object. + */ + @Embeddable + public static class Key implements Serializable { + @Column(name = "project_id", nullable = false) + public long projectId; + + @Column(name = "user_id", nullable = false) + public String userId; + + public Key(long projectId, String userId) { + this.projectId = projectId; + this.userId = userId; + } + + protected Key() {} + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Key key = (Key) o; + return projectId == key.projectId && userId.equals(key.userId); + } + + @Override + public int hashCode() { + return Objects.hash(projectId, userId); + } + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Scenario.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Scenario.java new file mode 100644 index 00000000..016e931b --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Scenario.java @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2023 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.server.model; + +import io.quarkus.hibernate.orm.panache.PanacheEntity; +import io.quarkus.hibernate.orm.panache.PanacheQuery; +import io.quarkus.panache.common.Parameters; +import java.util.ArrayList; +import java.util.List; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Embedded; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.ForeignKey; +import javax.persistence.Index; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import org.hibernate.annotations.Type; +import org.opendc.web.proto.OperationalPhenomena; + +/** + * A single scenario to be explored by the simulator. + */ +@Entity +@Table( + name = "scenarios", + uniqueConstraints = { + @UniqueConstraint( + name = "uk_scenarios_number", + columnNames = {"project_id", "number"}) + }, + indexes = {@Index(name = "ux_scenarios_number", columnList = "project_id, number")}) +@NamedQueries({ + @NamedQuery(name = "Scenario.findByProject", query = "SELECT s FROM Scenario s WHERE s.project.id = :projectId"), + @NamedQuery( + name = "Scenario.findByPortfolio", + query = + """ + SELECT s + FROM Scenario s + JOIN Portfolio p ON p.id = s.portfolio.id AND p.number = :number + WHERE s.project.id = :projectId + """), + @NamedQuery( + name = "Scenario.findOneByProject", + query = "SELECT s FROM Scenario s WHERE s.project.id = :projectId AND s.number = :number") +}) +public class Scenario extends PanacheEntity { + /** + * The {@link Project} to which this scenario belongs. + */ + @ManyToOne(optional = false) + @JoinColumn(name = "project_id", nullable = false, foreignKey = @ForeignKey(name = "fk_scenarios_project")) + public Project project; + + /** + * The {@link Portfolio} to which this scenario belongs. + */ + @ManyToOne(optional = false) + @JoinColumn(name = "portfolio_id", nullable = false, foreignKey = @ForeignKey(name = "fk_scenarios_portfolio")) + public Portfolio portfolio; + + /** + * Unique number of the scenario for the project. + */ + @Column(nullable = false) + public int number; + + /** + * The name of the scenario. + */ + @Column(nullable = false, updatable = false) + public String name; + + /** + * Workload details of the scenario. + */ + @Embedded + public Workload workload; + + /** + * Topology details of the scenario. + */ + @ManyToOne(optional = false) + @JoinColumn(name = "topology_id", nullable = false, foreignKey = @ForeignKey(name = "fk_scenarios_topology")) + public Topology topology; + + /** + * Operational phenomena activated in the scenario. + */ + @Type(type = "io.hypersistence.utils.hibernate.type.json.JsonType") + @Column(columnDefinition = "jsonb", nullable = false, updatable = false) + public OperationalPhenomena phenomena; + + /** + * The name of the VM scheduler used in the scenario. + */ + @Column(name = "scheduler_name", nullable = false, updatable = false) + public String schedulerName; + + /** + * The {@link Job} associated with the scenario. + */ + @OneToMany( + cascade = {CascadeType.ALL}, + mappedBy = "scenario", + fetch = FetchType.LAZY) + public List<Job> jobs = new ArrayList<>(); + + /** + * Construct a {@link Scenario} object. + */ + public Scenario( + Project project, + Portfolio portfolio, + int number, + String name, + Workload workload, + Topology topology, + OperationalPhenomena phenomena, + String schedulerName) { + this.project = project; + this.portfolio = portfolio; + this.number = number; + this.name = name; + this.workload = workload; + this.topology = topology; + this.phenomena = phenomena; + this.schedulerName = schedulerName; + } + + /** + * JPA constructor + */ + protected Scenario() {} + + /** + * Find all {@link Scenario}s that belong to the specified project + * + * @param projectId The unique identifier of the project. + * @return The query of scenarios that belong to the specified project. + */ + public static PanacheQuery<Scenario> findByProject(long projectId) { + return find("#Scenario.findByProject", Parameters.with("projectId", projectId)); + } + + /** + * Find all {@link Scenario}s that belong to the specified portfolio. + * + * @param projectId The unique identifier of the project. + * @param number The number of the portfolio. + * @return The query of scenarios that belong to the specified project and portfolio.. + */ + public static PanacheQuery<Scenario> findByPortfolio(long projectId, int number) { + return find( + "#Scenario.findByPortfolio", + Parameters.with("projectId", projectId).and("number", number)); + } + + /** + * Find the {@link Scenario} with the specified <code>number</code> belonging to the specified project. + * + * @param projectId The unique identifier of the project. + * @param number The number of the scenario. + * @return The scenario or <code>null</code> if it does not exist. + */ + public static Scenario findByProject(long projectId, int number) { + return find( + "#Scenario.findOneByProject", + Parameters.with("projectId", projectId).and("number", number)) + .firstResult(); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Topology.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Topology.java new file mode 100644 index 00000000..05a1ac12 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Topology.java @@ -0,0 +1,139 @@ +/* + * 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.server.model; + +import io.quarkus.hibernate.orm.panache.PanacheEntity; +import io.quarkus.hibernate.orm.panache.PanacheQuery; +import io.quarkus.panache.common.Parameters; +import java.time.Instant; +import java.util.List; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import org.hibernate.annotations.Type; +import org.opendc.web.proto.Room; + +/** + * A datacenter design in OpenDC. + */ +@Entity +@Table( + name = "topologies", + uniqueConstraints = { + @UniqueConstraint( + name = "uk_topologies_number", + columnNames = {"project_id", "number"}) + }, + indexes = {@Index(name = "ux_topologies_number", columnList = "project_id, number")}) +@NamedQueries({ + @NamedQuery(name = "Topology.findByProject", query = "SELECT t FROM Topology t WHERE t.project.id = :projectId"), + @NamedQuery( + name = "Topology.findOneByProject", + query = "SELECT t FROM Topology t WHERE t.project.id = :projectId AND t.number = :number") +}) +public class Topology extends PanacheEntity { + /** + * The {@link Project} to which the topology belongs. + */ + @ManyToOne(optional = false) + @JoinColumn(name = "project_id", nullable = false) + public Project project; + + /** + * Unique number of the topology for the project. + */ + @Column(nullable = false) + public int number; + + /** + * The name of the topology. + */ + @Column(nullable = false) + public String name; + + /** + * The instant at which the topology was created. + */ + @Column(name = "created_at", nullable = false, updatable = false) + public Instant createdAt; + + /** + * The instant at which the topology was updated. + */ + @Column(name = "updated_at", nullable = false) + public Instant updatedAt; + + /** + * Datacenter design in JSON + */ + @Type(type = "io.hypersistence.utils.hibernate.type.json.JsonType") + @Column(columnDefinition = "jsonb", nullable = false) + public List<Room> rooms; + + /** + * Construct a {@link Topology} object. + */ + public Topology(Project project, int number, String name, Instant createdAt, List<Room> rooms) { + this.project = project; + this.number = number; + this.name = name; + this.createdAt = createdAt; + this.updatedAt = createdAt; + this.rooms = rooms; + } + + /** + * JPA constructor + */ + protected Topology() {} + + /** + * Find all [Topology]s that belong to [project][projectId]. + * + * @param projectId The unique identifier of the project. + * @return The query of topologies that belong to the specified project. + */ + public static PanacheQuery<Topology> findByProject(long projectId) { + return find("#Topology.findByProject", Parameters.with("projectId", projectId)); + } + + /** + * Find the [Topology] with the specified [number] belonging to [project][projectId]. + * + * @param projectId The unique identifier of the project. + * @param number The number of the topology. + * @return The topology or `null` if it does not exist. + */ + public static Topology findByProject(long projectId, int number) { + return find( + "#Topology.findOneByProject", + Parameters.with("projectId", projectId).and("number", number)) + .firstResult(); + } +} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Trace.kt b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Trace.java index 14a88c5a..36d27abc 100644 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Trace.kt +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Trace.java @@ -20,44 +20,53 @@ * SOFTWARE. */ -package org.opendc.web.server.model +package org.opendc.web.server.model; -import javax.persistence.Column -import javax.persistence.Entity -import javax.persistence.Id -import javax.persistence.NamedQueries -import javax.persistence.NamedQuery -import javax.persistence.Table +import io.quarkus.hibernate.orm.panache.PanacheEntityBase; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; /** * A workload trace available for simulation. - * - * @param id The unique identifier of the trace. - * @param name The name of the trace. - * @param type The type of trace. */ @Entity @Table(name = "traces") -@NamedQueries( - value = [ - NamedQuery( - name = "Trace.findAll", - query = "SELECT t FROM Trace t" - ) - ] -) -class Trace( +public class Trace extends PanacheEntityBase { + /** + * The unique identifier of the trace. + */ @Id - val id: String, + public String id; + /** + * The name of the trace. + */ @Column(nullable = false, updatable = false) - val name: String, + public String name; + /** + * The type of trace. + */ @Column(nullable = false, updatable = false) - val type: String -) { + public String type; + + /** + * Construct a {@link Trace}. + * + * @param id The unique identifier of the trace. + * @param name The name of the trace. + * @param type The type of trace. + */ + public Trace(String id, String name, String type) { + this.id = id; + this.name = name; + this.type = type; + } + /** - * Return a string representation of this trace. + * JPA constructor. */ - override fun toString(): String = "Trace[id=$id,name=$name,type=$type]" + protected Trace() {} } diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/UserAccounting.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/UserAccounting.java new file mode 100644 index 00000000..fda4302f --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/UserAccounting.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2023 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.server.model; + +import io.quarkus.hibernate.orm.panache.PanacheEntityBase; +import io.quarkus.panache.common.Parameters; +import java.time.LocalDate; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; + +/** + * Entity to track the number of simulation minutes used by a user. + */ +@Entity +@Table(name = "user_accounting") +@NamedQueries({ + @NamedQuery( + name = "UserAccounting.consumeBudget", + query = + """ + UPDATE UserAccounting a + SET a.simulationTime = a.simulationTime + :seconds + WHERE a.userId = :userId AND a.periodEnd = :periodEnd + """), + @NamedQuery( + name = "UserAccounting.resetBudget", + query = + """ + UPDATE UserAccounting a + SET a.periodEnd = :periodEnd, a.simulationTime = :seconds + WHERE a.userId = :userId AND a.periodEnd = :oldPeriodEnd + """) +}) +public class UserAccounting extends PanacheEntityBase { + /** + * User to which this object belongs. + */ + @Id + @Column(name = "user_id", nullable = false) + public String userId; + + /** + * The end of the accounting period. + */ + @Column(name = "period_end", nullable = false) + public LocalDate periodEnd; + + /** + * The number of simulation seconds to be used per accounting period. + */ + @Column(name = "simulation_time_budget", nullable = false) + public int simulationTimeBudget; + + /** + * The number of simulation seconds used in this period. This number should reset once the accounting period has + * been reached. + */ + @Column(name = "simulation_time", nullable = false) + public int simulationTime = 0; + + /** + * Construct a new {@link UserAccounting} object. + * + * @param userId The identifier of the user that this object belongs to. + * @param periodEnd The end of the accounting period. + * @param simulationTimeBudget The number of simulation seconds available per accounting period. + */ + public UserAccounting(String userId, LocalDate periodEnd, int simulationTimeBudget) { + this.userId = userId; + this.periodEnd = periodEnd; + this.simulationTimeBudget = simulationTimeBudget; + } + + /** + * JPA constructor. + */ + protected UserAccounting() {} + + /** + * Return the {@link UserAccounting} object associated with the specified user id. + */ + public static UserAccounting findByUser(String userId) { + return findById(userId); + } + + /** + * Create a new {@link UserAccounting} object and persist it to the database. + * + * @param userId The identifier of the user that this object belongs to. + * @param periodEnd The end of the accounting period. + * @param simulationTimeBudget The number of simulation seconds available per accounting period. + * @param simulationTime The initial simulation time that has been consumed. + */ + public static UserAccounting create( + String userId, LocalDate periodEnd, int simulationTimeBudget, int simulationTime) { + UserAccounting newAccounting = new UserAccounting(userId, periodEnd, simulationTimeBudget); + newAccounting.simulationTime = simulationTime; + newAccounting.persistAndFlush(); + return newAccounting; + } + + /** + * Atomically consume the budget for this {@link UserAccounting} object. + * + * @param seconds The number of seconds to consume from the user. + * @return <code>true</code> when the update succeeded, <code>false</code> when there was a conflict. + */ + public boolean consumeBudget(int seconds) { + long count = update( + "#UserAccounting.consumeBudget", + Parameters.with("userId", userId).and("periodEnd", periodEnd).and("seconds", seconds)); + return count > 0; + } + + /** + * Atomically reset the budget for this {@link UserAccounting} object. + * + * @param periodEnd The new end period for the budget. + * @param seconds The number of seconds that have already been consumed. + * @return <code>true</code> when the update succeeded`, <code>false</code> when there was a conflict. + */ + public boolean resetBudget(LocalDate periodEnd, int seconds) { + long count = update( + "#UserAccounting.resetBudget", + Parameters.with("userId", userId) + .and("oldPeriodEnd", this.periodEnd) + .and("periodEnd", periodEnd) + .and("seconds", seconds)); + return count > 0; + } + + /** + * Determine whether the user has any remaining simulation budget. + * + * @return <code>true</code> when the user still has budget left, <code>false</code> otherwise. + */ + public boolean hasSimulationBudget() { + var today = LocalDate.now(); + + // The accounting period must be over or there must be budget remaining. + return !today.isBefore(periodEnd) || simulationTimeBudget > simulationTime; + } +} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Workload.kt b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Workload.java index 9c59dc25..129fb0c5 100644 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Workload.kt +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Workload.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 AtLarge Research + * Copyright (c) 2023 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 @@ -20,20 +20,42 @@ * SOFTWARE. */ -package org.opendc.web.server.model +package org.opendc.web.server.model; -import javax.persistence.Column -import javax.persistence.Embeddable -import javax.persistence.ManyToOne +import javax.persistence.Column; +import javax.persistence.Embeddable; +import javax.persistence.ManyToOne; /** - * Specification of the workload for a [Scenario]. + * Specification of the workload for a {@link Scenario} */ @Embeddable -class Workload( +public class Workload { + /** + * The {@link Trace} that the workload runs. + */ @ManyToOne(optional = false) - val trace: Trace, + public Trace trace; + /** + * The percentage of the trace that should be sampled. + */ @Column(name = "sampling_fraction", nullable = false, updatable = false) - val samplingFraction: Double -) + public double samplingFraction; + + /** + * Construct a {@link Workload} object. + * + * @param trace The {@link Trace} to run as workload. + * @param samplingFraction The percentage of the workload to sample. + */ + public Workload(Trace trace, double samplingFraction) { + this.trace = trace; + this.samplingFraction = samplingFraction; + } + + /** + * JPA constructor. + */ + protected Workload() {} +} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/TraceService.kt b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/BaseProtocol.java index bd14950c..44d2d569 100644 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/TraceService.kt +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/BaseProtocol.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 AtLarge Research + * Copyright (c) 2023 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 @@ -20,29 +20,31 @@ * SOFTWARE. */ -package org.opendc.web.server.service +package org.opendc.web.server.rest; -import org.opendc.web.proto.Trace -import org.opendc.web.server.repository.TraceRepository -import javax.enterprise.context.ApplicationScoped -import javax.inject.Inject +import org.opendc.web.server.model.Trace; +import org.opendc.web.server.model.Workload; /** - * Service for managing [Trace]s. + * DTO-conversions for the base protocol. */ -@ApplicationScoped -class TraceService @Inject constructor(private val repository: TraceRepository) { +public final class BaseProtocol { /** - * Obtain all available workload traces. + * Private constructor to prevent instantiation of class. */ - fun findAll(): List<Trace> { - return repository.findAll().map { it.toUserDto() } + private BaseProtocol() {} + + /** + * Convert a {@link Workload} entity into a DTO. + */ + public static org.opendc.web.proto.Workload toDto(Workload workload) { + return new org.opendc.web.proto.Workload(toDto(workload.trace), workload.samplingFraction); } /** - * Obtain a workload trace by identifier. + * Convert a {@link Trace] entity into a {@link org.opendc.web.proto.Trace} DTO. */ - fun findById(id: String): Trace? { - return repository.findOne(id)?.toUserDto() + public static org.opendc.web.proto.Trace toDto(Trace trace) { + return new org.opendc.web.proto.Trace(trace.id, trace.name, trace.type); } } diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/SchedulerResource.kt b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/SchedulerResource.java index 919b25fc..0fd58182 100644 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/SchedulerResource.kt +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/SchedulerResource.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 AtLarge Research + * Copyright (c) 2023 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 @@ -20,29 +20,33 @@ * SOFTWARE. */ -package org.opendc.web.server.rest +package org.opendc.web.server.rest; -import javax.ws.rs.GET -import javax.ws.rs.Path +import java.util.List; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; /** * A resource representing the available schedulers that can be used during experiments. */ +@Produces("application/json") @Path("/schedulers") -class SchedulerResource { +public final class SchedulerResource { /** * Obtain all available schedulers. */ @GET - fun getAll() = listOf( - "mem", - "mem-inv", - "core-mem", - "core-mem-inv", - "active-servers", - "active-servers-inv", - "provisioned-cores", - "provisioned-cores-inv", - "random" - ) + public List<String> getAll() { + return List.of( + "mem", + "mem-inv", + "core-mem", + "core-mem-inv", + "active-servers", + "active-servers-inv", + "provisioned-cores", + "provisioned-cores-inv", + "random"); + } } diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/TraceResource.kt b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/TraceResource.java index a33bd8f1..7316c93f 100644 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/TraceResource.kt +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/TraceResource.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 AtLarge Research + * Copyright (c) 2023 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 @@ -20,27 +20,30 @@ * SOFTWARE. */ -package org.opendc.web.server.rest +package org.opendc.web.server.rest; -import org.opendc.web.proto.Trace -import org.opendc.web.server.service.TraceService -import javax.inject.Inject -import javax.ws.rs.GET -import javax.ws.rs.Path -import javax.ws.rs.PathParam -import javax.ws.rs.WebApplicationException +import java.util.List; +import java.util.stream.Stream; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import org.opendc.web.server.model.Trace; /** * A resource representing the workload traces available in the OpenDC instance. */ +@Produces("application/json") @Path("/traces") -class TraceResource @Inject constructor(private val traceService: TraceService) { +public final class TraceResource { /** * Obtain all available traces. */ @GET - fun getAll(): List<Trace> { - return traceService.findAll() + public List<org.opendc.web.proto.Trace> getAll() { + Stream<Trace> entities = Trace.streamAll(); + return entities.map(TraceResource::toDto).toList(); } /** @@ -48,7 +51,20 @@ class TraceResource @Inject constructor(private val traceService: TraceService) */ @GET @Path("{id}") - fun get(@PathParam("id") id: String): Trace { - return traceService.findById(id) ?: throw WebApplicationException("Trace not found", 404) + public org.opendc.web.proto.Trace get(@PathParam("id") String id) { + Trace trace = Trace.findById(id); + + if (trace == null) { + throw new WebApplicationException("Trace not found", 404); + } + + return toDto(trace); + } + + /** + * Convert a {@link Trace] entity into a {@link org.opendc.web.proto.Trace} DTO. + */ + public static org.opendc.web.proto.Trace toDto(Trace trace) { + return new org.opendc.web.proto.Trace(trace.id, trace.name, trace.type); } } diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/error/MissingKotlinParameterExceptionMapper.kt b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/error/MissingKotlinParameterExceptionMapper.java index e50917aa..3b6be42e 100644 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/error/MissingKotlinParameterExceptionMapper.kt +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/error/MissingKotlinParameterExceptionMapper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 AtLarge Research + * Copyright (c) 2023 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 @@ -20,24 +20,27 @@ * SOFTWARE. */ -package org.opendc.web.server.rest.error +package org.opendc.web.server.rest.error; -import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException -import org.opendc.web.proto.ProtocolError -import javax.ws.rs.core.MediaType -import javax.ws.rs.core.Response -import javax.ws.rs.ext.ExceptionMapper -import javax.ws.rs.ext.Provider +import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; +import org.opendc.web.proto.ProtocolError; /** * An [ExceptionMapper] for [MissingKotlinParameterException] thrown by Jackson. */ @Provider -class MissingKotlinParameterExceptionMapper : ExceptionMapper<MissingKotlinParameterException> { - override fun toResponse(exception: MissingKotlinParameterException): Response { +public final class MissingKotlinParameterExceptionMapper implements ExceptionMapper<MissingKotlinParameterException> { + @Override + public Response toResponse(MissingKotlinParameterException exception) { return Response.status(Response.Status.BAD_REQUEST) - .entity(ProtocolError(Response.Status.BAD_REQUEST.statusCode, "Field '${exception.parameter.name}' is missing from body.")) - .type(MediaType.APPLICATION_JSON) - .build() + .entity(new ProtocolError( + Response.Status.BAD_REQUEST.getStatusCode(), + "Field " + exception.getParameter().getName() + " is missing from body.")) + .type(MediaType.APPLICATION_JSON) + .build(); } } diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/error/WebApplicationExceptionMapper.kt b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/error/WebApplicationExceptionMapper.java index aa046abf..ad1bb05e 100644 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/error/WebApplicationExceptionMapper.kt +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/error/WebApplicationExceptionMapper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 AtLarge Research + * Copyright (c) 2023 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 @@ -20,26 +20,32 @@ * SOFTWARE. */ -package org.opendc.web.server.rest.error +package org.opendc.web.server.rest.error; -import org.opendc.web.proto.ProtocolError -import javax.ws.rs.WebApplicationException -import javax.ws.rs.core.MediaType -import javax.ws.rs.core.Response -import javax.ws.rs.ext.ExceptionMapper -import javax.ws.rs.ext.Provider +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; +import org.opendc.web.proto.ProtocolError; /** - * Helper class to transform a [WebApplicationException] into an JSON error response. + * Helper class to transform a {@link WebApplicationException} into an JSON error response. */ @Provider -class WebApplicationExceptionMapper : ExceptionMapper<WebApplicationException> { - override fun toResponse(exception: WebApplicationException): Response { - val code = exception.response.status +public final class WebApplicationExceptionMapper implements ExceptionMapper<WebApplicationException> { + @Override + public Response toResponse(WebApplicationException exception) { + int code = exception.getResponse().getStatus(); + + String message = exception.getMessage(); + if (message == null) { + message = "Unknown error"; + } return Response.status(code) - .entity(ProtocolError(code, exception.message ?: "Unknown error")) - .type(MediaType.APPLICATION_JSON) - .build() + .entity(new ProtocolError(code, message)) + .type(MediaType.APPLICATION_JSON) + .build(); } } diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/JobResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/JobResource.java new file mode 100644 index 00000000..dff52526 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/JobResource.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2023 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.server.rest.runner; + +import java.util.List; +import javax.annotation.security.RolesAllowed; +import javax.transaction.Transactional; +import javax.validation.Valid; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import org.opendc.web.proto.JobState; +import org.opendc.web.server.model.Job; +import org.opendc.web.server.service.JobService; + +/** + * A resource representing the available simulation jobs. + */ +@Produces("application/json") +@Path("/jobs") +@RolesAllowed("runner") +public final class JobResource { + /** + * The {@link JobService} for helping manage the job lifecycle. + */ + private final JobService jobService; + + /** + * Construct a {@link JobResource} instance. + * + * @param jobService The {@link JobService} for managing the job lifecycle. + */ + public JobResource(JobService jobService) { + this.jobService = jobService; + } + + /** + * Obtain all pending simulation jobs. + */ + @GET + public List<org.opendc.web.proto.runner.Job> queryPending() { + return Job.findByState(JobState.PENDING).list().stream() + .map(RunnerProtocol::toDto) + .toList(); + } + + /** + * Get a job by identifier. + */ + @GET + @Path("{job}") + public org.opendc.web.proto.runner.Job get(@PathParam("job") long id) { + Job job = Job.findById(id); + + if (job == null) { + throw new WebApplicationException("Job not found", 404); + } + + return RunnerProtocol.toDto(job); + } + + /** + * Atomically update the state of a job. + */ + @POST + @Path("{job}") + @Consumes("application/json") + @Transactional + public org.opendc.web.proto.runner.Job update( + @PathParam("job") long id, @Valid org.opendc.web.proto.runner.Job.Update update) { + Job job = Job.findById(id); + if (job == null) { + throw new WebApplicationException("Job not found", 404); + } + + try { + jobService.updateJob(job, update.getState(), update.getRuntime(), update.getResults()); + } catch (IllegalArgumentException e) { + throw new WebApplicationException(e, 400); + } catch (IllegalStateException e) { + throw new WebApplicationException(e, 409); + } + + return RunnerProtocol.toDto(job); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/RunnerProtocol.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/RunnerProtocol.java new file mode 100644 index 00000000..6bf65d97 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/RunnerProtocol.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2023 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.server.rest.runner; + +import org.opendc.web.server.model.Job; +import org.opendc.web.server.model.Portfolio; +import org.opendc.web.server.model.Scenario; +import org.opendc.web.server.model.Topology; +import org.opendc.web.server.rest.BaseProtocol; + +/** + * DTO-conversions for the runner protocol. + */ +public final class RunnerProtocol { + /** + * Private constructor to prevent instantiation of class. + */ + private RunnerProtocol() {} + + /** + * Convert a {@link Job} into a runner-facing DTO. + */ + public static org.opendc.web.proto.runner.Job toDto(Job job) { + return new org.opendc.web.proto.runner.Job( + job.id, toDto(job.scenario), job.state, job.createdAt, job.updatedAt, job.runtime, job.results); + } + + /** + * Convert a {@link Scenario} into a runner-facing DTO. + */ + public static org.opendc.web.proto.runner.Scenario toDto(Scenario scenario) { + return new org.opendc.web.proto.runner.Scenario( + scenario.id, + scenario.number, + toDto(scenario.portfolio), + scenario.name, + BaseProtocol.toDto(scenario.workload), + toDto(scenario.topology), + scenario.phenomena, + scenario.schedulerName); + } + + /** + * Convert a {@link Portfolio} into a runner-facing DTO. + */ + public static org.opendc.web.proto.runner.Portfolio toDto(Portfolio portfolio) { + return new org.opendc.web.proto.runner.Portfolio( + portfolio.id, portfolio.number, portfolio.name, portfolio.targets); + } + + /** + * Convert a {@link Topology} into a runner-facing DTO. + */ + public static org.opendc.web.proto.runner.Topology toDto(Topology topology) { + return new org.opendc.web.proto.runner.Topology( + topology.id, topology.number, topology.name, topology.rooms, topology.createdAt, topology.updatedAt); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioResource.java new file mode 100644 index 00000000..d1fc980d --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioResource.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2023 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.server.rest.user; + +import io.quarkus.security.identity.SecurityIdentity; +import java.time.Instant; +import java.util.List; +import javax.annotation.security.RolesAllowed; +import javax.transaction.Transactional; +import javax.validation.Valid; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import org.opendc.web.server.model.Portfolio; +import org.opendc.web.server.model.ProjectAuthorization; + +/** + * A resource representing the portfolios of a project. + */ +@Produces("application/json") +@Path("/projects/{project}/portfolios") +@RolesAllowed("openid") +public final class PortfolioResource { + /** + * The identity of the current user. + */ + private final SecurityIdentity identity; + + /** + * Construct a {@link PortfolioResource}. + * + * @param identity The {@link SecurityIdentity} of the current user. + */ + public PortfolioResource(SecurityIdentity identity) { + this.identity = identity; + } + + /** + * Get all portfolios that belong to the specified project. + */ + @GET + public List<org.opendc.web.proto.user.Portfolio> getAll(@PathParam("project") long projectId) { + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + return List.of(); + } + + return Portfolio.findByProject(projectId).list().stream() + .map((p) -> UserProtocol.toDto(p, auth)) + .toList(); + } + + /** + * Create a portfolio for this project. + */ + @POST + @Transactional + @Consumes("application/json") + public org.opendc.web.proto.user.Portfolio create( + @PathParam("project") long projectId, @Valid org.opendc.web.proto.user.Portfolio.Create request) { + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + throw new WebApplicationException("Project not found", 404); + } else if (!auth.canEdit()) { + throw new WebApplicationException("Not permitted to edit project", 403); + } + + var now = Instant.now(); + var project = auth.project; + int number = project.allocatePortfolio(now); + + Portfolio portfolio = new Portfolio(project, number, request.getName(), request.getTargets()); + + project.portfolios.add(portfolio); + portfolio.persist(); + + return UserProtocol.toDto(portfolio, auth); + } + + /** + * Obtain a portfolio by its identifier. + */ + @GET + @Path("{portfolio}") + public org.opendc.web.proto.user.Portfolio get( + @PathParam("project") long projectId, @PathParam("portfolio") int number) { + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + throw new WebApplicationException("Portfolio not found", 404); + } + + Portfolio portfolio = Portfolio.findByProject(projectId, number); + + if (portfolio == null) { + throw new WebApplicationException("Portfolio not found", 404); + } + + return UserProtocol.toDto(portfolio, auth); + } + + /** + * Delete a portfolio. + */ + @DELETE + @Path("{portfolio}") + @Transactional + public org.opendc.web.proto.user.Portfolio delete( + @PathParam("project") long projectId, @PathParam("portfolio") int number) { + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + throw new WebApplicationException("Portfolio not found", 404); + } else if (!auth.canEdit()) { + throw new WebApplicationException("Not permitted to edit project", 403); + } + + Portfolio entity = Portfolio.findByProject(projectId, number); + if (entity == null) { + throw new WebApplicationException("Portfolio not found", 404); + } + + entity.delete(); + return UserProtocol.toDto(entity, auth); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioScenarioResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioScenarioResource.java new file mode 100644 index 00000000..a058cd31 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioScenarioResource.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2023 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.server.rest.user; + +import io.quarkus.security.identity.SecurityIdentity; +import java.time.Instant; +import java.util.List; +import javax.annotation.security.RolesAllowed; +import javax.transaction.Transactional; +import javax.validation.Valid; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import org.opendc.web.proto.JobState; +import org.opendc.web.server.model.Job; +import org.opendc.web.server.model.Portfolio; +import org.opendc.web.server.model.ProjectAuthorization; +import org.opendc.web.server.model.Scenario; +import org.opendc.web.server.model.Topology; +import org.opendc.web.server.model.Trace; +import org.opendc.web.server.model.Workload; +import org.opendc.web.server.service.UserAccountingService; + +/** + * A resource representing the scenarios of a portfolio. + */ +@Path("/projects/{project}/portfolios/{portfolio}/scenarios") +@RolesAllowed("openid") +@Produces("application/json") +public final class PortfolioScenarioResource { + /** + * The service for managing the user accounting. + */ + private final UserAccountingService accountingService; + + /** + * The identity of the current user. + */ + private final SecurityIdentity identity; + + /** + * Construct a {@link PortfolioScenarioResource}. + * + * @param accountingService The {@link UserAccountingService} instance to use. + * @param identity The {@link SecurityIdentity} of the current user. + */ + public PortfolioScenarioResource(UserAccountingService accountingService, SecurityIdentity identity) { + this.accountingService = accountingService; + this.identity = identity; + } + + /** + * Get all scenarios that belong to the specified portfolio. + */ + @GET + public List<org.opendc.web.proto.user.Scenario> get( + @PathParam("project") long projectId, @PathParam("portfolio") int portfolioNumber) { + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + return List.of(); + } + + return org.opendc.web.server.model.Scenario.findByPortfolio(projectId, portfolioNumber).list().stream() + .map((s) -> UserProtocol.toDto(s, auth)) + .toList(); + } + + /** + * Create a scenario for this portfolio. + */ + @POST + @Transactional + @Consumes("application/json") + public org.opendc.web.proto.user.Scenario create( + @PathParam("project") long projectId, + @PathParam("portfolio") int portfolioNumber, + @Valid org.opendc.web.proto.user.Scenario.Create request) { + // User must have access to project + String userId = identity.getPrincipal().getName(); + ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); + + if (auth == null) { + throw new WebApplicationException("Portfolio not found", 404); + } else if (!auth.canEdit()) { + throw new WebApplicationException("Not permitted to edit project", 403); + } + + Portfolio portfolio = Portfolio.findByProject(projectId, portfolioNumber); + + if (portfolio == null) { + throw new WebApplicationException("Portfolio not found", 404); + } + + Topology topology = Topology.findByProject(projectId, (int) request.getTopology()); + if (topology == null) { + throw new WebApplicationException("Referred topology does not exist", 400); + } + + Trace trace = Trace.findById(request.getWorkload().getTrace()); + if (trace == null) { + throw new WebApplicationException("Referred trace does not exist", 400); + } + + var now = Instant.now(); + var project = auth.project; + int number = project.allocateScenario(now); + + Scenario scenario = new Scenario( + project, + portfolio, + number, + request.getName(), + new Workload(trace, request.getWorkload().getSamplingFraction()), + topology, + request.getPhenomena(), + request.getSchedulerName()); + scenario.persist(); + + Job job = new Job(scenario, userId, now, portfolio.targets.getRepeats()); + job.persist(); + + // Fail the job if there is not enough budget for the simulation + if (!accountingService.hasSimulationBudget(userId)) { + job.state = JobState.FAILED; + } + + scenario.jobs.add(job); + portfolio.scenarios.add(scenario); + + return UserProtocol.toDto(scenario, auth); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/ProjectResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/ProjectResource.java new file mode 100644 index 00000000..da47c3ff --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/ProjectResource.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2023 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.server.rest.user; + +import io.quarkus.security.identity.SecurityIdentity; +import java.time.Instant; +import java.util.List; +import javax.annotation.security.RolesAllowed; +import javax.transaction.Transactional; +import javax.validation.Valid; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import org.opendc.web.proto.user.ProjectRole; +import org.opendc.web.server.model.Project; +import org.opendc.web.server.model.ProjectAuthorization; + +/** + * A resource representing the created projects. + */ +@Produces("application/json") +@Path("/projects") +@RolesAllowed("openid") +public final class ProjectResource { + /** + * The identity of the current user. + */ + private final SecurityIdentity identity; + + /** + * Construct a {@link ProjectResource}. + * + * @param identity The {@link SecurityIdentity} of the current user. + */ + public ProjectResource(SecurityIdentity identity) { + this.identity = identity; + } + + /** + * Obtain all the projects of the current user. + */ + @GET + public List<org.opendc.web.proto.user.Project> getAll() { + return ProjectAuthorization.findByUser(identity.getPrincipal().getName()).list().stream() + .map(UserProtocol::toDto) + .toList(); + } + + /** + * Create a new project for the current user. + */ + @POST + @Transactional + @Consumes("application/json") + public org.opendc.web.proto.user.Project create(@Valid org.opendc.web.proto.user.Project.Create request) { + Instant now = Instant.now(); + Project entity = new Project(request.getName(), now); + entity.persist(); + + ProjectAuthorization authorization = + new ProjectAuthorization(entity, identity.getPrincipal().getName(), ProjectRole.OWNER); + + entity.authorizations.add(authorization); + authorization.persist(); + + return UserProtocol.toDto(authorization); + } + + /** + * Obtain a single project by its identifier. + */ + @GET + @Path("{project}") + public org.opendc.web.proto.user.Project get(@PathParam("project") long id) { + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), id); + + if (auth == null) { + throw new WebApplicationException("Project not found", 404); + } + + return UserProtocol.toDto(auth); + } + + /** + * Delete a project. + */ + @DELETE + @Path("{project}") + @Transactional + public org.opendc.web.proto.user.Project delete(@PathParam("project") long id) { + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), id); + + if (auth == null) { + throw new WebApplicationException("Project not found", 404); + } else if (!auth.canDelete()) { + throw new WebApplicationException("Not allowed to delete project", 403); + } + + auth.project.updatedAt = Instant.now(); + org.opendc.web.proto.user.Project project = UserProtocol.toDto(auth); + auth.project.delete(); + return project; + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/ScenarioResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/ScenarioResource.java new file mode 100644 index 00000000..cf933c32 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/ScenarioResource.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2023 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.server.rest.user; + +import io.quarkus.security.identity.SecurityIdentity; +import java.util.List; +import javax.annotation.security.RolesAllowed; +import javax.transaction.Transactional; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import org.opendc.web.server.model.ProjectAuthorization; +import org.opendc.web.server.model.Scenario; + +/** + * A resource representing the scenarios of a portfolio. + */ +@Produces("application/json") +@Path("/projects/{project}/scenarios") +@RolesAllowed("openid") +public final class ScenarioResource { + /** + * The identity of the current user. + */ + private final SecurityIdentity identity; + + /** + * Construct a {@link ScenarioResource}. + * + * @param identity The {@link SecurityIdentity} of the current user. + */ + public ScenarioResource(SecurityIdentity identity) { + this.identity = identity; + } + + /** + * Obtain the scenarios belonging to a project. + */ + @GET + public List<org.opendc.web.proto.user.Scenario> getAll(@PathParam("project") long projectId) { + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + throw new WebApplicationException("Project not found", 404); + } + + return Scenario.findByProject(projectId).list().stream() + .map((s) -> UserProtocol.toDto(s, auth)) + .toList(); + } + + /** + * Obtain a scenario by its identifier. + */ + @GET + @Path("{scenario}") + public org.opendc.web.proto.user.Scenario get( + @PathParam("project") long projectId, @PathParam("scenario") int number) { + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + throw new WebApplicationException("Project not found", 404); + } + + Scenario scenario = Scenario.findByProject(projectId, number); + + if (scenario == null) { + throw new WebApplicationException("Scenario not found", 404); + } + + return UserProtocol.toDto(scenario, auth); + } + + /** + * Delete a scenario. + */ + @DELETE + @Path("{scenario}") + @Transactional + public org.opendc.web.proto.user.Scenario delete( + @PathParam("project") long projectId, @PathParam("scenario") int number) { + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + throw new WebApplicationException("Project not found", 404); + } else if (!auth.canEdit()) { + throw new WebApplicationException("Not permitted to edit project", 403); + } + + Scenario entity = Scenario.findByProject(projectId, number); + if (entity == null) { + throw new WebApplicationException("Scenario not found", 404); + } + + entity.delete(); + return UserProtocol.toDto(entity, auth); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/TopologyResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/TopologyResource.java new file mode 100644 index 00000000..2b66b64b --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/TopologyResource.java @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2023 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.server.rest.user; + +import io.quarkus.security.identity.SecurityIdentity; +import java.time.Instant; +import java.util.List; +import javax.annotation.security.RolesAllowed; +import javax.transaction.Transactional; +import javax.validation.Valid; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import org.opendc.web.server.model.Project; +import org.opendc.web.server.model.ProjectAuthorization; +import org.opendc.web.server.model.Topology; + +/** + * A resource representing the constructed datacenter topologies. + */ +@Produces("application/json") +@Path("/projects/{project}/topologies") +@RolesAllowed("openid") +public final class TopologyResource { + /** + * The identity of the current user. + */ + private final SecurityIdentity identity; + + /** + * Construct a {@link TopologyResource}. + * + * @param identity The {@link SecurityIdentity} of the current user. + */ + public TopologyResource(SecurityIdentity identity) { + this.identity = identity; + } + + /** + * Get all topologies that belong to the specified project. + */ + @GET + public List<org.opendc.web.proto.user.Topology> getAll(@PathParam("project") long projectId) { + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + return List.of(); + } + + return Topology.findByProject(projectId).list().stream() + .map((t) -> UserProtocol.toDto(t, auth)) + .toList(); + } + + /** + * Create a topology for this project. + */ + @POST + @Consumes("application/json") + @Transactional + public org.opendc.web.proto.user.Topology create( + @PathParam("project") long projectId, @Valid org.opendc.web.proto.user.Topology.Create request) { + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + throw new WebApplicationException("Topology not found", 404); + } else if (!auth.canEdit()) { + throw new WebApplicationException("Not permitted to edit project", 403); + } + + Instant now = Instant.now(); + Project project = auth.project; + int number = project.allocateTopology(now); + + Topology topology = new Topology(project, number, request.getName(), now, request.getRooms()); + + project.topologies.add(topology); + topology.persist(); + + return UserProtocol.toDto(topology, auth); + } + + /** + * Obtain a topology by its number. + */ + @GET + @Path("{topology}") + public org.opendc.web.proto.user.Topology get( + @PathParam("project") long projectId, @PathParam("topology") int number) { + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + throw new WebApplicationException("Topology not found", 404); + } + + Topology topology = Topology.findByProject(projectId, number); + + if (topology == null) { + throw new WebApplicationException("Topology not found", 404); + } + + return UserProtocol.toDto(topology, auth); + } + + /** + * Update the specified topology by its number. + */ + @PUT + @Path("{topology}") + @Consumes("application/json") + @Transactional + public org.opendc.web.proto.user.Topology update( + @PathParam("project") long projectId, + @PathParam("topology") int number, + @Valid org.opendc.web.proto.user.Topology.Update request) { + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + throw new WebApplicationException("Topology not found", 404); + } else if (!auth.canEdit()) { + throw new WebApplicationException("Not permitted to edit project", 403); + } + + Topology entity = Topology.findByProject(projectId, number); + + if (entity == null) { + throw new WebApplicationException("Topology not found", 404); + } + + entity.updatedAt = Instant.now(); + entity.rooms = request.getRooms(); + + return UserProtocol.toDto(entity, auth); + } + + /** + * Delete the specified topology. + */ + @Path("{topology}") + @DELETE + @Transactional + public org.opendc.web.proto.user.Topology delete( + @PathParam("project") long projectId, @PathParam("topology") int number) { + // User must have access to project + ProjectAuthorization auth = + ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId); + + if (auth == null) { + throw new WebApplicationException("Topology not found", 404); + } else if (!auth.canEdit()) { + throw new WebApplicationException("Not permitted to edit project", 403); + } + + Topology entity = Topology.findByProject(projectId, number); + + if (entity == null) { + throw new WebApplicationException("Topology not found", 404); + } + + entity.updatedAt = Instant.now(); + entity.delete(); + return UserProtocol.toDto(entity, auth); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/UserProtocol.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/UserProtocol.java new file mode 100644 index 00000000..8196a9d6 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/UserProtocol.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2023 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.server.rest.user; + +import org.opendc.web.server.model.Job; +import org.opendc.web.server.model.Portfolio; +import org.opendc.web.server.model.Project; +import org.opendc.web.server.model.ProjectAuthorization; +import org.opendc.web.server.model.Scenario; +import org.opendc.web.server.model.Topology; +import org.opendc.web.server.rest.BaseProtocol; + +/** + * DTO-conversions for the user protocol. + */ +public final class UserProtocol { + /** + * Private constructor to prevent instantiation of class. + */ + private UserProtocol() {} + + /** + * Convert a {@link ProjectAuthorization} entity into a {@link Project} DTO. + */ + public static org.opendc.web.proto.user.Project toDto(ProjectAuthorization auth) { + Project project = auth.project; + return new org.opendc.web.proto.user.Project( + project.id, project.name, project.createdAt, project.updatedAt, auth.role); + } + + /** + * Convert a {@link Portfolio} entity into a {@link org.opendc.web.proto.user.Portfolio} DTO. + */ + public static org.opendc.web.proto.user.Portfolio toDto(Portfolio portfolio, ProjectAuthorization auth) { + return new org.opendc.web.proto.user.Portfolio( + portfolio.id, + portfolio.number, + toDto(auth), + portfolio.name, + portfolio.targets, + portfolio.scenarios.stream().map(UserProtocol::toSummaryDto).toList()); + } + + /** + * Convert a {@link Portfolio} entity into a {@link org.opendc.web.proto.user.Portfolio.Summary} DTO. + */ + public static org.opendc.web.proto.user.Portfolio.Summary toSummaryDto(Portfolio portfolio) { + return new org.opendc.web.proto.user.Portfolio.Summary( + portfolio.id, portfolio.number, portfolio.name, portfolio.targets); + } + + /** + * Convert a {@link Topology} entity into a {@link org.opendc.web.proto.user.Topology} DTO. + */ + public static org.opendc.web.proto.user.Topology toDto(Topology topology, ProjectAuthorization auth) { + return new org.opendc.web.proto.user.Topology( + topology.id, + topology.number, + toDto(auth), + topology.name, + topology.rooms, + topology.createdAt, + topology.updatedAt); + } + + /** + * Convert a {@link Topology} entity into a {@link org.opendc.web.proto.user.Topology.Summary} DTO. + */ + public static org.opendc.web.proto.user.Topology.Summary toSummaryDto(Topology topology) { + return new org.opendc.web.proto.user.Topology.Summary( + topology.id, topology.number, topology.name, topology.createdAt, topology.updatedAt); + } + + /** + * Convert a {@link Scenario} entity into a {@link org.opendc.web.proto.user.Scenario} DTO. + */ + public static org.opendc.web.proto.user.Scenario toDto(Scenario scenario, ProjectAuthorization auth) { + return new org.opendc.web.proto.user.Scenario( + scenario.id, + scenario.number, + toDto(auth), + toSummaryDto(scenario.portfolio), + scenario.name, + BaseProtocol.toDto(scenario.workload), + toSummaryDto(scenario.topology), + scenario.phenomena, + scenario.schedulerName, + scenario.jobs.stream().map(UserProtocol::toDto).toList()); + } + + /** + * Convert a {@link Scenario} entity into a {@link org.opendc.web.proto.user.Scenario.Summary} DTO. + */ + public static org.opendc.web.proto.user.Scenario.Summary toSummaryDto(Scenario scenario) { + return new org.opendc.web.proto.user.Scenario.Summary( + scenario.id, + scenario.number, + scenario.name, + BaseProtocol.toDto(scenario.workload), + toSummaryDto(scenario.topology), + scenario.phenomena, + scenario.schedulerName, + scenario.jobs.stream().map(UserProtocol::toDto).toList()); + } + + /** + * Convert a {@link Job} entity into a {@link org.opendc.web.proto.user.Job} DTO. + */ + public static org.opendc.web.proto.user.Job toDto(Job job) { + return new org.opendc.web.proto.user.Job(job.id, job.state, job.createdAt, job.updatedAt, job.results); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/UserResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/UserResource.java new file mode 100644 index 00000000..c3fb2866 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/UserResource.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2023 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.server.rest.user; + +import io.quarkus.security.identity.SecurityIdentity; +import javax.annotation.security.RolesAllowed; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import org.opendc.web.proto.user.User; +import org.opendc.web.proto.user.UserAccounting; +import org.opendc.web.server.service.UserAccountingService; + +/** + * A resource representing the active user. + */ +@Produces("application/json") +@Path("/users") +@RolesAllowed("openid") +public final class UserResource { + /** + * The service for managing the user accounting. + */ + private final UserAccountingService accountingService; + + /** + * The identity of the current user. + */ + private final SecurityIdentity identity; + + /** + * Construct a {@link UserResource}. + * + * @param accountingService The {@link UserAccountingService} instance to use. + * @param identity The {@link SecurityIdentity} of the current user. + */ + public UserResource(UserAccountingService accountingService, SecurityIdentity identity) { + this.accountingService = accountingService; + this.identity = identity; + } + + /** + * Get the current active user data. + */ + @GET + @Path("me") + public User get() { + String userId = identity.getPrincipal().getName(); + UserAccounting accounting = accountingService.getAccounting(userId); + + return new User(userId, accounting); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/JobService.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/JobService.java new file mode 100644 index 00000000..ed0eaf9c --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/JobService.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2023 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.server.service; + +import java.time.Instant; +import java.util.Map; +import javax.enterprise.context.ApplicationScoped; +import org.opendc.web.proto.JobState; +import org.opendc.web.server.model.Job; + +/** + * A service for managing the lifecycle of a job and ensuring that the user does not consume + * too much simulation resources. + */ +@ApplicationScoped +public final class JobService { + /** + * The {@link UserAccountingService} responsible for accounting the simulation time of users. + */ + private final UserAccountingService accountingService; + + /** + * Construct a {@link JobService} instance. + * + * @param accountingService The {@link UserAccountingService} for accounting the simulation time of users. + */ + public JobService(UserAccountingService accountingService) { + this.accountingService = accountingService; + } + + /** + * Update the job state. + * + * @param job The {@link Job} to update. + * @param newState The new state to transition the job to. + * @param runtime The runtime (in seconds) consumed by the simulation jbo so far. + * @param results The results to attach to the job. + * @throws IllegalArgumentException if the state transition is invalid. + * @throws IllegalStateException if someone tries to update the job concurrently. + */ + public void updateJob(Job job, JobState newState, int runtime, Map<String, ?> results) { + JobState state = job.state; + + if (!job.canTransitionTo(newState)) { + throw new IllegalArgumentException("Invalid transition from %s to %s".formatted(state, newState)); + } + + Instant now = Instant.now(); + JobState nextState = newState; + int consumedBudget = Math.min(1, runtime - job.runtime); + + // Check whether the user still has any simulation budget left + if (accountingService.consumeSimulationBudget(job.createdBy, consumedBudget) && nextState == JobState.RUNNING) { + nextState = JobState.FAILED; // User has consumed all their budget; cancel the job + } + + if (!job.updateAtomically(nextState, now, runtime, results)) { + throw new IllegalStateException("Conflicting update"); + } + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/UserAccountingService.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/UserAccountingService.java new file mode 100644 index 00000000..e5003cb4 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/UserAccountingService.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2023 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.server.service; + +import java.time.Duration; +import java.time.LocalDate; +import java.time.temporal.TemporalAdjusters; +import javax.enterprise.context.ApplicationScoped; +import javax.persistence.EntityExistsException; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.opendc.web.server.model.UserAccounting; + +/** + * Service to track the simulation budget of users. + */ +@ApplicationScoped +public final class UserAccountingService { + /** + * The default simulation budget for new users. + */ + private final Duration simulationBudget; + + /** + * Construct a {@link UserAccountingService} instance. + * + * @param simulationBudget The default simulation budget for new users. + */ + public UserAccountingService( + @ConfigProperty(name = "opendc.accounting.simulation-budget", defaultValue = "2000m") + Duration simulationBudget) { + this.simulationBudget = simulationBudget; + } + + /** + * Return the {@link org.opendc.web.proto.user.UserAccounting} object for the user with the + * specified <code>userId</code>. If the object does not exist in the database, a default value is constructed. + */ + public org.opendc.web.proto.user.UserAccounting getAccounting(String userId) { + UserAccounting accounting = UserAccounting.findByUser(userId); + if (accounting != null) { + return new org.opendc.web.proto.user.UserAccounting( + accounting.periodEnd, accounting.simulationTime, accounting.simulationTimeBudget); + } + + return new org.opendc.web.proto.user.UserAccounting( + getNextAccountingPeriod(LocalDate.now()), 0, (int) simulationBudget.toSeconds()); + } + + /** + * Determine whether the user with <code>userId</code> has any remaining simulation budget. + * + * @param userId The unique identifier of the user. + * @return <code>true</code> when the user still has budget left, <code>false</code> otherwise. + */ + public boolean hasSimulationBudget(String userId) { + UserAccounting accounting = UserAccounting.findByUser(userId); + if (accounting == null) { + return true; + } + return accounting.hasSimulationBudget(); + } + + /** + * Consume <code>seconds</code> from the simulation budget of the user with <code>userId</code>. + * + * @param userId The unique identifier of the user. + * @param seconds The seconds to consume from the simulation budget. + * @return <code>true</code> if the user has consumed his full budget or <code>false</code> if there is still budget + * remaining. + */ + public boolean consumeSimulationBudget(String userId, int seconds) { + LocalDate today = LocalDate.now(); + LocalDate nextAccountingPeriod = getNextAccountingPeriod(today); + + // We need to be careful to prevent conflicts in case of concurrency + // 1. First, we try to create the accounting object if it does not exist yet. This may fail if another instance + // creates the object concurrently. + // 2. Second, we check if the budget needs to be reset and try this atomically. + // 3. Finally, we atomically consume the budget from the object + // This is repeated three times in case there is a conflict + for (int i = 0; i < 3; i++) { + UserAccounting accounting = UserAccounting.findByUser(userId); + + if (accounting == null) { + try { + UserAccounting newAccounting = UserAccounting.create( + userId, nextAccountingPeriod, (int) simulationBudget.toSeconds(), seconds); + return !newAccounting.hasSimulationBudget(); + } catch (EntityExistsException e) { + // Conflict due to concurrency; retry + } + } else { + boolean success; + + if (!today.isBefore(accounting.periodEnd)) { + success = accounting.resetBudget(nextAccountingPeriod, seconds); + } else { + success = accounting.consumeBudget(seconds); + } + + if (success) { + return !accounting.hasSimulationBudget(); + } + } + } + + throw new IllegalStateException("Failed to allocate consume budget due to conflict"); + } + + /** + * Helper method to find next accounting period. + */ + private static LocalDate getNextAccountingPeriod(LocalDate today) { + return today.with(TemporalAdjusters.firstDayOfNextMonth()); + } +} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/DevSecurityOverrideFilter.kt b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/DevSecurityOverrideFilter.java index 0bdf959a..de4478cb 100644 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/DevSecurityOverrideFilter.kt +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/DevSecurityOverrideFilter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 AtLarge Research + * Copyright (c) 2021 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 @@ -20,15 +20,15 @@ * SOFTWARE. */ -package org.opendc.web.server.util +package org.opendc.web.server.util; -import io.quarkus.arc.properties.IfBuildProperty -import java.security.Principal -import javax.ws.rs.container.ContainerRequestContext -import javax.ws.rs.container.ContainerRequestFilter -import javax.ws.rs.container.PreMatching -import javax.ws.rs.core.SecurityContext -import javax.ws.rs.ext.Provider +import io.quarkus.arc.properties.IfBuildProperty; +import java.security.Principal; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.container.PreMatching; +import javax.ws.rs.core.SecurityContext; +import javax.ws.rs.ext.Provider; /** * Helper class to disable security for the OpenDC web API when in development mode. @@ -36,16 +36,29 @@ import javax.ws.rs.ext.Provider @Provider @PreMatching @IfBuildProperty(name = "opendc.security.enabled", stringValue = "false") -class DevSecurityOverrideFilter : ContainerRequestFilter { - override fun filter(requestContext: ContainerRequestContext) { - requestContext.securityContext = object : SecurityContext { - override fun getUserPrincipal(): Principal = Principal { "anon" } +public class DevSecurityOverrideFilter implements ContainerRequestFilter { + @Override + public void filter(ContainerRequestContext requestContext) { + requestContext.setSecurityContext(new SecurityContext() { + @Override + public Principal getUserPrincipal() { + return () -> "anon"; + } - override fun isSecure(): Boolean = false + @Override + public boolean isUserInRole(String role) { + return true; + } - override fun isUserInRole(role: String): Boolean = true + @Override + public boolean isSecure() { + return false; + } - override fun getAuthenticationScheme(): String = "basic" - } + @Override + public String getAuthenticationScheme() { + return "basic"; + } + }); } } diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/KotlinModuleCustomizer.kt b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/KotlinModuleCustomizer.java index 8634c8a4..c30edcbf 100644 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/KotlinModuleCustomizer.kt +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/KotlinModuleCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 AtLarge Research + * Copyright (c) 2023 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 @@ -20,19 +20,20 @@ * SOFTWARE. */ -package org.opendc.web.server.util +package org.opendc.web.server.util; -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.module.kotlin.KotlinModule -import io.quarkus.jackson.ObjectMapperCustomizer -import javax.inject.Singleton +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.module.kotlin.KotlinModule; +import io.quarkus.jackson.ObjectMapperCustomizer; +import javax.inject.Singleton; /** * Helper class to register the Kotlin Jackson module. */ @Singleton -class KotlinModuleCustomizer : ObjectMapperCustomizer { - override fun customize(objectMapper: ObjectMapper) { - objectMapper.registerModule(KotlinModule.Builder().build()) +public final class KotlinModuleCustomizer implements ObjectMapperCustomizer { + @Override + public void customize(ObjectMapper objectMapper) { + objectMapper.registerModule(new KotlinModule.Builder().build()); } } diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/Utils.kt b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/QuarkusObjectMapperSupplier.java index 2d0da3b3..e46c74ed 100644 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/Utils.kt +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/QuarkusObjectMapperSupplier.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 AtLarge Research + * Copyright (c) 2023 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 @@ -20,21 +20,20 @@ * SOFTWARE. */ -package org.opendc.web.server.service +package org.opendc.web.server.util; -import org.opendc.web.proto.user.ProjectRole +import com.fasterxml.jackson.databind.ObjectMapper; +import io.hypersistence.utils.hibernate.type.util.ObjectMapperSupplier; +import io.quarkus.runtime.annotations.RegisterForReflection; +import javax.enterprise.inject.spi.CDI; /** - * Flag to indicate that the user can edit a project. + * A supplier for an {@link ObjectMapper} used by the Hypersistence utilities. */ -internal val ProjectRole.canEdit: Boolean - get() = when (this) { - ProjectRole.OWNER, ProjectRole.EDITOR -> true - ProjectRole.VIEWER -> false +@RegisterForReflection +public class QuarkusObjectMapperSupplier implements ObjectMapperSupplier { + @Override + public ObjectMapper get() { + return CDI.current().select(ObjectMapper.class).get(); } - -/** - * Flag to indicate that the user can delete a project. - */ -internal val ProjectRole.canDelete: Boolean - get() = this == ProjectRole.OWNER +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/runner/QuarkusJobManager.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/runner/QuarkusJobManager.java new file mode 100644 index 00000000..0331eacf --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/runner/QuarkusJobManager.java @@ -0,0 +1,114 @@ +/* + * 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.server.util.runner; + +import java.util.Map; +import javax.enterprise.context.ApplicationScoped; +import javax.transaction.Transactional; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.opendc.web.proto.JobState; +import org.opendc.web.runner.JobManager; +import org.opendc.web.server.model.Job; +import org.opendc.web.server.rest.runner.RunnerProtocol; +import org.opendc.web.server.service.JobService; + +/** + * Implementation of {@link JobManager} that interfaces directly with the database without overhead of the REST API. + */ +@ApplicationScoped +public class QuarkusJobManager implements JobManager { + /** + * The {@link JobService} used to manage the job's lifecycle. + */ + private final JobService jobService; + + /** + * Construct a {@link QuarkusJobManager}. + * + * @param jobService The {@link JobService} for managing the job's lifecycle. + */ + public QuarkusJobManager(JobService jobService) { + this.jobService = jobService; + } + + @Transactional + @Nullable + @Override + public org.opendc.web.proto.runner.Job findNext() { + var job = Job.findByState(JobState.PENDING).firstResult(); + if (job == null) { + return null; + } + + return RunnerProtocol.toDto(job); + } + + @Transactional + @Override + public boolean claim(long id) { + return updateState(id, JobState.CLAIMED, 0, null); + } + + @Transactional + @Override + public boolean heartbeat(long id, int runtime) { + return updateState(id, JobState.RUNNING, runtime, null); + } + + @Transactional + @Override + public void fail(long id, int runtime) { + updateState(id, JobState.FAILED, runtime, null); + } + + @Transactional + @Override + public void finish(long id, int runtime, @NotNull Map<String, ?> results) { + updateState(id, JobState.FINISHED, runtime, results); + } + + /** + * Helper method to update the state of a job. + * + * @param id The unique id of the job. + * @param newState The new state to transition to. + * @param runtime The runtime of the job. + * @param results The results of the job. + * @return <code>true</code> if the operation succeeded, <code>false</code> otherwise. + */ + private boolean updateState(long id, JobState newState, int runtime, Map<String, ?> results) { + Job job = Job.findById(id); + + if (job == null) { + return false; + } + + try { + jobService.updateJob(job, newState, runtime, results); + return true; + } catch (IllegalArgumentException | IllegalStateException e) { + return false; + } + } +} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/OpenDCApplication.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/OpenDCApplication.kt deleted file mode 100644 index 1a426095..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/OpenDCApplication.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) 2021 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.server - -import javax.ws.rs.core.Application - -/** - * [Application] definition for the OpenDC web API. - */ -class OpenDCApplication : Application() diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Job.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Job.kt deleted file mode 100644 index 84a71acf..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Job.kt +++ /dev/null @@ -1,114 +0,0 @@ -/* - * 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.server.model - -import org.hibernate.annotations.Type -import org.hibernate.annotations.TypeDef -import org.opendc.web.proto.JobState -import org.opendc.web.server.util.hibernate.json.JsonType -import java.time.Instant -import javax.persistence.Column -import javax.persistence.Entity -import javax.persistence.FetchType -import javax.persistence.GeneratedValue -import javax.persistence.GenerationType -import javax.persistence.Id -import javax.persistence.JoinColumn -import javax.persistence.NamedQueries -import javax.persistence.NamedQuery -import javax.persistence.OneToOne -import javax.persistence.Table - -/** - * A simulation job to be run by the simulator. - */ -@TypeDef(name = "json", typeClass = JsonType::class) -@Entity -@Table(name = "jobs") -@NamedQueries( - value = [ - NamedQuery( - name = "Job.findAll", - query = "SELECT j FROM Job j WHERE j.state = :state" - ), - NamedQuery( - name = "Job.updateOne", - query = """ - UPDATE Job j - SET j.state = :newState, j.updatedAt = :updatedAt, j.runtime = :runtime, j.results = :results - WHERE j.id = :id AND j.state = :oldState - """ - ) - ] -) -class Job( - @Id - @GeneratedValue(strategy = GenerationType.AUTO) - val id: Long, - - @Column(name = "created_by", nullable = false, updatable = false) - val createdBy: String, - - @OneToOne(optional = false, mappedBy = "job", fetch = FetchType.EAGER) - @JoinColumn(name = "scenario_id", nullable = false) - val scenario: Scenario, - - @Column(name = "created_at", nullable = false, updatable = false) - val createdAt: Instant, - - /** - * The number of simulation runs to perform. - */ - @Column(nullable = false, updatable = false) - val repeats: Int -) { - /** - * The instant at which the job was updated. - */ - @Column(name = "updated_at", nullable = false) - var updatedAt: Instant = createdAt - - /** - * The state of the job. - */ - @Column(nullable = false) - var state: JobState = JobState.PENDING - - /** - * The runtime of the job (in seconds). - */ - @Column(nullable = false) - var runtime: Int = 0 - - /** - * Experiment results in JSON - */ - @Type(type = "json") - @Column(columnDefinition = "jsonb") - var results: Map<String, Any>? = null - - /** - * Return a string representation of this job. - */ - override fun toString(): String = "Job[id=$id,scenario=${scenario.id},state=$state]" -} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Portfolio.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Portfolio.kt deleted file mode 100644 index 09437712..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Portfolio.kt +++ /dev/null @@ -1,103 +0,0 @@ -/* - * 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.server.model - -import org.hibernate.annotations.Type -import org.hibernate.annotations.TypeDef -import org.opendc.web.proto.Targets -import org.opendc.web.server.util.hibernate.json.JsonType -import javax.persistence.CascadeType -import javax.persistence.Column -import javax.persistence.Entity -import javax.persistence.GeneratedValue -import javax.persistence.GenerationType -import javax.persistence.Id -import javax.persistence.Index -import javax.persistence.JoinColumn -import javax.persistence.ManyToOne -import javax.persistence.NamedQueries -import javax.persistence.NamedQuery -import javax.persistence.OneToMany -import javax.persistence.OrderBy -import javax.persistence.Table -import javax.persistence.UniqueConstraint - -/** - * A portfolio is the composition of multiple scenarios. - */ -@TypeDef(name = "json", typeClass = JsonType::class) -@Entity -@Table( - name = "portfolios", - uniqueConstraints = [UniqueConstraint(columnNames = ["project_id", "number"])], - indexes = [Index(name = "fn_portfolios_number", columnList = "project_id, number")] -) -@NamedQueries( - value = [ - NamedQuery( - name = "Portfolio.findAll", - query = "SELECT p FROM Portfolio p WHERE p.project.id = :projectId" - ), - NamedQuery( - name = "Portfolio.findOne", - query = "SELECT p FROM Portfolio p WHERE p.project.id = :projectId AND p.number = :number" - ) - ] -) -class Portfolio( - @Id - @GeneratedValue(strategy = GenerationType.AUTO) - val id: Long, - - /** - * Unique number of the portfolio for the project. - */ - @Column(nullable = false) - val number: Int, - - @Column(nullable = false) - val name: String, - - @ManyToOne(optional = false) - @JoinColumn(name = "project_id", nullable = false) - val project: Project, - - /** - * The portfolio targets (metrics, repetitions). - */ - @Type(type = "json") - @Column(columnDefinition = "jsonb", nullable = false, updatable = false) - val targets: Targets -) { - /** - * The scenarios in this portfolio. - */ - @OneToMany(cascade = [CascadeType.ALL], mappedBy = "portfolio", orphanRemoval = true) - @OrderBy("id ASC") - val scenarios: MutableSet<Scenario> = mutableSetOf() - - /** - * Return a string representation of this portfolio. - */ - override fun toString(): String = "Job[id=$id,name=$name,project=${project.id},targets=$targets]" -} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Project.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Project.kt deleted file mode 100644 index 41d1a786..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Project.kt +++ /dev/null @@ -1,144 +0,0 @@ -/* - * 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.server.model - -import java.time.Instant -import javax.persistence.CascadeType -import javax.persistence.Column -import javax.persistence.Entity -import javax.persistence.GeneratedValue -import javax.persistence.GenerationType -import javax.persistence.Id -import javax.persistence.NamedQueries -import javax.persistence.NamedQuery -import javax.persistence.OneToMany -import javax.persistence.OrderBy -import javax.persistence.Table - -/** - * A project in OpenDC encapsulates all the datacenter designs and simulation runs for a set of users. - */ -@Entity -@Table(name = "projects") -@NamedQueries( - value = [ - NamedQuery( - name = "Project.findAll", - query = """ - SELECT a - FROM ProjectAuthorization a - WHERE a.key.userId = :userId - """ - ), - NamedQuery( - name = "Project.allocatePortfolio", - query = """ - UPDATE Project p - SET p.portfoliosCreated = :oldState + 1, p.updatedAt = :now - WHERE p.id = :id AND p.portfoliosCreated = :oldState - """ - ), - NamedQuery( - name = "Project.allocateTopology", - query = """ - UPDATE Project p - SET p.topologiesCreated = :oldState + 1, p.updatedAt = :now - WHERE p.id = :id AND p.topologiesCreated = :oldState - """ - ), - NamedQuery( - name = "Project.allocateScenario", - query = """ - UPDATE Project p - SET p.scenariosCreated = :oldState + 1, p.updatedAt = :now - WHERE p.id = :id AND p.scenariosCreated = :oldState - """ - ) - ] -) -class Project( - @Id - @GeneratedValue(strategy = GenerationType.AUTO) - val id: Long, - - @Column(nullable = false) - var name: String, - - @Column(name = "created_at", nullable = false, updatable = false) - val createdAt: Instant -) { - /** - * The instant at which the project was updated. - */ - @Column(name = "updated_at", nullable = false) - var updatedAt: Instant = createdAt - - /** - * The portfolios belonging to this project. - */ - @OneToMany(cascade = [CascadeType.ALL], mappedBy = "project", orphanRemoval = true) - @OrderBy("id ASC") - val portfolios: MutableSet<Portfolio> = mutableSetOf() - - /** - * The number of portfolios created for this project (including deleted portfolios). - */ - @Column(name = "portfolios_created", nullable = false) - var portfoliosCreated: Int = 0 - - /** - * The topologies belonging to this project. - */ - @OneToMany(cascade = [CascadeType.ALL], mappedBy = "project", orphanRemoval = true) - @OrderBy("id ASC") - val topologies: MutableSet<Topology> = mutableSetOf() - - /** - * The number of topologies created for this project (including deleted topologies). - */ - @Column(name = "topologies_created", nullable = false) - var topologiesCreated: Int = 0 - - /** - * The scenarios belonging to this project. - */ - @OneToMany(mappedBy = "project", orphanRemoval = true) - val scenarios: MutableSet<Scenario> = mutableSetOf() - - /** - * The number of scenarios created for this project (including deleted scenarios). - */ - @Column(name = "scenarios_created", nullable = false) - var scenariosCreated: Int = 0 - - /** - * The users authorized to access the project. - */ - @OneToMany(cascade = [CascadeType.ALL], mappedBy = "project", orphanRemoval = true) - val authorizations: MutableSet<ProjectAuthorization> = mutableSetOf() - - /** - * Return a string representation of this project. - */ - override fun toString(): String = "Project[id=$id,name=$name]" -} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/ProjectAuthorization.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/ProjectAuthorization.kt deleted file mode 100644 index 791725cd..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/ProjectAuthorization.kt +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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.server.model - -import org.opendc.web.proto.user.ProjectRole -import javax.persistence.Column -import javax.persistence.EmbeddedId -import javax.persistence.Entity -import javax.persistence.JoinColumn -import javax.persistence.ManyToOne -import javax.persistence.MapsId -import javax.persistence.Table - -/** - * An authorization for some user to participate in a project. - */ -@Entity -@Table(name = "project_authorizations") -class ProjectAuthorization( - /** - * The user identifier of the authorization. - */ - @EmbeddedId - val key: ProjectAuthorizationKey, - - /** - * The project that the user is authorized to participate in. - */ - @ManyToOne(optional = false) - @MapsId("projectId") - @JoinColumn(name = "project_id", updatable = false, insertable = false, nullable = false) - val project: Project, - - /** - * The role of the user in the project. - */ - @Column(nullable = false) - val role: ProjectRole -) { - /** - * Return a string representation of this project authorization. - */ - override fun toString(): String = "ProjectAuthorization[project=${key.projectId},user=${key.userId},role=$role]" -} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/ProjectAuthorizationKey.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/ProjectAuthorizationKey.kt deleted file mode 100644 index 449b6608..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/ProjectAuthorizationKey.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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.server.model - -import javax.persistence.Column -import javax.persistence.Embeddable - -/** - * Key for representing a [ProjectAuthorization] object. - */ -@Embeddable -data class ProjectAuthorizationKey( - @Column(name = "user_id", nullable = false) - val userId: String, - - @Column(name = "project_id", nullable = false) - val projectId: Long -) : java.io.Serializable diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Scenario.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Scenario.kt deleted file mode 100644 index 62adc9e2..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Scenario.kt +++ /dev/null @@ -1,121 +0,0 @@ -/* - * 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.server.model - -import org.hibernate.annotations.Type -import org.hibernate.annotations.TypeDef -import org.opendc.web.proto.OperationalPhenomena -import org.opendc.web.server.util.hibernate.json.JsonType -import javax.persistence.CascadeType -import javax.persistence.Column -import javax.persistence.Embedded -import javax.persistence.Entity -import javax.persistence.GeneratedValue -import javax.persistence.GenerationType -import javax.persistence.Id -import javax.persistence.Index -import javax.persistence.JoinColumn -import javax.persistence.ManyToOne -import javax.persistence.NamedQueries -import javax.persistence.NamedQuery -import javax.persistence.OneToOne -import javax.persistence.Table -import javax.persistence.UniqueConstraint - -/** - * A single scenario to be explored by the simulator. - */ -@TypeDef(name = "json", typeClass = JsonType::class) -@Entity -@Table( - name = "scenarios", - uniqueConstraints = [UniqueConstraint(columnNames = ["project_id", "number"])], - indexes = [Index(name = "fn_scenarios_number", columnList = "project_id, number")] -) -@NamedQueries( - value = [ - NamedQuery( - name = "Scenario.findAll", - query = "SELECT s FROM Scenario s WHERE s.project.id = :projectId" - ), - NamedQuery( - name = "Scenario.findAllForPortfolio", - query = """ - SELECT s - FROM Scenario s - JOIN Portfolio p ON p.id = s.portfolio.id AND p.number = :number - WHERE s.project.id = :projectId - """ - ), - NamedQuery( - name = "Scenario.findOne", - query = "SELECT s FROM Scenario s WHERE s.project.id = :projectId AND s.number = :number" - ) - ] -) -class Scenario( - @Id - @GeneratedValue(strategy = GenerationType.AUTO) - val id: Long, - - /** - * Unique number of the scenario for the project. - */ - @Column(nullable = false) - val number: Int, - - @Column(nullable = false, updatable = false) - val name: String, - - @ManyToOne(optional = false) - @JoinColumn(name = "project_id", nullable = false) - val project: Project, - - @ManyToOne(optional = false) - @JoinColumn(name = "portfolio_id", nullable = false) - val portfolio: Portfolio, - - @Embedded - val workload: Workload, - - @ManyToOne(optional = false) - val topology: Topology, - - @Type(type = "json") - @Column(columnDefinition = "jsonb", nullable = false, updatable = false) - val phenomena: OperationalPhenomena, - - @Column(name = "scheduler_name", nullable = false, updatable = false) - val schedulerName: String -) { - /** - * The [Job] associated with the scenario. - */ - @OneToOne(cascade = [CascadeType.ALL]) - lateinit var job: Job - - /** - * Return a string representation of this scenario. - */ - override fun toString(): String = "Scenario[id=$id,name=$name,project=${project.id},portfolio=${portfolio.id}]" -} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Topology.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Topology.kt deleted file mode 100644 index 26368455..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/Topology.kt +++ /dev/null @@ -1,103 +0,0 @@ -/* - * 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.server.model - -import org.hibernate.annotations.Type -import org.hibernate.annotations.TypeDef -import org.opendc.web.proto.Room -import org.opendc.web.server.util.hibernate.json.JsonType -import java.time.Instant -import javax.persistence.Column -import javax.persistence.Entity -import javax.persistence.GeneratedValue -import javax.persistence.GenerationType -import javax.persistence.Id -import javax.persistence.Index -import javax.persistence.JoinColumn -import javax.persistence.ManyToOne -import javax.persistence.NamedQueries -import javax.persistence.NamedQuery -import javax.persistence.Table -import javax.persistence.UniqueConstraint - -/** - * A datacenter design in OpenDC. - */ -@TypeDef(name = "json", typeClass = JsonType::class) -@Entity -@Table( - name = "topologies", - uniqueConstraints = [UniqueConstraint(columnNames = ["project_id", "number"])], - indexes = [Index(name = "fn_topologies_number", columnList = "project_id, number")] -) -@NamedQueries( - value = [ - NamedQuery( - name = "Topology.findAll", - query = "SELECT t FROM Topology t WHERE t.project.id = :projectId" - ), - NamedQuery( - name = "Topology.findOne", - query = "SELECT t FROM Topology t WHERE t.project.id = :projectId AND t.number = :number" - ) - ] -) -class Topology( - @Id - @GeneratedValue(strategy = GenerationType.AUTO) - val id: Long, - - /** - * Unique number of the topology for the project. - */ - @Column(nullable = false) - val number: Int, - - @Column(nullable = false) - val name: String, - - @ManyToOne(optional = false) - @JoinColumn(name = "project_id", nullable = false) - val project: Project, - - @Column(name = "created_at", nullable = false, updatable = false) - val createdAt: Instant, - - /** - * Datacenter design in JSON - */ - @Type(type = "json") - @Column(columnDefinition = "jsonb", nullable = false) - var rooms: List<Room> = emptyList() -) { - /** - * The instant at which the topology was updated. - */ - @Column(name = "updated_at", nullable = false) - var updatedAt: Instant = createdAt - - /** - * Return a string representation of this topology. - */ - override fun toString(): String = "Topology[id=$id,name=$name,project=${project.id}]" -} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/UserAccounting.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/UserAccounting.kt deleted file mode 100644 index 5b813044..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/model/UserAccounting.kt +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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.server.model - -import java.time.LocalDate -import javax.persistence.Column -import javax.persistence.Entity -import javax.persistence.Id -import javax.persistence.NamedQueries -import javax.persistence.NamedQuery -import javax.persistence.Table - -/** - * Entity to track the number of simulation minutes used by a user. - */ -@Entity -@Table(name = "user_accounting") -@NamedQueries( - value = [ - NamedQuery( - name = "UserAccounting.consumeBudget", - query = """ - UPDATE UserAccounting a - SET a.simulationTime = a.simulationTime + :seconds - WHERE a.userId = :userId AND a.periodEnd = :periodEnd - """ - ), - NamedQuery( - name = "UserAccounting.resetBudget", - query = """ - UPDATE UserAccounting a - SET a.periodEnd = :periodEnd, a.simulationTime = :seconds - WHERE a.userId = :userId AND a.periodEnd = :oldPeriodEnd - """ - ) - ] -) -class UserAccounting( - @Id - @Column(name = "user_id", nullable = false) - val userId: String, - - /** - * The end of the accounting period. - */ - @Column(name = "period_end", nullable = false) - var periodEnd: LocalDate, - - /** - * The number of simulation seconds to be used per accounting period. - */ - @Column(name = "simulation_time_budget", nullable = false) - var simulationTimeBudget: Int -) { - /** - * The number of simulation seconds used in this period. This number should reset once the accounting period has - * been reached. - */ - @Column(name = "simulation_time", nullable = false) - var simulationTime: Int = 0 -} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/JobRepository.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/JobRepository.kt deleted file mode 100644 index e9bf0af0..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/JobRepository.kt +++ /dev/null @@ -1,94 +0,0 @@ -/* - * 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.server.repository - -import org.opendc.web.proto.JobState -import org.opendc.web.server.model.Job -import java.time.Instant -import javax.enterprise.context.ApplicationScoped -import javax.inject.Inject -import javax.persistence.EntityManager - -/** - * A repository to manage [Job] entities. - */ -@ApplicationScoped -class JobRepository @Inject constructor(private val em: EntityManager) { - /** - * Find all jobs currently residing in [state]. - * - * @param state The state in which the jobs should be. - * @return The list of jobs in state [state]. - */ - fun findAll(state: JobState): List<Job> { - return em.createNamedQuery("Job.findAll", Job::class.java) - .setParameter("state", state) - .resultList - } - - /** - * Find the [Job] with the specified [id]. - * - * @param id The unique identifier of the job. - * @return The trace or `null` if it does not exist. - */ - fun findOne(id: Long): Job? { - return em.find(Job::class.java, id) - } - - /** - * Delete the specified [job]. - */ - fun delete(job: Job) { - em.remove(job) - } - - /** - * Save the specified [job] to the database. - */ - fun save(job: Job) { - em.persist(job) - } - - /** - * Atomically update the specified [job]. - * - * @param job The job to update atomically. - * @param newState The new state to enter into. - * @param time The time at which the update occurs. - * @param results The results to possible set. - * @return `true` when the update succeeded`, `false` when there was a conflict. - */ - fun updateOne(job: Job, newState: JobState, time: Instant, runtime: Int, results: Map<String, Any>?): Boolean { - val count = em.createNamedQuery("Job.updateOne") - .setParameter("id", job.id) - .setParameter("oldState", job.state) - .setParameter("newState", newState) - .setParameter("updatedAt", Instant.now()) - .setParameter("runtime", runtime) - .setParameter("results", results) - .executeUpdate() - em.refresh(job) - return count > 0 - } -} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/PortfolioRepository.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/PortfolioRepository.kt deleted file mode 100644 index 77130c15..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/PortfolioRepository.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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.server.repository - -import org.opendc.web.server.model.Portfolio -import javax.enterprise.context.ApplicationScoped -import javax.inject.Inject -import javax.persistence.EntityManager - -/** - * A repository to manage [Portfolio] entities. - */ -@ApplicationScoped -class PortfolioRepository @Inject constructor(private val em: EntityManager) { - /** - * Find all [Portfolio]s that belong to [project][projectId]. - * - * @param projectId The unique identifier of the project. - * @return The list of portfolios that belong to the specified project. - */ - fun findAll(projectId: Long): List<Portfolio> { - return em.createNamedQuery("Portfolio.findAll", Portfolio::class.java) - .setParameter("projectId", projectId) - .resultList - } - - /** - * Find the [Portfolio] with the specified [number] belonging to [project][projectId]. - * - * @param projectId The unique identifier of the project. - * @param number The number of the portfolio. - * @return The portfolio or `null` if it does not exist. - */ - fun findOne(projectId: Long, number: Int): Portfolio? { - return em.createNamedQuery("Portfolio.findOne", Portfolio::class.java) - .setParameter("projectId", projectId) - .setParameter("number", number) - .setMaxResults(1) - .resultList - .firstOrNull() - } - - /** - * Delete the specified [portfolio]. - */ - fun delete(portfolio: Portfolio) { - em.remove(portfolio) - } - - /** - * Save the specified [portfolio] to the database. - */ - fun save(portfolio: Portfolio) { - em.persist(portfolio) - } -} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/ProjectRepository.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/ProjectRepository.kt deleted file mode 100644 index 519da3de..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/ProjectRepository.kt +++ /dev/null @@ -1,157 +0,0 @@ -/* - * 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.server.repository - -import org.opendc.web.server.model.Project -import org.opendc.web.server.model.ProjectAuthorization -import org.opendc.web.server.model.ProjectAuthorizationKey -import java.time.Instant -import javax.enterprise.context.ApplicationScoped -import javax.inject.Inject -import javax.persistence.EntityManager - -/** - * A repository to manage [Project] entities. - */ -@ApplicationScoped -class ProjectRepository @Inject constructor(private val em: EntityManager) { - /** - * List all projects for the user with the specified [userId]. - * - * @param userId The identifier of the user that is requesting the list of projects. - * @return A list of projects that the user has received authorization for. - */ - fun findAll(userId: String): List<ProjectAuthorization> { - return em.createNamedQuery("Project.findAll", ProjectAuthorization::class.java) - .setParameter("userId", userId) - .resultList - } - - /** - * Find the project with [id] for the user with the specified [userId]. - * - * @param userId The identifier of the user that is requesting the list of projects. - * @param id The unique identifier of the project. - * @return The project with the specified identifier or `null` if it does not exist or is not accessible to the - * user with the specified identifier. - */ - fun findOne(userId: String, id: Long): ProjectAuthorization? { - return em.find(ProjectAuthorization::class.java, ProjectAuthorizationKey(userId, id)) - } - - /** - * Delete the specified [project]. - */ - fun delete(project: Project) { - em.remove(project) - } - - /** - * Save the specified [project] to the database. - */ - fun save(project: Project) { - em.persist(project) - } - - /** - * Save the specified [auth] to the database. - */ - fun save(auth: ProjectAuthorization) { - em.persist(auth) - } - - /** - * Allocate the next portfolio number for the specified [project]. - * - * @param project The project to allocate the portfolio number for. - * @param time The time at which the new portfolio is created. - * @param tries The number of times to try to allocate the number before failing. - */ - fun allocatePortfolio(project: Project, time: Instant, tries: Int = 4): Int { - repeat(tries) { - val count = em.createNamedQuery("Project.allocatePortfolio") - .setParameter("id", project.id) - .setParameter("oldState", project.portfoliosCreated) - .setParameter("now", time) - .executeUpdate() - - if (count > 0) { - return project.portfoliosCreated + 1 - } else { - em.refresh(project) - } - } - - throw IllegalStateException("Failed to allocate next portfolio") - } - - /** - * Allocate the next topology number for the specified [project]. - * - * @param project The project to allocate the topology number for. - * @param time The time at which the new topology is created. - * @param tries The number of times to try to allocate the number before failing. - */ - fun allocateTopology(project: Project, time: Instant, tries: Int = 4): Int { - repeat(tries) { - val count = em.createNamedQuery("Project.allocateTopology") - .setParameter("id", project.id) - .setParameter("oldState", project.topologiesCreated) - .setParameter("now", time) - .executeUpdate() - - if (count > 0) { - return project.topologiesCreated + 1 - } else { - em.refresh(project) - } - } - - throw IllegalStateException("Failed to allocate next topology") - } - - /** - * Allocate the next scenario number for the specified [project]. - * - * @param project The project to allocate the scenario number for. - * @param time The time at which the new scenario is created. - * @param tries The number of times to try to allocate the number before failing. - */ - fun allocateScenario(project: Project, time: Instant, tries: Int = 4): Int { - repeat(tries) { - val count = em.createNamedQuery("Project.allocateScenario") - .setParameter("id", project.id) - .setParameter("oldState", project.scenariosCreated) - .setParameter("now", time) - .executeUpdate() - - if (count > 0) { - return project.scenariosCreated + 1 - } else { - em.refresh(project) - } - } - - throw IllegalStateException("Failed to allocate next scenario") - } -} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/ScenarioRepository.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/ScenarioRepository.kt deleted file mode 100644 index 145db71d..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/ScenarioRepository.kt +++ /dev/null @@ -1,90 +0,0 @@ -/* - * 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.server.repository - -import org.opendc.web.server.model.Scenario -import javax.enterprise.context.ApplicationScoped -import javax.inject.Inject -import javax.persistence.EntityManager - -/** - * A repository to manage [Scenario] entities. - */ -@ApplicationScoped -class ScenarioRepository @Inject constructor(private val em: EntityManager) { - /** - * Find all [Scenario]s that belong to [project][projectId]. - * - * @param projectId The unique identifier of the project. - * @return The list of scenarios that belong to the specified project. - */ - fun findAll(projectId: Long): List<Scenario> { - return em.createNamedQuery("Scenario.findAll", Scenario::class.java) - .setParameter("projectId", projectId) - .resultList - } - - /** - * Find all [Scenario]s that belong to [portfolio][number] of [project][projectId]. - * - * @param projectId The unique identifier of the project. - * @param number The number of the portfolio to which the scenarios should belong. - * @return The list of scenarios that belong to the specified portfolio. - */ - fun findAll(projectId: Long, number: Int): List<Scenario> { - return em.createNamedQuery("Scenario.findAllForPortfolio", Scenario::class.java) - .setParameter("projectId", projectId) - .setParameter("number", number) - .resultList - } - - /** - * Find the [Scenario] with the specified [number] belonging to [project][projectId]. - * - * @param projectId The unique identifier of the project. - * @param number The number of the scenario. - * @return The scenario or `null` if it does not exist. - */ - fun findOne(projectId: Long, number: Int): Scenario? { - return em.createNamedQuery("Scenario.findOne", Scenario::class.java) - .setParameter("projectId", projectId) - .setParameter("number", number) - .setMaxResults(1) - .resultList - .firstOrNull() - } - - /** - * Delete the specified [scenario]. - */ - fun delete(scenario: Scenario) { - em.remove(scenario) - } - - /** - * Save the specified [scenario] to the database. - */ - fun save(scenario: Scenario) { - em.persist(scenario) - } -} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/TopologyRepository.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/TopologyRepository.kt deleted file mode 100644 index e8eadd63..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/TopologyRepository.kt +++ /dev/null @@ -1,86 +0,0 @@ -/* - * 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.server.repository - -import org.opendc.web.server.model.Topology -import javax.enterprise.context.ApplicationScoped -import javax.inject.Inject -import javax.persistence.EntityManager - -/** - * A repository to manage [Topology] entities. - */ -@ApplicationScoped -class TopologyRepository @Inject constructor(private val em: EntityManager) { - /** - * Find all [Topology]s that belong to [project][projectId]. - * - * @param projectId The unique identifier of the project. - * @return The list of topologies that belong to the specified project. - */ - fun findAll(projectId: Long): List<Topology> { - return em.createNamedQuery("Topology.findAll", Topology::class.java) - .setParameter("projectId", projectId) - .resultList - } - - /** - * Find the [Topology] with the specified [number] belonging to [project][projectId]. - * - * @param projectId The unique identifier of the project. - * @param number The number of the topology. - * @return The topology or `null` if it does not exist. - */ - fun findOne(projectId: Long, number: Int): Topology? { - return em.createNamedQuery("Topology.findOne", Topology::class.java) - .setParameter("projectId", projectId) - .setParameter("number", number) - .setMaxResults(1) - .resultList - .firstOrNull() - } - - /** - * Find the [Topology] with the specified [id]. - * - * @param id Unique identifier of the topology. - * @return The topology or `null` if it does not exist. - */ - fun findOne(id: Long): Topology? { - return em.find(Topology::class.java, id) - } - - /** - * Delete the specified [topology]. - */ - fun delete(topology: Topology) { - em.remove(topology) - } - - /** - * Save the specified [topology] to the database. - */ - fun save(topology: Topology) { - em.persist(topology) - } -} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/TraceRepository.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/TraceRepository.kt deleted file mode 100644 index f328eea6..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/TraceRepository.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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.server.repository - -import org.opendc.web.server.model.Trace -import javax.enterprise.context.ApplicationScoped -import javax.inject.Inject -import javax.persistence.EntityManager - -/** - * A repository to manage [Trace] entities. - */ -@ApplicationScoped -class TraceRepository @Inject constructor(private val em: EntityManager) { - /** - * Find all workload traces in the database. - * - * @return The list of available workload traces. - */ - fun findAll(): List<Trace> { - return em.createNamedQuery("Trace.findAll", Trace::class.java).resultList - } - - /** - * Find the [Trace] with the specified [id]. - * - * @param id The unique identifier of the trace. - * @return The trace or `null` if it does not exist. - */ - fun findOne(id: String): Trace? { - return em.find(Trace::class.java, id) - } -} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/UserAccountingRepository.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/UserAccountingRepository.kt deleted file mode 100644 index f0265d3d..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/repository/UserAccountingRepository.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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.server.repository - -import org.opendc.web.server.model.UserAccounting -import java.time.LocalDate -import javax.enterprise.context.ApplicationScoped -import javax.inject.Inject -import javax.persistence.EntityManager - -/** - * A repository to manage [UserAccounting] entities. - */ -@ApplicationScoped -class UserAccountingRepository @Inject constructor(private val em: EntityManager) { - /** - * Find the [UserAccounting] object for the specified [userId]. - * - * @param userId The unique identifier of the user. - * @return The [UserAccounting] object or `null` if it does not exist. - */ - fun findForUser(userId: String): UserAccounting? { - return em.find(UserAccounting::class.java, userId) - } - - /** - * Save the specified [UserAccounting] object to the database. - */ - fun save(accounting: UserAccounting) { - em.persist(accounting) - } - - /** - * Atomically consume the budget for the specified [UserAccounting] object. - * - * @param accounting The [UserAccounting] object to update atomically. - * @param seconds The number of seconds to consume from the user. - * @return `true` when the update succeeded`, `false` when there was a conflict. - */ - fun consumeBudget(accounting: UserAccounting, seconds: Int): Boolean { - val count = em.createNamedQuery("UserAccounting.consumeBudget") - .setParameter("userId", accounting.userId) - .setParameter("periodEnd", accounting.periodEnd) - .setParameter("seconds", seconds) - .executeUpdate() - em.refresh(accounting) - return count > 0 - } - - /** - * Atomically reset the budget for the specified [UserAccounting] object. - * - * @param accounting The [UserAccounting] object to update atomically. - * @param periodEnd The new end period for the budget. - * @param seconds The number of seconds that have already been consumed. - * @return `true` when the update succeeded`, `false` when there was a conflict. - */ - fun resetBudget(accounting: UserAccounting, periodEnd: LocalDate, seconds: Int): Boolean { - val count = em.createNamedQuery("UserAccounting.resetBudget") - .setParameter("userId", accounting.userId) - .setParameter("oldPeriodEnd", accounting.periodEnd) - .setParameter("periodEnd", periodEnd) - .setParameter("seconds", seconds) - .executeUpdate() - em.refresh(accounting) - return count > 0 - } -} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/runner/JobResource.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/runner/JobResource.kt deleted file mode 100644 index d0432360..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/runner/JobResource.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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.server.rest.runner - -import org.opendc.web.proto.runner.Job -import org.opendc.web.server.service.JobService -import javax.annotation.security.RolesAllowed -import javax.inject.Inject -import javax.transaction.Transactional -import javax.validation.Valid -import javax.ws.rs.GET -import javax.ws.rs.POST -import javax.ws.rs.Path -import javax.ws.rs.PathParam -import javax.ws.rs.WebApplicationException - -/** - * A resource representing the available simulation jobs. - */ -@Path("/jobs") -@RolesAllowed("runner") -class JobResource @Inject constructor(private val jobService: JobService) { - /** - * Obtain all pending simulation jobs. - */ - @GET - fun queryPending(): List<Job> { - return jobService.queryPending() - } - - /** - * Get a job by identifier. - */ - @GET - @Path("{job}") - fun get(@PathParam("job") id: Long): Job { - return jobService.findById(id) ?: throw WebApplicationException("Job not found", 404) - } - - /** - * Atomically update the state of a job. - */ - @POST - @Path("{job}") - @Transactional - fun update(@PathParam("job") id: Long, @Valid update: Job.Update): Job { - return try { - jobService.updateState(id, update.state, update.runtime, update.results) - ?: throw WebApplicationException("Job not found", 404) - } catch (e: IllegalArgumentException) { - throw WebApplicationException(e, 400) - } catch (e: IllegalStateException) { - throw WebApplicationException(e, 409) - } - } -} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/PortfolioResource.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/PortfolioResource.kt deleted file mode 100644 index ebe57ae2..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/PortfolioResource.kt +++ /dev/null @@ -1,83 +0,0 @@ -/* - * 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.server.rest.user - -import io.quarkus.security.identity.SecurityIdentity -import org.opendc.web.proto.user.Portfolio -import org.opendc.web.server.service.PortfolioService -import javax.annotation.security.RolesAllowed -import javax.inject.Inject -import javax.transaction.Transactional -import javax.validation.Valid -import javax.ws.rs.DELETE -import javax.ws.rs.GET -import javax.ws.rs.POST -import javax.ws.rs.Path -import javax.ws.rs.PathParam -import javax.ws.rs.WebApplicationException - -/** - * A resource representing the portfolios of a project. - */ -@Path("/projects/{project}/portfolios") -@RolesAllowed("openid") -class PortfolioResource @Inject constructor( - private val portfolioService: PortfolioService, - private val identity: SecurityIdentity -) { - /** - * Get all portfolios that belong to the specified project. - */ - @GET - fun getAll(@PathParam("project") projectId: Long): List<Portfolio> { - return portfolioService.findAll(identity.principal.name, projectId) - } - - /** - * Create a portfolio for this project. - */ - @POST - @Transactional - fun create(@PathParam("project") projectId: Long, @Valid request: Portfolio.Create): Portfolio { - return portfolioService.create(identity.principal.name, projectId, request) ?: throw WebApplicationException("Project not found", 404) - } - - /** - * Obtain a portfolio by its identifier. - */ - @GET - @Path("{portfolio}") - fun get(@PathParam("project") projectId: Long, @PathParam("portfolio") number: Int): Portfolio { - return portfolioService.findOne(identity.principal.name, projectId, number) ?: throw WebApplicationException("Portfolio not found", 404) - } - - /** - * Delete a portfolio. - */ - @DELETE - @Path("{portfolio}") - @Transactional - fun delete(@PathParam("project") projectId: Long, @PathParam("portfolio") number: Int): Portfolio { - return portfolioService.delete(identity.principal.name, projectId, number) ?: throw WebApplicationException("Portfolio not found", 404) - } -} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/PortfolioScenarioResource.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/PortfolioScenarioResource.kt deleted file mode 100644 index 82f35127..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/PortfolioScenarioResource.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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.server.rest.user - -import io.quarkus.security.identity.SecurityIdentity -import org.opendc.web.proto.user.Scenario -import org.opendc.web.server.service.ScenarioService -import javax.annotation.security.RolesAllowed -import javax.inject.Inject -import javax.transaction.Transactional -import javax.validation.Valid -import javax.ws.rs.GET -import javax.ws.rs.POST -import javax.ws.rs.Path -import javax.ws.rs.PathParam -import javax.ws.rs.WebApplicationException - -/** - * A resource representing the scenarios of a portfolio. - */ -@Path("/projects/{project}/portfolios/{portfolio}/scenarios") -@RolesAllowed("openid") -class PortfolioScenarioResource @Inject constructor( - private val scenarioService: ScenarioService, - private val identity: SecurityIdentity -) { - /** - * Get all scenarios that belong to the specified portfolio. - */ - @GET - fun get(@PathParam("project") projectId: Long, @PathParam("portfolio") portfolioNumber: Int): List<Scenario> { - return scenarioService.findAll(identity.principal.name, projectId, portfolioNumber) - } - - /** - * Create a scenario for this portfolio. - */ - @POST - @Transactional - fun create(@PathParam("project") projectId: Long, @PathParam("portfolio") portfolioNumber: Int, @Valid request: Scenario.Create): Scenario { - return scenarioService.create(identity.principal.name, projectId, portfolioNumber, request) ?: throw WebApplicationException("Portfolio not found", 404) - } -} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/ProjectResource.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/ProjectResource.kt deleted file mode 100644 index 817f53a5..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/ProjectResource.kt +++ /dev/null @@ -1,87 +0,0 @@ -/* - * 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.server.rest.user - -import io.quarkus.security.identity.SecurityIdentity -import org.opendc.web.proto.user.Project -import org.opendc.web.server.service.ProjectService -import javax.annotation.security.RolesAllowed -import javax.inject.Inject -import javax.transaction.Transactional -import javax.validation.Valid -import javax.ws.rs.DELETE -import javax.ws.rs.GET -import javax.ws.rs.POST -import javax.ws.rs.Path -import javax.ws.rs.PathParam -import javax.ws.rs.WebApplicationException - -/** - * A resource representing the created projects. - */ -@Path("/projects") -@RolesAllowed("openid") -class ProjectResource @Inject constructor( - private val projectService: ProjectService, - private val identity: SecurityIdentity -) { - /** - * Obtain all the projects of the current user. - */ - @GET - fun getAll(): List<Project> { - return projectService.findWithUser(identity.principal.name) - } - - /** - * Create a new project for the current user. - */ - @POST - @Transactional - fun create(@Valid request: Project.Create): Project { - return projectService.createForUser(identity.principal.name, request.name) - } - - /** - * Obtain a single project by its identifier. - */ - @GET - @Path("{project}") - fun get(@PathParam("project") id: Long): Project { - return projectService.findWithUser(identity.principal.name, id) ?: throw WebApplicationException("Project not found", 404) - } - - /** - * Delete a project. - */ - @DELETE - @Path("{project}") - @Transactional - fun delete(@PathParam("project") id: Long): Project { - try { - return projectService.deleteWithUser(identity.principal.name, id) ?: throw WebApplicationException("Project not found", 404) - } catch (e: IllegalArgumentException) { - throw WebApplicationException(e.message, 403) - } - } -} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/ScenarioResource.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/ScenarioResource.kt deleted file mode 100644 index 56bb4290..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/ScenarioResource.kt +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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.server.rest.user - -import io.quarkus.security.identity.SecurityIdentity -import org.opendc.web.proto.user.Scenario -import org.opendc.web.server.service.ScenarioService -import javax.annotation.security.RolesAllowed -import javax.inject.Inject -import javax.transaction.Transactional -import javax.ws.rs.DELETE -import javax.ws.rs.GET -import javax.ws.rs.Path -import javax.ws.rs.PathParam -import javax.ws.rs.WebApplicationException - -/** - * A resource representing the scenarios of a portfolio. - */ -@Path("/projects/{project}/scenarios") -@RolesAllowed("openid") -class ScenarioResource @Inject constructor( - private val scenarioService: ScenarioService, - private val identity: SecurityIdentity -) { - /** - * Obtain a scenario by its identifier. - */ - @GET - @Path("{scenario}") - fun get(@PathParam("project") projectId: Long, @PathParam("scenario") number: Int): Scenario { - return scenarioService.findOne(identity.principal.name, projectId, number) ?: throw WebApplicationException("Scenario not found", 404) - } - - /** - * Delete a scenario. - */ - @DELETE - @Path("{scenario}") - @Transactional - fun delete(@PathParam("project") projectId: Long, @PathParam("scenario") number: Int): Scenario { - return scenarioService.delete(identity.principal.name, projectId, number) ?: throw WebApplicationException("Scenario not found", 404) - } -} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/TopologyResource.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/TopologyResource.kt deleted file mode 100644 index 8eef66c8..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/TopologyResource.kt +++ /dev/null @@ -1,94 +0,0 @@ -/* - * 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.server.rest.user - -import io.quarkus.security.identity.SecurityIdentity -import org.opendc.web.proto.user.Topology -import org.opendc.web.server.service.TopologyService -import javax.annotation.security.RolesAllowed -import javax.inject.Inject -import javax.transaction.Transactional -import javax.validation.Valid -import javax.ws.rs.DELETE -import javax.ws.rs.GET -import javax.ws.rs.POST -import javax.ws.rs.PUT -import javax.ws.rs.Path -import javax.ws.rs.PathParam -import javax.ws.rs.WebApplicationException - -/** - * A resource representing the constructed datacenter topologies. - */ -@Path("/projects/{project}/topologies") -@RolesAllowed("openid") -class TopologyResource @Inject constructor( - private val topologyService: TopologyService, - private val identity: SecurityIdentity -) { - /** - * Get all topologies that belong to the specified project. - */ - @GET - fun getAll(@PathParam("project") projectId: Long): List<Topology> { - return topologyService.findAll(identity.principal.name, projectId) - } - - /** - * Create a topology for this project. - */ - @POST - @Transactional - fun create(@PathParam("project") projectId: Long, @Valid request: Topology.Create): Topology { - return topologyService.create(identity.principal.name, projectId, request) ?: throw WebApplicationException("Topology not found", 404) - } - - /** - * Obtain a topology by its number. - */ - @GET - @Path("{topology}") - fun get(@PathParam("project") projectId: Long, @PathParam("topology") number: Int): Topology { - return topologyService.findOne(identity.principal.name, projectId, number) ?: throw WebApplicationException("Topology not found", 404) - } - - /** - * Update the specified topology by its number. - */ - @PUT - @Path("{topology}") - @Transactional - fun update(@PathParam("project") projectId: Long, @PathParam("topology") number: Int, @Valid request: Topology.Update): Topology { - return topologyService.update(identity.principal.name, projectId, number, request) ?: throw WebApplicationException("Topology not found", 404) - } - - /** - * Delete the specified topology. - */ - @Path("{topology}") - @DELETE - @Transactional - fun delete(@PathParam("project") projectId: Long, @PathParam("topology") number: Int): Topology { - return topologyService.delete(identity.principal.name, projectId, number) ?: throw WebApplicationException("Topology not found", 404) - } -} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/UserResource.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/UserResource.kt deleted file mode 100644 index d640cc08..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/rest/user/UserResource.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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.server.rest.user - -import io.quarkus.security.identity.SecurityIdentity -import org.opendc.web.proto.user.User -import org.opendc.web.server.service.UserService -import javax.annotation.security.RolesAllowed -import javax.inject.Inject -import javax.ws.rs.GET -import javax.ws.rs.Path - -/** - * A resource representing the active user. - */ -@Path("/users") -@RolesAllowed("openid") -class UserResource @Inject constructor(private val userService: UserService, private val identity: SecurityIdentity) { - /** - * Get the current active user data. - */ - @GET - @Path("me") - fun get(): User = userService.getUser(identity) -} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/JobService.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/JobService.kt deleted file mode 100644 index a0ebd4f4..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/JobService.kt +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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.server.service - -import org.opendc.web.proto.JobState -import org.opendc.web.proto.runner.Job -import org.opendc.web.server.repository.JobRepository -import java.time.Instant -import javax.enterprise.context.ApplicationScoped -import javax.inject.Inject - -/** - * Service for managing [Job]s. - */ -@ApplicationScoped -class JobService @Inject constructor( - private val repository: JobRepository, - private val accountingService: UserAccountingService -) { - /** - * Query the pending simulation jobs. - */ - fun queryPending(): List<Job> { - return repository.findAll(JobState.PENDING).map { it.toRunnerDto() } - } - - /** - * Find a job by its identifier. - */ - fun findById(id: Long): Job? { - return repository.findOne(id)?.toRunnerDto() - } - - /** - * Atomically update the state of a [Job]. - * - * @param id The identifier of the job. - * @param newState The next state for the job. - * @param runtime The runtime of the job (in seconds). - * @param results The potential results of the job. - */ - fun updateState(id: Long, newState: JobState, runtime: Int, results: Map<String, Any>?): Job? { - val entity = repository.findOne(id) ?: return null - val state = entity.state - if (!state.isTransitionLegal(newState)) { - throw IllegalArgumentException("Invalid transition from $state to $newState") - } - - val now = Instant.now() - var nextState = newState - val consumedBudget = (runtime - entity.runtime).coerceAtLeast(1) - - // Check whether the user still has any simulation budget left - if (accountingService.consumeSimulationBudget(entity.createdBy, consumedBudget) && nextState == JobState.RUNNING) { - nextState = JobState.FAILED // User has consumed all their budget; cancel the job - } - - if (!repository.updateOne(entity, nextState, now, runtime, results)) { - throw IllegalStateException("Conflicting update") - } - - return entity.toRunnerDto() - } - - /** - * Determine whether the transition from [this] to [newState] is legal. - */ - private fun JobState.isTransitionLegal(newState: JobState): Boolean { - // Note that we always allow transitions from the state - return newState == this || when (this) { - JobState.PENDING -> newState == JobState.CLAIMED - JobState.CLAIMED -> newState == JobState.RUNNING || newState == JobState.FAILED - JobState.RUNNING -> newState == JobState.FINISHED || newState == JobState.FAILED - JobState.FINISHED, JobState.FAILED -> false - } - } -} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/PortfolioService.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/PortfolioService.kt deleted file mode 100644 index c83b7a54..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/PortfolioService.kt +++ /dev/null @@ -1,103 +0,0 @@ -/* - * 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.server.service - -import org.opendc.web.proto.user.Portfolio -import org.opendc.web.server.repository.PortfolioRepository -import org.opendc.web.server.repository.ProjectRepository -import java.time.Instant -import javax.enterprise.context.ApplicationScoped -import javax.inject.Inject -import org.opendc.web.server.model.Portfolio as PortfolioEntity - -/** - * Service for managing [Portfolio]s. - */ -@ApplicationScoped -class PortfolioService @Inject constructor( - private val projectRepository: ProjectRepository, - private val portfolioRepository: PortfolioRepository -) { - /** - * List all [Portfolio]s that belong a certain project. - */ - fun findAll(userId: String, projectId: Long): List<Portfolio> { - // User must have access to project - val auth = projectRepository.findOne(userId, projectId) ?: return emptyList() - val project = auth.toUserDto() - return portfolioRepository.findAll(projectId).map { it.toUserDto(project) } - } - - /** - * Find a [Portfolio] with the specified [number] belonging to [project][projectId]. - */ - fun findOne(userId: String, projectId: Long, number: Int): Portfolio? { - // User must have access to project - val auth = projectRepository.findOne(userId, projectId) ?: return null - return portfolioRepository.findOne(projectId, number)?.toUserDto(auth.toUserDto()) - } - - /** - * Delete the portfolio with the specified [number] belonging to [project][projectId]. - */ - fun delete(userId: String, projectId: Long, number: Int): Portfolio? { - // User must have access to project - val auth = projectRepository.findOne(userId, projectId) - - if (auth == null) { - return null - } else if (!auth.role.canEdit) { - throw IllegalStateException("Not permitted to edit project") - } - - val entity = portfolioRepository.findOne(projectId, number) ?: return null - val portfolio = entity.toUserDto(auth.toUserDto()) - portfolioRepository.delete(entity) - return portfolio - } - - /** - * Construct a new [Portfolio] with the specified name. - */ - fun create(userId: String, projectId: Long, request: Portfolio.Create): Portfolio? { - // User must have access to project - val auth = projectRepository.findOne(userId, projectId) - - if (auth == null) { - return null - } else if (!auth.role.canEdit) { - throw IllegalStateException("Not permitted to edit project") - } - - val now = Instant.now() - val project = auth.project - val number = projectRepository.allocatePortfolio(auth.project, now) - - val portfolio = PortfolioEntity(0, number, request.name, project, request.targets) - - project.portfolios.add(portfolio) - portfolioRepository.save(portfolio) - - return portfolio.toUserDto(auth.toUserDto()) - } -} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/ProjectService.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/ProjectService.kt deleted file mode 100644 index 2fc5a054..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/ProjectService.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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.server.service - -import org.opendc.web.proto.user.ProjectRole -import org.opendc.web.server.model.Project -import org.opendc.web.server.model.ProjectAuthorization -import org.opendc.web.server.model.ProjectAuthorizationKey -import org.opendc.web.server.repository.ProjectRepository -import java.time.Instant -import javax.enterprise.context.ApplicationScoped -import javax.inject.Inject -import org.opendc.web.proto.user.Project as ProjectDto - -/** - * Service for managing [Project]s. - */ -@ApplicationScoped -class ProjectService @Inject constructor(private val repository: ProjectRepository) { - /** - * List all projects for the user with the specified [userId]. - */ - fun findWithUser(userId: String): List<ProjectDto> { - return repository.findAll(userId).map { it.toUserDto() } - } - - /** - * Obtain the project with the specified [id] for the user with the specified [userId]. - */ - fun findWithUser(userId: String, id: Long): ProjectDto? { - return repository.findOne(userId, id)?.toUserDto() - } - - /** - * Create a new [Project] for the user with the specified [userId]. - */ - fun createForUser(userId: String, name: String): ProjectDto { - val now = Instant.now() - val entity = Project(0, name, now) - repository.save(entity) - - val authorization = ProjectAuthorization(ProjectAuthorizationKey(userId, entity.id), entity, ProjectRole.OWNER) - - entity.authorizations.add(authorization) - repository.save(authorization) - - return authorization.toUserDto() - } - - /** - * Delete a project by its identifier. - * - * @param userId The user that invokes the action. - * @param id The identifier of the project. - */ - fun deleteWithUser(userId: String, id: Long): ProjectDto? { - val auth = repository.findOne(userId, id) ?: return null - - if (!auth.role.canDelete) { - throw IllegalArgumentException("Not allowed to delete project") - } - - val now = Instant.now() - val project = auth.toUserDto().copy(updatedAt = now) - repository.delete(auth.project) - return project - } -} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/RunnerConversions.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/RunnerConversions.kt deleted file mode 100644 index 465ac2df..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/RunnerConversions.kt +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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.server.service - -import org.opendc.web.server.model.Job -import org.opendc.web.server.model.Portfolio -import org.opendc.web.server.model.Scenario -import org.opendc.web.server.model.Topology - -/** - * Conversions into DTOs provided to OpenDC runners. - */ - -/** - * Convert a [Topology] into a runner-facing DTO. - */ -internal fun Topology.toRunnerDto(): org.opendc.web.proto.runner.Topology { - return org.opendc.web.proto.runner.Topology(id, number, name, rooms, createdAt, updatedAt) -} - -/** - * Convert a [Portfolio] into a runner-facing DTO. - */ -internal fun Portfolio.toRunnerDto(): org.opendc.web.proto.runner.Portfolio { - return org.opendc.web.proto.runner.Portfolio(id, number, name, targets) -} - -/** - * Convert a [Job] into a runner-facing DTO. - */ -internal fun Job.toRunnerDto(): org.opendc.web.proto.runner.Job { - return org.opendc.web.proto.runner.Job(id, scenario.toRunnerDto(), state, createdAt, updatedAt, runtime, results) -} - -/** - * Convert a [Job] into a runner-facing DTO. - */ -internal fun Scenario.toRunnerDto(): org.opendc.web.proto.runner.Scenario { - return org.opendc.web.proto.runner.Scenario( - id, - number, - portfolio.toRunnerDto(), - name, - workload.toDto(), - topology.toRunnerDto(), - phenomena, - schedulerName - ) -} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/ScenarioService.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/ScenarioService.kt deleted file mode 100644 index 083f2451..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/ScenarioService.kt +++ /dev/null @@ -1,141 +0,0 @@ -/* - * 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.server.service - -import org.opendc.web.proto.JobState -import org.opendc.web.server.model.Job -import org.opendc.web.server.model.Scenario -import org.opendc.web.server.model.Workload -import org.opendc.web.server.repository.PortfolioRepository -import org.opendc.web.server.repository.ProjectRepository -import org.opendc.web.server.repository.ScenarioRepository -import org.opendc.web.server.repository.TopologyRepository -import org.opendc.web.server.repository.TraceRepository -import java.time.Instant -import javax.enterprise.context.ApplicationScoped -import javax.inject.Inject -import org.opendc.web.proto.user.Scenario as ScenarioDto - -/** - * Service for managing [Scenario]s. - */ -@ApplicationScoped -class ScenarioService @Inject constructor( - private val projectRepository: ProjectRepository, - private val portfolioRepository: PortfolioRepository, - private val topologyRepository: TopologyRepository, - private val traceRepository: TraceRepository, - private val scenarioRepository: ScenarioRepository, - private val accountingService: UserAccountingService -) { - /** - * List all [Scenario]s that belong a certain portfolio. - */ - fun findAll(userId: String, projectId: Long, number: Int): List<ScenarioDto> { - // User must have access to project - val auth = projectRepository.findOne(userId, projectId) ?: return emptyList() - val project = auth.toUserDto() - return scenarioRepository.findAll(projectId).map { it.toUserDto(project) } - } - - /** - * Obtain a [Scenario] by identifier. - */ - fun findOne(userId: String, projectId: Long, number: Int): ScenarioDto? { - // User must have access to project - val auth = projectRepository.findOne(userId, projectId) ?: return null - val project = auth.toUserDto() - return scenarioRepository.findOne(projectId, number)?.toUserDto(project) - } - - /** - * Delete the specified scenario. - */ - fun delete(userId: String, projectId: Long, number: Int): ScenarioDto? { - // User must have access to project - val auth = projectRepository.findOne(userId, projectId) - - if (auth == null) { - return null - } else if (!auth.role.canEdit) { - throw IllegalStateException("Not permitted to edit project") - } - - val entity = scenarioRepository.findOne(projectId, number) ?: return null - val scenario = entity.toUserDto(auth.toUserDto()) - scenarioRepository.delete(entity) - return scenario - } - - /** - * Construct a new [Scenario] with the specified data. - */ - fun create(userId: String, projectId: Long, portfolioNumber: Int, request: ScenarioDto.Create): ScenarioDto? { - // User must have access to project - val auth = projectRepository.findOne(userId, projectId) - - if (auth == null) { - return null - } else if (!auth.role.canEdit) { - throw IllegalStateException("Not permitted to edit project") - } - - val portfolio = portfolioRepository.findOne(projectId, portfolioNumber) ?: return null - val topology = requireNotNull( - topologyRepository.findOne( - projectId, - request.topology.toInt() - ) - ) { "Referred topology does not exist" } - val trace = - requireNotNull(traceRepository.findOne(request.workload.trace)) { "Referred trace does not exist" } - - val now = Instant.now() - val project = auth.project - val number = projectRepository.allocateScenario(auth.project, now) - - val scenario = Scenario( - 0, - number, - request.name, - project, - portfolio, - Workload(trace, request.workload.samplingFraction), - topology, - request.phenomena, - request.schedulerName - ) - val job = Job(0, userId, scenario, now, portfolio.targets.repeats) - - // Fail the job if there is not enough budget for the simulation - if (!accountingService.hasSimulationBudget(userId)) { - job.state = JobState.FAILED - } - - scenario.job = job - portfolio.scenarios.add(scenario) - scenarioRepository.save(scenario) - - return scenario.toUserDto(auth.toUserDto()) - } -} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/TopologyService.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/TopologyService.kt deleted file mode 100644 index 5c2a457a..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/TopologyService.kt +++ /dev/null @@ -1,127 +0,0 @@ -/* - * 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.server.service - -import org.opendc.web.proto.user.Topology -import org.opendc.web.server.repository.ProjectRepository -import org.opendc.web.server.repository.TopologyRepository -import java.time.Instant -import javax.enterprise.context.ApplicationScoped -import javax.inject.Inject -import org.opendc.web.server.model.Topology as TopologyEntity - -/** - * Service for managing [Topology]s. - */ -@ApplicationScoped -class TopologyService @Inject constructor( - private val projectRepository: ProjectRepository, - private val topologyRepository: TopologyRepository -) { - /** - * List all [Topology]s that belong a certain project. - */ - fun findAll(userId: String, projectId: Long): List<Topology> { - // User must have access to project - val auth = projectRepository.findOne(userId, projectId) ?: return emptyList() - val project = auth.toUserDto() - return topologyRepository.findAll(projectId).map { it.toUserDto(project) } - } - - /** - * Find the [Topology] with the specified [number] belonging to [project][projectId]. - */ - fun findOne(userId: String, projectId: Long, number: Int): Topology? { - // User must have access to project - val auth = projectRepository.findOne(userId, projectId) ?: return null - return topologyRepository.findOne(projectId, number)?.toUserDto(auth.toUserDto()) - } - - /** - * Delete the [Topology] with the specified [number] belonging to [project][projectId]. - */ - fun delete(userId: String, projectId: Long, number: Int): Topology? { - // User must have access to project - val auth = projectRepository.findOne(userId, projectId) - - if (auth == null) { - return null - } else if (!auth.role.canEdit) { - throw IllegalStateException("Not permitted to edit project") - } - - val entity = topologyRepository.findOne(projectId, number) ?: return null - val now = Instant.now() - val topology = entity.toUserDto(auth.toUserDto()).copy(updatedAt = now) - topologyRepository.delete(entity) - - return topology - } - - /** - * Update a [Topology] with the specified [number] belonging to [project][projectId]. - */ - fun update(userId: String, projectId: Long, number: Int, request: Topology.Update): Topology? { - // User must have access to project - val auth = projectRepository.findOne(userId, projectId) - - if (auth == null) { - return null - } else if (!auth.role.canEdit) { - throw IllegalStateException("Not permitted to edit project") - } - - val entity = topologyRepository.findOne(projectId, number) ?: return null - val now = Instant.now() - - entity.updatedAt = now - entity.rooms = request.rooms - - return entity.toUserDto(auth.toUserDto()) - } - - /** - * Construct a new [Topology] with the specified name. - */ - fun create(userId: String, projectId: Long, request: Topology.Create): Topology? { - // User must have access to project - val auth = projectRepository.findOne(userId, projectId) - - if (auth == null) { - return null - } else if (!auth.role.canEdit) { - throw IllegalStateException("Not permitted to edit project") - } - - val now = Instant.now() - val project = auth.project - val number = projectRepository.allocateTopology(auth.project, now) - - val topology = TopologyEntity(0, number, request.name, project, now, request.rooms) - - project.topologies.add(topology) - topologyRepository.save(topology) - - return topology.toUserDto(auth.toUserDto()) - } -} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserAccountingService.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserAccountingService.kt deleted file mode 100644 index 11066bfb..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserAccountingService.kt +++ /dev/null @@ -1,128 +0,0 @@ -/* - * 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.server.service - -import org.eclipse.microprofile.config.inject.ConfigProperty -import org.opendc.web.server.model.UserAccounting -import org.opendc.web.server.repository.UserAccountingRepository -import java.time.Duration -import java.time.LocalDate -import java.time.temporal.TemporalAdjusters -import javax.enterprise.context.ApplicationScoped -import javax.inject.Inject -import javax.persistence.EntityExistsException -import org.opendc.web.proto.user.UserAccounting as UserAccountingDto - -/** - * Service for tracking the simulation budget of users. - * - * @param repository The [UserAccountingRepository] used to communicate with the database. - * @param simulationBudget The default simulation budget for new users. - */ -@ApplicationScoped -class UserAccountingService @Inject constructor( - private val repository: UserAccountingRepository, - @ConfigProperty(name = "opendc.accounting.simulation-budget", defaultValue = "2000m") - private val simulationBudget: Duration -) { - /** - * Return the [UserAccountingDto] object for the user with the specified [userId]. If the object does not exist in the - * database, a default value is constructed. - */ - fun getAccounting(userId: String): UserAccountingDto { - val accounting = repository.findForUser(userId) - return if (accounting != null) { - UserAccountingDto(accounting.periodEnd, accounting.simulationTime, accounting.simulationTimeBudget) - } else { - UserAccountingDto(getNextAccountingPeriod(), 0, simulationBudget.toSeconds().toInt()) - } - } - - /** - * Determine whether the user with [userId] has any remaining simulation budget. - * - * @param userId The unique identifier of the user. - * @return `true` when the user still has budget left, `false` otherwise. - */ - fun hasSimulationBudget(userId: String): Boolean { - val accounting = repository.findForUser(userId) ?: return true - val today = LocalDate.now() - - // The accounting period must be over or there must be budget remaining. - return !today.isBefore(accounting.periodEnd) || accounting.simulationTimeBudget > accounting.simulationTime - } - - /** - * Consume [seconds] from the simulation budget of the user with [userId]. - * - * @param userId The unique identifier of the user. - * @param seconds The seconds to consume from the simulation budget. - * @param `true` if the user has consumed his full budget or `false` if there is still budget remaining. - */ - fun consumeSimulationBudget(userId: String, seconds: Int): Boolean { - val today = LocalDate.now() - val nextAccountingPeriod = getNextAccountingPeriod(today) - val repository = repository - - // We need to be careful to prevent conflicts in case of concurrency - // 1. First, we try to create the accounting object if it does not exist yet. This may fail if another instance - // creates the object concurrently. - // 2. Second, we check if the budget needs to be reset and try this atomically. - // 3. Finally, we atomically consume the budget from the object - // This is repeated three times in case there is a conflict - repeat(3) { - val accounting = repository.findForUser(userId) - - if (accounting == null) { - try { - val newAccounting = UserAccounting(userId, nextAccountingPeriod, simulationBudget.toSeconds().toInt()) - newAccounting.simulationTime = seconds - repository.save(newAccounting) - - return newAccounting.simulationTime >= newAccounting.simulationTimeBudget - } catch (e: EntityExistsException) { - // Conflict due to concurrency; retry - } - } else { - val success = if (!today.isBefore(accounting.periodEnd)) { - repository.resetBudget(accounting, nextAccountingPeriod, seconds) - } else { - repository.consumeBudget(accounting, seconds) - } - - if (success) { - return accounting.simulationTimeBudget <= accounting.simulationTime - } - } - } - - throw IllegalStateException("Failed to allocate consume budget due to conflict") - } - - /** - * Helper method to find next accounting period. - */ - private fun getNextAccountingPeriod(today: LocalDate = LocalDate.now()): LocalDate { - return today.with(TemporalAdjusters.firstDayOfNextMonth()) - } -} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserConversions.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserConversions.kt deleted file mode 100644 index e28d9c0f..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserConversions.kt +++ /dev/null @@ -1,126 +0,0 @@ -/* - * 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.server.service - -import org.opendc.web.proto.user.Project -import org.opendc.web.server.model.Job -import org.opendc.web.server.model.Portfolio -import org.opendc.web.server.model.ProjectAuthorization -import org.opendc.web.server.model.Scenario -import org.opendc.web.server.model.Topology -import org.opendc.web.server.model.Trace -import org.opendc.web.server.model.Workload - -/** - * Conversions into DTOs provided to users. - */ - -/** - * Convert a [Trace] entity into a [org.opendc.web.proto.Trace] DTO. - */ -internal fun Trace.toUserDto(): org.opendc.web.proto.Trace { - return org.opendc.web.proto.Trace(id, name, type) -} - -/** - * Convert a [ProjectAuthorization] entity into a [Project] DTO. - */ -internal fun ProjectAuthorization.toUserDto(): Project { - return Project(project.id, project.name, project.createdAt, project.updatedAt, role) -} - -/** - * Convert a [Topology] entity into a [org.opendc.web.proto.user.Topology] DTO. - */ -internal fun Topology.toUserDto(project: Project): org.opendc.web.proto.user.Topology { - return org.opendc.web.proto.user.Topology(id, number, project, name, rooms, createdAt, updatedAt) -} - -/** - * Convert a [Topology] entity into a [org.opendc.web.proto.user.Topology.Summary] DTO. - */ -private fun Topology.toSummaryDto(): org.opendc.web.proto.user.Topology.Summary { - return org.opendc.web.proto.user.Topology.Summary(id, number, name, createdAt, updatedAt) -} - -/** - * Convert a [Portfolio] entity into a [org.opendc.web.proto.user.Portfolio] DTO. - */ -internal fun Portfolio.toUserDto(project: Project): org.opendc.web.proto.user.Portfolio { - return org.opendc.web.proto.user.Portfolio(id, number, project, name, targets, scenarios.map { it.toSummaryDto() }) -} - -/** - * Convert a [Portfolio] entity into a [org.opendc.web.proto.user.Portfolio.Summary] DTO. - */ -private fun Portfolio.toSummaryDto(): org.opendc.web.proto.user.Portfolio.Summary { - return org.opendc.web.proto.user.Portfolio.Summary(id, number, name, targets) -} - -/** - * Convert a [Scenario] entity into a [org.opendc.web.proto.user.Scenario] DTO. - */ -internal fun Scenario.toUserDto(project: Project): org.opendc.web.proto.user.Scenario { - return org.opendc.web.proto.user.Scenario( - id, - number, - project, - portfolio.toSummaryDto(), - name, - workload.toDto(), - topology.toSummaryDto(), - phenomena, - schedulerName, - job.toUserDto() - ) -} - -/** - * Convert a [Scenario] entity into a [org.opendc.web.proto.user.Scenario.Summary] DTO. - */ -private fun Scenario.toSummaryDto(): org.opendc.web.proto.user.Scenario.Summary { - return org.opendc.web.proto.user.Scenario.Summary( - id, - number, - name, - workload.toDto(), - topology.toSummaryDto(), - phenomena, - schedulerName, - job.toUserDto() - ) -} - -/** - * Convert a [Job] entity into a [org.opendc.web.proto.user.Job] DTO. - */ -internal fun Job.toUserDto(): org.opendc.web.proto.user.Job { - return org.opendc.web.proto.user.Job(id, state, createdAt, updatedAt, results) -} - -/** - * Convert a [Workload] entity into a DTO. - */ -internal fun Workload.toDto(): org.opendc.web.proto.Workload { - return org.opendc.web.proto.Workload(trace.toUserDto(), samplingFraction) -} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserService.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserService.kt deleted file mode 100644 index 39352267..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/service/UserService.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2021 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.server.service - -import io.quarkus.security.identity.SecurityIdentity -import org.opendc.web.proto.user.User -import javax.enterprise.context.ApplicationScoped -import javax.inject.Inject - -/** - * Service for managing [User]s. - */ -@ApplicationScoped -class UserService @Inject constructor(private val accounting: UserAccountingService) { - /** - * Obtain the [User] object for the specified [identity]. - */ - fun getUser(identity: SecurityIdentity): User { - val userId = identity.principal.name - val accounting = accounting.getAccounting(userId) - - return User(userId, accounting) - } -} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/AbstractJsonSqlTypeDescriptor.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/AbstractJsonSqlTypeDescriptor.kt deleted file mode 100644 index 9e29b734..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/AbstractJsonSqlTypeDescriptor.kt +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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.server.util.hibernate.json - -import org.hibernate.type.descriptor.ValueExtractor -import org.hibernate.type.descriptor.WrapperOptions -import org.hibernate.type.descriptor.java.JavaTypeDescriptor -import org.hibernate.type.descriptor.sql.BasicExtractor -import org.hibernate.type.descriptor.sql.SqlTypeDescriptor -import java.sql.CallableStatement -import java.sql.ResultSet -import java.sql.Types - -/** - * Abstract implementation of a [SqlTypeDescriptor] for Hibernate JSON type. - */ -internal abstract class AbstractJsonSqlTypeDescriptor : SqlTypeDescriptor { - - override fun getSqlType(): Int { - return Types.OTHER - } - - override fun canBeRemapped(): Boolean { - return true - } - - override fun <X> getExtractor(typeDescriptor: JavaTypeDescriptor<X>): ValueExtractor<X> { - return object : BasicExtractor<X>(typeDescriptor, this) { - override fun doExtract(rs: ResultSet, name: String, options: WrapperOptions): X { - return typeDescriptor.wrap(extractJson(rs, name), options) - } - - override fun doExtract(statement: CallableStatement, index: Int, options: WrapperOptions): X { - return typeDescriptor.wrap(extractJson(statement, index), options) - } - - override fun doExtract(statement: CallableStatement, name: String, options: WrapperOptions): X { - return typeDescriptor.wrap(extractJson(statement, name), options) - } - } - } - - open fun extractJson(rs: ResultSet, name: String): Any? { - return rs.getObject(name) - } - - open fun extractJson(statement: CallableStatement, index: Int): Any? { - return statement.getObject(index) - } - - open fun extractJson(statement: CallableStatement, name: String): Any? { - return statement.getObject(name) - } -} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonBinarySqlTypeDescriptor.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonBinarySqlTypeDescriptor.kt deleted file mode 100644 index df6a3013..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonBinarySqlTypeDescriptor.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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.server.util.hibernate.json - -import com.fasterxml.jackson.databind.JsonNode -import org.hibernate.type.descriptor.ValueBinder -import org.hibernate.type.descriptor.WrapperOptions -import org.hibernate.type.descriptor.java.JavaTypeDescriptor -import org.hibernate.type.descriptor.sql.BasicBinder -import java.sql.CallableStatement -import java.sql.PreparedStatement - -/** - * A [AbstractJsonSqlTypeDescriptor] that stores the JSON as binary (JSONB). - */ -internal object JsonBinarySqlTypeDescriptor : AbstractJsonSqlTypeDescriptor() { - override fun <X> getBinder(typeDescriptor: JavaTypeDescriptor<X>): ValueBinder<X> { - return object : BasicBinder<X>(typeDescriptor, this) { - override fun doBind(st: PreparedStatement, value: X, index: Int, options: WrapperOptions) { - st.setObject(index, typeDescriptor.unwrap(value, JsonNode::class.java, options), sqlType) - } - - override fun doBind(st: CallableStatement, value: X, name: String, options: WrapperOptions) { - st.setObject(name, typeDescriptor.unwrap(value, JsonNode::class.java, options), sqlType) - } - } - } -} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonBytesSqlTypeDescriptor.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonBytesSqlTypeDescriptor.kt deleted file mode 100644 index 4924f586..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonBytesSqlTypeDescriptor.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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.server.util.hibernate.json - -import org.hibernate.type.descriptor.ValueBinder -import org.hibernate.type.descriptor.WrapperOptions -import org.hibernate.type.descriptor.java.JavaTypeDescriptor -import org.hibernate.type.descriptor.sql.BasicBinder -import java.io.UnsupportedEncodingException -import java.sql.CallableStatement -import java.sql.PreparedStatement -import java.sql.ResultSet -import java.sql.Types - -/** - * A [AbstractJsonSqlTypeDescriptor] that stores the JSON as UTF-8 encoded bytes. - */ -internal object JsonBytesSqlTypeDescriptor : AbstractJsonSqlTypeDescriptor() { - private val CHARSET = Charsets.UTF_8 - - override fun getSqlType(): Int { - return Types.BINARY - } - - override fun <X> getBinder(javaTypeDescriptor: JavaTypeDescriptor<X>): ValueBinder<X> { - return object : BasicBinder<X>(javaTypeDescriptor, this) { - override fun doBind(st: PreparedStatement, value: X, index: Int, options: WrapperOptions) { - st.setBytes(index, toJsonBytes(javaTypeDescriptor.unwrap(value, String::class.java, options))) - } - - override fun doBind(st: CallableStatement, value: X, name: String, options: WrapperOptions) { - st.setBytes(name, toJsonBytes(javaTypeDescriptor.unwrap(value, String::class.java, options))) - } - } - } - - override fun extractJson(rs: ResultSet, name: String): Any? { - return fromJsonBytes(rs.getBytes(name)) - } - - override fun extractJson(statement: CallableStatement, index: Int): Any? { - return fromJsonBytes(statement.getBytes(index)) - } - - override fun extractJson(statement: CallableStatement, name: String): Any? { - return fromJsonBytes(statement.getBytes(name)) - } - - private fun toJsonBytes(jsonValue: String): ByteArray? { - return try { - jsonValue.toByteArray(CHARSET) - } catch (e: UnsupportedEncodingException) { - throw IllegalStateException(e) - } - } - - private fun fromJsonBytes(jsonBytes: ByteArray?): String? { - return if (jsonBytes == null) { - null - } else { - try { - String(jsonBytes, CHARSET) - } catch (e: UnsupportedEncodingException) { - throw IllegalStateException(e) - } - } - } -} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonSqlTypeDescriptor.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonSqlTypeDescriptor.kt deleted file mode 100644 index bd22ffbe..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonSqlTypeDescriptor.kt +++ /dev/null @@ -1,110 +0,0 @@ -/* - * 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.server.util.hibernate.json - -import org.hibernate.dialect.H2Dialect -import org.hibernate.dialect.PostgreSQL81Dialect -import org.hibernate.internal.SessionImpl -import org.hibernate.type.descriptor.ValueBinder -import org.hibernate.type.descriptor.ValueExtractor -import org.hibernate.type.descriptor.WrapperOptions -import org.hibernate.type.descriptor.java.JavaTypeDescriptor -import org.hibernate.type.descriptor.sql.BasicBinder -import org.hibernate.type.descriptor.sql.BasicExtractor -import org.hibernate.type.descriptor.sql.SqlTypeDescriptor -import java.sql.CallableStatement -import java.sql.PreparedStatement -import java.sql.ResultSet -import java.sql.Types - -/** - * A [SqlTypeDescriptor] that automatically selects the correct implementation for the database dialect. - */ -internal object JsonSqlTypeDescriptor : SqlTypeDescriptor { - - override fun getSqlType(): Int = Types.OTHER - - override fun canBeRemapped(): Boolean = true - - override fun <X> getExtractor(javaTypeDescriptor: JavaTypeDescriptor<X>): ValueExtractor<X> { - return object : BasicExtractor<X>(javaTypeDescriptor, this) { - private var delegate: AbstractJsonSqlTypeDescriptor? = null - - override fun doExtract(rs: ResultSet, name: String, options: WrapperOptions): X { - return javaTypeDescriptor.wrap(delegate(options).extractJson(rs, name), options) - } - - override fun doExtract(statement: CallableStatement, index: Int, options: WrapperOptions): X { - return javaTypeDescriptor.wrap(delegate(options).extractJson(statement, index), options) - } - - override fun doExtract(statement: CallableStatement, name: String, options: WrapperOptions): X { - return javaTypeDescriptor.wrap(delegate(options).extractJson(statement, name), options) - } - - private fun delegate(options: WrapperOptions): AbstractJsonSqlTypeDescriptor { - var delegate = delegate - if (delegate == null) { - delegate = resolveSqlTypeDescriptor(options) - this.delegate = delegate - } - return delegate - } - } - } - - override fun <X> getBinder(javaTypeDescriptor: JavaTypeDescriptor<X>): ValueBinder<X> { - return object : BasicBinder<X>(javaTypeDescriptor, this) { - private var delegate: ValueBinder<X>? = null - - override fun doBind(st: PreparedStatement, value: X, index: Int, options: WrapperOptions) { - delegate(options).bind(st, value, index, options) - } - - override fun doBind(st: CallableStatement, value: X, name: String, options: WrapperOptions) { - delegate(options).bind(st, value, name, options) - } - - private fun delegate(options: WrapperOptions): ValueBinder<X> { - var delegate = delegate - if (delegate == null) { - delegate = checkNotNull(resolveSqlTypeDescriptor(options).getBinder(javaTypeDescriptor)) - this.delegate = delegate - } - return delegate - } - } - } - - /** - * Helper method to resolve the appropriate [SqlTypeDescriptor] based on the [WrapperOptions]. - */ - private fun resolveSqlTypeDescriptor(options: WrapperOptions): AbstractJsonSqlTypeDescriptor { - val session = options as? SessionImpl - return when (session?.jdbcServices?.dialect) { - is PostgreSQL81Dialect -> JsonBinarySqlTypeDescriptor - is H2Dialect -> JsonBytesSqlTypeDescriptor - else -> JsonStringSqlTypeDescriptor - } - } -} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonStringSqlTypeDescriptor.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonStringSqlTypeDescriptor.kt deleted file mode 100644 index 6e015762..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonStringSqlTypeDescriptor.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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.server.util.hibernate.json - -import org.hibernate.type.descriptor.ValueBinder -import org.hibernate.type.descriptor.WrapperOptions -import org.hibernate.type.descriptor.java.JavaTypeDescriptor -import org.hibernate.type.descriptor.sql.BasicBinder -import java.sql.CallableStatement -import java.sql.PreparedStatement -import java.sql.ResultSet -import java.sql.Types - -/** - * A [AbstractJsonSqlTypeDescriptor] that stores the JSON as string (VARCHAR). - */ -internal object JsonStringSqlTypeDescriptor : AbstractJsonSqlTypeDescriptor() { - override fun getSqlType(): Int = Types.VARCHAR - - override fun <X> getBinder(typeDescriptor: JavaTypeDescriptor<X>): ValueBinder<X> { - return object : BasicBinder<X>(typeDescriptor, this) { - override fun doBind(st: PreparedStatement, value: X, index: Int, options: WrapperOptions) { - st.setString(index, typeDescriptor.unwrap(value, String::class.java, options)) - } - - override fun doBind(st: CallableStatement, value: X, name: String, options: WrapperOptions) { - st.setString(name, typeDescriptor.unwrap(value, String::class.java, options)) - } - } - } - - override fun extractJson(rs: ResultSet, name: String): Any? { - return rs.getString(name) - } - - override fun extractJson(statement: CallableStatement, index: Int): Any? { - return statement.getString(index) - } - - override fun extractJson(statement: CallableStatement, name: String): Any? { - return statement.getString(name) - } -} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonType.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonType.kt deleted file mode 100644 index 9ee21a4c..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonType.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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.server.util.hibernate.json - -import com.fasterxml.jackson.databind.ObjectMapper -import org.hibernate.type.AbstractSingleColumnStandardBasicType -import org.hibernate.type.BasicType -import org.hibernate.usertype.DynamicParameterizedType -import java.util.Properties -import javax.enterprise.inject.spi.CDI - -/** - * A [BasicType] that contains JSON. - */ -class JsonType(objectMapper: ObjectMapper) : AbstractSingleColumnStandardBasicType<Any>(JsonSqlTypeDescriptor, JsonTypeDescriptor(objectMapper)), DynamicParameterizedType { - /** - * No-arg constructor for Hibernate to instantiate. - */ - constructor() : this(CDI.current().select(ObjectMapper::class.java).get()) - - override fun getName(): String = "json" - - override fun registerUnderJavaType(): Boolean = true - - override fun setParameterValues(parameters: Properties) { - (javaTypeDescriptor as JsonTypeDescriptor).setParameterValues(parameters) - } -} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonTypeDescriptor.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonTypeDescriptor.kt deleted file mode 100644 index 9407f940..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/hibernate/json/JsonTypeDescriptor.kt +++ /dev/null @@ -1,150 +0,0 @@ -/* - * 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.server.util.hibernate.json - -import com.fasterxml.jackson.databind.ObjectMapper -import org.hibernate.HibernateException -import org.hibernate.annotations.common.reflection.XProperty -import org.hibernate.annotations.common.reflection.java.JavaXMember -import org.hibernate.engine.jdbc.BinaryStream -import org.hibernate.engine.jdbc.internal.BinaryStreamImpl -import org.hibernate.type.descriptor.WrapperOptions -import org.hibernate.type.descriptor.java.AbstractTypeDescriptor -import org.hibernate.type.descriptor.java.BlobTypeDescriptor -import org.hibernate.type.descriptor.java.DataHelper -import org.hibernate.type.descriptor.java.MutableMutabilityPlan -import org.hibernate.usertype.DynamicParameterizedType -import java.io.ByteArrayInputStream -import java.io.IOException -import java.io.InputStream -import java.lang.reflect.Type -import java.sql.Blob -import java.sql.SQLException -import java.util.Objects -import java.util.Properties - -/** - * An [AbstractTypeDescriptor] implementation for Hibernate JSON type. - */ -internal class JsonTypeDescriptor(private val objectMapper: ObjectMapper) : AbstractTypeDescriptor<Any>(Any::class.java, JsonMutabilityPlan(objectMapper)), DynamicParameterizedType { - private var type: Type? = null - - override fun setParameterValues(parameters: Properties) { - val xProperty = parameters[DynamicParameterizedType.XPROPERTY] as XProperty - type = if (xProperty is JavaXMember) { - val x = xProperty as JavaXMember - x.javaType - } else { - (parameters[DynamicParameterizedType.PARAMETER_TYPE] as DynamicParameterizedType.ParameterType).returnedClass - } - } - - override fun areEqual(one: Any?, another: Any?): Boolean { - return when { - one === another -> true - one == null || another == null -> false - one is String && another is String -> one == another - one is Collection<*> && another is Collection<*> -> Objects.equals(one, another) - else -> areJsonEqual(one, another) - } - } - - override fun toString(value: Any?): String { - return objectMapper.writeValueAsString(value) - } - - override fun fromString(string: String): Any? { - return objectMapper.readValue(string, objectMapper.typeFactory.constructType(type)) - } - - override fun <X> unwrap(value: Any?, type: Class<X>, options: WrapperOptions): X? { - if (value == null) { - return null - } - - @Suppress("UNCHECKED_CAST") - return when { - String::class.java.isAssignableFrom(type) -> toString(value) - BinaryStream::class.java.isAssignableFrom(type) || ByteArray::class.java.isAssignableFrom(type) -> { - val stringValue = if (value is String) value else toString(value) - BinaryStreamImpl(DataHelper.extractBytes(ByteArrayInputStream(stringValue.toByteArray()))) - } - Blob::class.java.isAssignableFrom(type) -> { - val stringValue = if (value is String) value else toString(value) - BlobTypeDescriptor.INSTANCE.fromString(stringValue) - } - Any::class.java.isAssignableFrom(type) -> toJsonType(value) - else -> throw unknownUnwrap(type) - } as X - } - - override fun <X> wrap(value: X?, options: WrapperOptions): Any? { - if (value == null) { - return null - } - - var blob: Blob? = null - if (Blob::class.java.isAssignableFrom(value.javaClass)) { - blob = options.lobCreator.wrap(value as Blob?) - } else if (ByteArray::class.java.isAssignableFrom(value.javaClass)) { - blob = options.lobCreator.createBlob(value as ByteArray?) - } else if (InputStream::class.java.isAssignableFrom(value.javaClass)) { - val inputStream = value as InputStream - blob = try { - options.lobCreator.createBlob(inputStream, inputStream.available().toLong()) - } catch (e: IOException) { - throw unknownWrap(value.javaClass) - } - } - - val stringValue: String = try { - if (blob != null) String(DataHelper.extractBytes(blob.binaryStream)) else value.toString() - } catch (e: SQLException) { - throw HibernateException("Unable to extract binary stream from Blob", e) - } - - return fromString(stringValue) - } - - private class JsonMutabilityPlan(private val objectMapper: ObjectMapper) : MutableMutabilityPlan<Any>() { - override fun deepCopyNotNull(value: Any): Any { - return objectMapper.treeToValue(objectMapper.valueToTree(value), value.javaClass) - } - } - - private fun readObject(value: String): Any { - return objectMapper.readTree(value) - } - - private fun areJsonEqual(one: Any, another: Any): Boolean { - return readObject(objectMapper.writeValueAsString(one)) == readObject(objectMapper.writeValueAsString(another)) - } - - private fun toJsonType(value: Any?): Any { - return try { - readObject(objectMapper.writeValueAsString(value)) - } catch (e: Exception) { - throw IllegalArgumentException(e) - } - } -} diff --git a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/runner/QuarkusJobManager.kt b/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/runner/QuarkusJobManager.kt deleted file mode 100644 index 742a510c..00000000 --- a/opendc-web/opendc-web-server/src/main/kotlin/org/opendc/web/server/util/runner/QuarkusJobManager.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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.server.util.runner - -import org.opendc.web.proto.JobState -import org.opendc.web.proto.runner.Job -import org.opendc.web.runner.JobManager -import org.opendc.web.server.service.JobService -import javax.enterprise.context.ApplicationScoped -import javax.inject.Inject -import javax.transaction.Transactional - -/** - * Implementation of [JobManager] that interfaces directly with [JobService] without overhead of the REST API. - */ -@ApplicationScoped -class QuarkusJobManager @Inject constructor(private val service: JobService) : JobManager { - @Transactional - override fun findNext(): Job? { - return service.queryPending().firstOrNull() - } - - @Transactional - override fun claim(id: Long): Boolean { - return try { - service.updateState(id, JobState.CLAIMED, 0, null) - true - } catch (e: IllegalStateException) { - false - } - } - - @Transactional - override fun heartbeat(id: Long, runtime: Int): Boolean { - val res = service.updateState(id, JobState.RUNNING, runtime, null) - return res?.state != JobState.FAILED - } - - @Transactional - override fun fail(id: Long, runtime: Int) { - service.updateState(id, JobState.FAILED, runtime, null) - } - - @Transactional - override fun finish(id: Long, runtime: Int, results: Map<String, Any>) { - service.updateState(id, JobState.FINISHED, runtime, results) - } -} diff --git a/opendc-web/opendc-web-server/src/main/resources/application-test.properties b/opendc-web/opendc-web-server/src/main/resources/application-test.properties index 338a00b9..bee17221 100644 --- a/opendc-web/opendc-web-server/src/main/resources/application-test.properties +++ b/opendc-web/opendc-web-server/src/main/resources/application-test.properties @@ -23,7 +23,9 @@ quarkus.datasource.db-kind = h2 quarkus.datasource.jdbc.url=jdbc:h2:mem:default;DB_CLOSE_DELAY=-1;INIT=CREATE TYPE IF NOT EXISTS "JSONB" AS json; quarkus.hibernate-orm.dialect=org.hibernate.dialect.H2Dialect +quarkus.hibernate-orm.log.sql=true quarkus.flyway.clean-at-start=true +quarkus.flyway.locations=db/migration,db/testing # Disable security quarkus.oidc.enabled=false diff --git a/opendc-web/opendc-web-server/src/main/resources/application.properties b/opendc-web/opendc-web-server/src/main/resources/application.properties index 40933304..0f47db30 100644 --- a/opendc-web/opendc-web-server/src/main/resources/application.properties +++ b/opendc-web/opendc-web-server/src/main/resources/application.properties @@ -20,6 +20,7 @@ # Enable CORS quarkus.http.cors=true +quarkus.http.cors.origins=http://localhost:3000,https://opendc.org # Security quarkus.oidc.enabled=${opendc.security.enabled} diff --git a/opendc-web/opendc-web-server/src/main/resources/db/migration/V1.0.0__core.sql b/opendc-web/opendc-web-server/src/main/resources/db/migration/V3.0__core.sql index 1a0e4046..40654b6b 100644 --- a/opendc-web/opendc-web-server/src/main/resources/db/migration/V1.0.0__core.sql +++ b/opendc-web/opendc-web-server/src/main/resources/db/migration/V3.0__core.sql @@ -7,19 +7,21 @@ create table projects id bigint not null, created_at timestamp not null, name varchar(255) not null, - portfolios_created integer not null, - scenarios_created integer not null, - topologies_created integer not null, + portfolios_created integer not null default 0, + scenarios_created integer not null default 0, + topologies_created integer not null default 0, updated_at timestamp not null, primary key (id) ); +create type project_role as enum ('OWNER', 'EDITOR', 'VIEWER'); + -- Project authorizations authorize users specific permissions to a project. create table project_authorizations ( project_id bigint not null, user_id varchar(255) not null, - role integer not null, + role project_role not null, primary key (project_id, user_id) ); @@ -55,7 +57,6 @@ create table scenarios phenomena jsonb not null, scheduler_name varchar(255) not null, sampling_fraction double precision not null, - job_id bigint, portfolio_id bigint not null, project_id bigint not null, topology_id bigint not null, @@ -63,16 +64,19 @@ create table scenarios primary key (id) ); +create type job_state as enum ('PENDING', 'CLAIMED', 'RUNNING', 'FINISHED', 'FAILED'); + create table jobs ( - id bigint not null, - created_by varchar(255) not null, - created_at timestamp not null, - repeats integer not null, - results jsonb, - state integer not null, - runtime integer not null, - updated_at timestamp not null, + id bigint not null, + created_by varchar(255) not null, + created_at timestamp not null, + repeats integer not null, + results jsonb, + state job_state not null default 'PENDING', + runtime integer not null default 0, + updated_at timestamp not null, + scenario_id bigint not null, primary key (id) ); @@ -97,60 +101,60 @@ create table traces -- Relations alter table project_authorizations - add constraint FK824hw0npe6gwiamwb6vohsu19 + add constraint fk_project_authorizations foreign key (project_id) references projects; -create index fn_topologies_number on topologies (project_id, number); +create index ux_topologies_number on topologies (project_id, number); alter table topologies - add constraint UK2s5na63qtu2of4g7odocmwi2a unique (project_id, number); + add constraint uk_topologies_number unique (project_id, number); alter table topologies - add constraint FK1kpw87pylq7m2ct9lq0ed1u3b + add constraint fk_topologies_project foreign key (project_id) references projects; -create index fn_portfolios_number on portfolios (project_id, number); +create index ux_portfolios_number on portfolios (project_id, number); alter table portfolios - add constraint FK31ytuaxb7aboxueng9hq7owwa + add constraint fk_portfolios_project foreign key (project_id) references projects; alter table portfolios - add constraint UK56dtskxruwj22dvxny2hfhks1 unique (project_id, number); - -create index fn_scenarios_number on scenarios (project_id, number); - -alter table scenarios - add constraint UKd0bk6fmtw5qiu9ty7t3g9crqd unique (project_id, number); + add constraint uk_portfolios_number unique (project_id, number); -alter table scenarios - add constraint FK9utvg0i5uu8db9pa17a1d77iy - foreign key (job_id) - references jobs; +create index ux_scenarios_number on scenarios (project_id, number); alter table scenarios - add constraint FK181y5hv0uibhj7fpbpkdy90s5 - foreign key (portfolio_id) - references portfolios; + add constraint uk_scenarios_number unique (project_id, number); alter table scenarios - add constraint FKbvwyh4joavs444rj270o3b8fr + add constraint fk_scenarios_project foreign key (project_id) references projects; alter table scenarios - add constraint FKrk6ltvaf9lp0aukp9dq3qjujj + add constraint fk_scenarios_topology foreign key (topology_id) references topologies; alter table scenarios - add constraint FK5m05tqeekqjkbbsaj3ehl6o8n + add constraint fk_scenarios_portfolio + foreign key (portfolio_id) + references portfolios; + +alter table scenarios + add constraint fk_scenarios_trace foreign key (trace_id) references traces; +alter table jobs + add constraint fk_scenarios_job + foreign key (scenario_id) + references scenarios; + -- Initial data insert into traces (id, name, type) values ('bitbrains-small', 'Bitbrains Small', 'vm'); diff --git a/opendc-web/opendc-web-server/src/main/resources/db/testing/V3.0.1__entities.sql b/opendc-web/opendc-web-server/src/main/resources/db/testing/V3.0.1__entities.sql new file mode 100644 index 00000000..1b702f4e --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/resources/db/testing/V3.0.1__entities.sql @@ -0,0 +1,24 @@ +-- Test entities + +alter sequence hibernate_sequence restart with 500; + +insert into projects (id, created_at, name, portfolios_created, scenarios_created, topologies_created, updated_at) +values (1, current_timestamp(), 'Test Project', 1, 2, 1, current_timestamp()); +insert into project_authorizations (project_id, user_id, role) +values (1, 'owner', 'OWNER'), + (1, 'editor', 'EDITOR'), + (1, 'viewer', 'VIEWER'); + +insert into portfolios (id, name, number, targets, project_id) +values (1, 'Test Portfolio', 1, '{ "metrics": [] }' format json, 1); + +insert into topologies (id, created_at, name, number, rooms, updated_at, project_id) +values (1, current_timestamp(), 'Test Topology', 1, '[]' format json, current_timestamp(), 1); + +insert into scenarios (id, name, number, phenomena, scheduler_name, sampling_fraction, portfolio_id, project_id, topology_id, trace_id) +values (1, 'Test Scenario', 1, '{ "failures": false, "interference": false }' format json, 'mem', 1.0, 1, 1, 1, 'bitbrains-small'), + (2, 'Test Scenario', 2, '{ "failures": false, "interference": false }' format json, 'mem', 1.0, 1, 1, 1, 'bitbrains-small'); + +insert into jobs (id, created_by, created_at, repeats, updated_at, scenario_id) +values (1, 'owner', current_timestamp(), 1, current_timestamp(), 1), + (2, 'owner', current_timestamp(), 1, current_timestamp(), 2); diff --git a/opendc-web/opendc-web-server/src/main/resources/hypersistence-utils.properties b/opendc-web/opendc-web-server/src/main/resources/hypersistence-utils.properties new file mode 100644 index 00000000..451ce2d8 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/resources/hypersistence-utils.properties @@ -0,0 +1 @@ +hypersistence.utils.jackson.object.mapper=org.opendc.web.server.util.QuarkusObjectMapperSupplier diff --git a/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/SchedulerResourceTest.kt b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/SchedulerResourceTest.java index c1460db9..feeac4d3 100644 --- a/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/SchedulerResourceTest.kt +++ b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/SchedulerResourceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 AtLarge Research + * Copyright (c) 2023 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 @@ -20,29 +20,26 @@ * SOFTWARE. */ -package org.opendc.web.server.rest +package org.opendc.web.server.rest; -import io.quarkus.test.junit.QuarkusTest -import io.restassured.http.ContentType -import io.restassured.module.kotlin.extensions.Then -import io.restassured.module.kotlin.extensions.When -import org.junit.jupiter.api.Test +import static io.restassured.RestAssured.when; + +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.Test; /** - * Test suite for [SchedulerResource] + * Test suite for {@link SchedulerResource}. */ @QuarkusTest -class SchedulerResourceTest { +@TestHTTPEndpoint(SchedulerResource.class) +public final class SchedulerResourceTest { /** * Test to verify whether we can obtain all schedulers. */ @Test - fun testGetSchedulers() { - When { - get("/schedulers") - } Then { - statusCode(200) - contentType(ContentType.JSON) - } + public void testGetSchedulers() { + when().get().then().statusCode(200).contentType(ContentType.JSON); } } diff --git a/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/TraceResourceTest.java b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/TraceResourceTest.java new file mode 100644 index 00000000..5c5976db --- /dev/null +++ b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/TraceResourceTest.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2023 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.server.rest; + +import static io.restassured.RestAssured.when; +import static org.hamcrest.Matchers.equalTo; + +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.Test; + +/** + * Test suite for {@link TraceResource}. + */ +@QuarkusTest +@TestHTTPEndpoint(TraceResource.class) +public final class TraceResourceTest { + /** + * Test that tries to obtain all traces. + */ + @Test + public void testGetAllEmpty() { + when().get().then().statusCode(200).contentType(ContentType.JSON); + } + + /** + * Test that tries to obtain a non-existent trace. + */ + @Test + public void testGetNonExisting() { + when().get("/unknown").then().statusCode(404).contentType(ContentType.JSON); + } + + /** + * Test that tries to obtain an existing trace. + */ + @Test + public void testGetExisting() { + when().get("/bitbrains-small") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("name", equalTo("Bitbrains Small")); + } +} diff --git a/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/runner/JobResourceTest.java b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/runner/JobResourceTest.java new file mode 100644 index 00000000..94b2cef0 --- /dev/null +++ b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/runner/JobResourceTest.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2023 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.server.rest.runner; + +import static io.restassured.RestAssured.given; +import static io.restassured.RestAssured.when; +import static org.hamcrest.Matchers.equalTo; + +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.Test; +import org.opendc.web.proto.JobState; + +/** + * Test suite for {@link JobResource}. + */ +@QuarkusTest +@TestHTTPEndpoint(JobResource.class) +public final class JobResourceTest { + /** + * Test that tries to query the pending jobs without token. + */ + @Test + public void testQueryWithoutToken() { + when().get().then().statusCode(401); + } + + /** + * Test that tries to query the pending jobs for a user. + */ + @Test + @TestSecurity( + user = "test", + roles = {"openid"}) + public void testQueryInvalidScope() { + when().get().then().statusCode(403); + } + + /** + * Test that tries to query the pending jobs for a runner. + */ + @Test + @TestSecurity( + user = "test", + roles = {"runner"}) + public void testQuery() { + when().get().then().statusCode(200).contentType(ContentType.JSON).body("get(0).state", equalTo("PENDING")); + } + + /** + * Test that tries to obtain a non-existent job. + */ + @Test + @TestSecurity( + user = "test", + roles = {"runner"}) + public void testGetNonExisting() { + when().get("/0").then().statusCode(404).contentType(ContentType.JSON); + } + + /** + * Test that tries to obtain a job. + */ + @Test + @TestSecurity( + user = "test", + roles = {"runner"}) + public void testGetExisting() { + when().get("/1").then().statusCode(200).contentType(ContentType.JSON).body("id", equalTo(1)); + } + + /** + * Test that tries to update a non-existent job. + */ + @Test + @TestSecurity( + user = "test", + roles = {"runner"}) + public void testUpdateNonExistent() { + given().body(new org.opendc.web.proto.runner.Job.Update(JobState.PENDING, 0, null)) + .contentType(ContentType.JSON) + .when() + .post("/0") + .then() + .statusCode(404) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to update a job. + */ + @Test + @TestSecurity( + user = "test", + roles = {"runner"}) + public void testUpdateState() { + given().body(new org.opendc.web.proto.runner.Job.Update(JobState.CLAIMED, 0, null)) + .contentType(ContentType.JSON) + .when() + .post("/2") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("state", equalTo(JobState.CLAIMED.toString())); + } + + /** + * Test that tries to update a job with invalid input. + */ + @Test + @TestSecurity( + user = "test", + roles = {"runner"}) + public void testUpdateInvalidInput() { + given().body("{ \"test\": \"test\" }") + .contentType(ContentType.JSON) + .when() + .post("/1") + .then() + .statusCode(400) + .contentType(ContentType.JSON); + } +} diff --git a/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/PortfolioResourceTest.java b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/PortfolioResourceTest.java new file mode 100644 index 00000000..a952d83f --- /dev/null +++ b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/PortfolioResourceTest.java @@ -0,0 +1,287 @@ +/* + * Copyright (c) 2023 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.server.rest.user; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; + +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import java.util.Set; +import org.junit.jupiter.api.Test; +import org.opendc.web.proto.Targets; + +/** + * Test suite for {@link PortfolioResource}. + */ +@QuarkusTest +@TestHTTPEndpoint(PortfolioResource.class) +public final class PortfolioResourceTest { + /** + * Test that tries to obtain the list of portfolios belonging to a project. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testGetForProject() { + given().pathParam("project", 1).when().get().then().statusCode(200).contentType(ContentType.JSON); + } + + /** + * Test that tries to obtain the list of portfolios belonging to a project without authorization. + */ + @Test + @TestSecurity( + user = "unknown", + roles = {"openid"}) + public void testGetForProjectNoAuthorization() { + given().pathParam("project", 1).when().get().then().statusCode(200).contentType(ContentType.JSON); + } + + /** + * Test that tries to create a topology for a project. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testCreateNonExistent() { + given().pathParam("project", "0") + .body(new org.opendc.web.proto.user.Portfolio.Create("test", new Targets(Set.of(), 1))) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(404) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to create a topology for a project. + */ + @Test + @TestSecurity( + user = "viewer", + roles = {"openid"}) + public void testCreateNotPermitted() { + given().pathParam("project", "1") + .body(new org.opendc.web.proto.user.Portfolio.Create("test", new Targets(Set.of(), 1))) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(403) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to create a portfolio for a project. + */ + @Test + @TestSecurity( + user = "editor", + roles = {"openid"}) + public void testCreate() { + given().pathParam("project", "1") + .body(new org.opendc.web.proto.user.Portfolio.Create("test", new Targets(Set.of(), 1))) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("name", equalTo("test")); + } + + /** + * Test to create a portfolio with an empty body. + */ + @Test + @TestSecurity( + user = "editor", + roles = {"openid"}) + public void testCreateEmpty() { + given().pathParam("project", "1") + .body("{}") + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(400) + .contentType(ContentType.JSON); + } + + /** + * Test to create a portfolio with a blank name. + */ + @Test + @TestSecurity( + user = "editor", + roles = {"openid"}) + public void testCreateBlankName() { + given().pathParam("project", "1") + .body(new org.opendc.web.proto.user.Portfolio.Create("", new Targets(Set.of(), 1))) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(400) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to obtain a portfolio without token. + */ + @Test + public void testGetWithoutToken() { + given().pathParam("project", "1").when().get("/1").then().statusCode(401); + } + + /** + * Test that tries to obtain a portfolio with an invalid scope. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"runner"}) + public void testGetInvalidToken() { + given().pathParam("project", "1").when().get("/1").then().statusCode(403); + } + + /** + * Test that tries to obtain a non-existent portfolio. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testGetNonExisting() { + given().pathParam("project", "1") + .when() + .get("/0") + .then() + .statusCode(404) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to obtain a portfolio for a non-existent project. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testGetNonExistingProject() { + given().pathParam("project", "0") + .when() + .get("/1") + .then() + .statusCode(404) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to obtain a portfolio. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testGetExisting() { + given().pathParam("project", "1") + .when() + .get("/1") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("id", equalTo(1)); + } + + /** + * Test to delete a non-existent portfolio. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testDeleteNonExistent() { + given().pathParam("project", "1").when().delete("/0").then().statusCode(404); + } + + /** + * Test to delete a portfolio on a non-existent project. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testDeleteNonExistentProject() { + given().pathParam("project", "0").when().delete("/1").then().statusCode(404); + } + + /** + * Test to delete a portfolio. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testDelete() { + int number = given().pathParam("project", "1") + .body(new org.opendc.web.proto.user.Portfolio.Create("Delete Portfolio", new Targets(Set.of(), 1))) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .extract() + .path("number"); + + given().pathParam("project", "1") + .when() + .delete("/" + number) + .then() + .statusCode(200) + .contentType(ContentType.JSON); + } + + /** + * Test to delete a portfolio as a viewer. + */ + @Test + @TestSecurity( + user = "viewer", + roles = {"openid"}) + public void testDeleteAsViewer() { + given().pathParam("project", "1") + .when() + .delete("/1") + .then() + .statusCode(403) + .contentType(ContentType.JSON); + } +} diff --git a/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/PortfolioScenarioResourceTest.java b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/PortfolioScenarioResourceTest.java new file mode 100644 index 00000000..4f8d412c --- /dev/null +++ b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/PortfolioScenarioResourceTest.java @@ -0,0 +1,273 @@ +/* + * Copyright (c) 2023 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.server.rest.user; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; + +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.Test; +import org.opendc.web.proto.OperationalPhenomena; +import org.opendc.web.proto.Workload; +import org.opendc.web.proto.user.Scenario; + +/** + * Test suite for {@link PortfolioScenarioResource}. + */ +@QuarkusTest +@TestHTTPEndpoint(PortfolioScenarioResource.class) +public final class PortfolioScenarioResourceTest { + /** + * Test that tries to obtain a portfolio without token. + */ + @Test + public void testGetWithoutToken() { + given().pathParam("project", "1") + .pathParam("portfolio", "1") + .when() + .get() + .then() + .statusCode(401); + } + + /** + * Test that tries to obtain a portfolio with an invalid scope. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"runner"}) + public void testGetInvalidToken() { + given().pathParam("project", "1") + .pathParam("portfolio", "1") + .when() + .get() + .then() + .statusCode(403); + } + + /** + * Test that tries to obtain a scenario without authorization. + */ + @Test + @TestSecurity( + user = "unknown", + roles = {"openid"}) + public void testGetUnauthorized() { + given().pathParam("project", "1") + .pathParam("portfolio", "1") + .when() + .get() + .then() + .statusCode(200) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to obtain a scenario. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testGet() { + given().pathParam("project", "1") + .pathParam("portfolio", "1") + .when() + .get() + .then() + .statusCode(200) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to create a scenario for a portfolio. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testCreateNonExistent() { + given().pathParam("project", "1") + .pathParam("portfolio", "0") + .body(new Scenario.Create( + "test", new Workload.Spec("test", 1.0), 1, new OperationalPhenomena(false, false), "test")) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(404) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to create a scenario for a portfolio without authorization. + */ + @Test + @TestSecurity( + user = "unknown", + roles = {"openid"}) + public void testCreateUnauthorized() { + given().pathParam("project", "1") + .pathParam("portfolio", "0") + .body(new Scenario.Create( + "test", new Workload.Spec("test", 1.0), 1, new OperationalPhenomena(false, false), "test")) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(404) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to create a scenario for a portfolio as a viewer. + */ + @Test + @TestSecurity( + user = "viewer", + roles = {"openid"}) + public void testCreateAsViewer() { + given().pathParam("project", "1") + .pathParam("portfolio", "0") + .body(new Scenario.Create( + "test", new Workload.Spec("test", 1.0), 1, new OperationalPhenomena(false, false), "test")) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(403) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to create a scenario for a portfolio. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testCreate() { + given().pathParam("project", "1") + .pathParam("portfolio", "1") + .body(new Scenario.Create( + "test", + new Workload.Spec("bitbrains-small", 1.0), + 1, + new OperationalPhenomena(false, false), + "test")) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("name", equalTo("test")); + } + + /** + * Test to create a project with an empty body. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testCreateEmpty() { + given().pathParam("project", "1") + .pathParam("portfolio", "1") + .body("{}") + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(400) + .contentType(ContentType.JSON); + } + + /** + * Test to create a project with a blank name. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testCreateBlankName() { + given().pathParam("project", "1") + .pathParam("portfolio", "1") + .body(new Scenario.Create( + "", new Workload.Spec("test", 1.0), 1, new OperationalPhenomena(false, false), "test")) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(400) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to create a scenario for a portfolio. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testCreateUnknownTopology() { + given().pathParam("project", "1") + .pathParam("portfolio", "1") + .body(new Scenario.Create( + "test", + new Workload.Spec("bitbrains-small", 1.0), + -1, + new OperationalPhenomena(false, false), + "test")) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(400) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to create a scenario for a portfolio. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testCreateUnknownTrace() { + given().pathParam("project", "1") + .pathParam("portfolio", "1") + .body(new Scenario.Create( + "test", new Workload.Spec("unknown", 1.0), 1, new OperationalPhenomena(false, false), "test")) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(400) + .contentType(ContentType.JSON); + } +} diff --git a/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/ProjectResourceTest.java b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/ProjectResourceTest.java new file mode 100644 index 00000000..8bd60808 --- /dev/null +++ b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/ProjectResourceTest.java @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2023 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.server.rest.user; + +import static io.restassured.RestAssured.given; +import static io.restassured.RestAssured.when; +import static org.hamcrest.Matchers.equalTo; + +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.Test; + +/** + * Test suite for [ProjectResource]. + */ +@QuarkusTest +@TestHTTPEndpoint(ProjectResource.class) +public final class ProjectResourceTest { + /** + * Test that tries to obtain all projects without token. + */ + @Test + public void testGetAllWithoutToken() { + when().get().then().statusCode(401); + } + + /** + * Test that tries to obtain all projects with an invalid scope. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"runner"}) + public void testGetAllWithInvalidScope() { + when().get().then().statusCode(403); + } + + /** + * Test that tries to obtain all project for a user. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testGetAll() { + when().get().then().statusCode(200).contentType(ContentType.JSON).body("get(0).name", equalTo("Test Project")); + } + + /** + * Test that tries to obtain a non-existent project. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testGetNonExisting() { + when().get("/0").then().statusCode(404).contentType(ContentType.JSON); + } + + /** + * Test that tries to obtain a project. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testGetExisting() { + when().get("/1").then().statusCode(200).contentType(ContentType.JSON).body("id", equalTo(1)); + } + + /** + * Test that tries to create a project. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testCreate() { + given().body(new org.opendc.web.proto.user.Project.Create("test")) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("name", equalTo("test")); + } + + /** + * Test to create a project with an empty body. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testCreateEmpty() { + given().body("{}") + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(400) + .contentType(ContentType.JSON); + } + + /** + * Test to create a project with a blank name. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testCreateBlankName() { + given().body(new org.opendc.web.proto.user.Project.Create("")) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(400) + .contentType(ContentType.JSON); + } + + /** + * Test to delete a non-existent project. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testDeleteNonExistent() { + when().delete("/0").then().statusCode(404).contentType(ContentType.JSON); + } + + /** + * Test to delete a project. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testDelete() { + int id = given().body(new org.opendc.web.proto.user.Project.Create("Delete Project")) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .extract() + .path("id"); + + when().delete("/" + id).then().statusCode(200).contentType(ContentType.JSON); + } + + /** + * Test to delete a project which the user does not own. + */ + @Test + @TestSecurity( + user = "viewer", + roles = {"openid"}) + public void testDeleteNonOwner() { + when().delete("/1").then().statusCode(403).contentType(ContentType.JSON); + } +} diff --git a/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/ScenarioResourceTest.java b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/ScenarioResourceTest.java new file mode 100644 index 00000000..a980e4e2 --- /dev/null +++ b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/ScenarioResourceTest.java @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2023 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.server.rest.user; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; + +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.http.ContentType; +import io.restassured.specification.RequestSpecification; +import org.junit.jupiter.api.Test; +import org.opendc.web.proto.OperationalPhenomena; +import org.opendc.web.proto.Workload; +import org.opendc.web.proto.user.Scenario; + +/** + * Test suite for {@link ScenarioResource}. + */ +@QuarkusTest +@TestHTTPEndpoint(ScenarioResource.class) +public final class ScenarioResourceTest { + /** + * Test that tries to obtain all scenarios belonging to a project without authorization. + */ + @Test + @TestSecurity( + user = "unknown", + roles = {"openid"}) + public void testGetAllUnauthorized() { + given().pathParam("project", "1").when().get().then().statusCode(404).contentType(ContentType.JSON); + } + + /** + * Test that tries to obtain all scenarios belonging to a project. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testGetAll() { + given().pathParam("project", "1").when().get().then().statusCode(200).contentType(ContentType.JSON); + } + + /** + * Test that tries to obtain a scenario without token. + */ + @Test + public void testGetWithoutToken() { + given().pathParam("project", "1").when().get("/1").then().statusCode(401); + } + + /** + * Test that tries to obtain a scenario with an invalid scope. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"runner"}) + public void testGetInvalidToken() { + given().pathParam("project", "1").when().get("/1").then().statusCode(403); + } + + /** + * Test that tries to obtain a non-existent scenario. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testGetNonExisting() { + given().pathParam("project", "1") + .when() + .get("/0") + .then() + .statusCode(404) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to obtain a scenario. + */ + @Test + @TestSecurity( + user = "unknown", + roles = {"openid"}) + public void testGetExistingUnauthorized() { + given().pathParam("project", "1") + .when() + .get("/1") + .then() + .statusCode(404) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to obtain a scenario. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testGetExisting() { + given().pathParam("project", "1") + .when() + .get("/1") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("id", equalTo(1)); + } + + /** + * Test to delete a non-existent scenario. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testDeleteNonExistent() { + given().pathParam("project", "1").when().delete("/0").then().statusCode(404); + } + + /** + * Test to delete a scenario without authorization. + */ + @Test + @TestSecurity( + user = "unknown", + roles = {"openid"}) + public void testDeleteUnauthorized() { + given().pathParam("project", "1").when().delete("/1").then().statusCode(404); + } + + /** + * Test to delete a scenario as a viewer. + */ + @Test + @TestSecurity( + user = "viewer", + roles = {"openid"}) + public void testDeleteAsViewer() { + given().pathParam("project", "1").when().delete("/1").then().statusCode(403); + } + + /** + * Test to delete a scenario. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testDelete() { + RequestSpecification spec = new RequestSpecBuilder() + .setBasePath("/projects/1/portfolios/1/scenarios") + .build(); + + int number = given(spec) + .body(new Scenario.Create( + "test", + new Workload.Spec("bitbrains-small", 1.0), + 1, + new OperationalPhenomena(false, false), + "test")) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .extract() + .path("number"); + + given().pathParam("project", "1") + .when() + .delete("/" + number) + .then() + .statusCode(200) + .contentType(ContentType.JSON); + } +} diff --git a/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/TopologyResourceTest.java b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/TopologyResourceTest.java new file mode 100644 index 00000000..21e35b09 --- /dev/null +++ b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/TopologyResourceTest.java @@ -0,0 +1,358 @@ +/* + * Copyright (c) 2023 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.server.rest.user; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; + +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.opendc.web.proto.user.Topology; + +/** + * Test suite for {@link TopologyResource}. + */ +@QuarkusTest +@TestHTTPEndpoint(TopologyResource.class) +public final class TopologyResourceTest { + /** + * Test that tries to obtain the list of topologies of a project without proper authorization. + */ + @Test + @TestSecurity( + user = "unknown", + roles = {"openid"}) + public void testGetAllWithoutAuth() { + given().pathParam("project", "1") + .when() + .get() + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body(equalTo("[]")); + } + + /** + * Test that tries to obtain the list of topologies belonging to a project. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testGetAll() { + given().pathParam("project", "1").when().get().then().statusCode(200).contentType(ContentType.JSON); + } + + /** + * Test that tries to create a topology for a project. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testCreateNonExistent() { + given().pathParam("project", "0") + .body(new Topology.Create("test", List.of())) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(404) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to create a topology for a project as viewer. + */ + @Test + @TestSecurity( + user = "viewer", + roles = {"openid"}) + public void testCreateUnauthorized() { + given().pathParam("project", "1") + .body(new Topology.Create("test", List.of())) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(403) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to create a topology for a project. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testCreate() { + given().pathParam("project", "1") + .body(new Topology.Create("test", List.of())) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("name", equalTo("test")); + } + + /** + * Test to create a topology with an empty body. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testCreateEmpty() { + given().pathParam("project", "1") + .body("{}") + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(400) + .contentType(ContentType.JSON); + } + + /** + * Test to create a topology with a blank name. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testCreateBlankName() { + given().pathParam("project", "1") + .body(new Topology.Create("", List.of())) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(400) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to obtain a topology without token. + */ + @Test + public void testGetWithoutToken() { + given().pathParam("project", "1").when().get("/1").then().statusCode(401); + } + + /** + * Test that tries to obtain a topology with an invalid scope. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"runner"}) + public void testGetInvalidToken() { + given().pathParam("project", "1").when().get("/1").then().statusCode(403); + } + + /** + * Test that tries to obtain a non-existent topology. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testGetNonExisting() { + given().pathParam("project", "1") + .when() + .get("/0") + .then() + .statusCode(404) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to obtain a topology without authorization. + */ + @Test + @TestSecurity( + user = "unknown", + roles = {"openid"}) + public void testGetUnauthorized() { + given().pathParam("project", "1") + .when() + .get("/1") + .then() + .statusCode(404) + .contentType(ContentType.JSON); + } + + /** + * Test that tries to obtain a topology. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testGetExisting() { + given().pathParam("project", "1") + .when() + .get("/1") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("id", equalTo(1)); + } + + /** + * Test to delete a non-existent topology. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testUpdateNonExistent() { + given().pathParam("project", "1") + .body(new Topology.Update(List.of())) + .contentType(ContentType.JSON) + .when() + .put("/0") + .then() + .statusCode(404); + } + + /** + * Test to delete a topology without authorization. + */ + @Test + @TestSecurity( + user = "unknown", + roles = {"openid"}) + public void testUpdateUnauthorized() { + given().pathParam("project", "1") + .body(new Topology.Update(List.of())) + .contentType(ContentType.JSON) + .when() + .put("/1") + .then() + .statusCode(404); + } + + /** + * Test to update a topology as a viewer. + */ + @Test + @TestSecurity( + user = "viewer", + roles = {"openid"}) + public void testUpdateAsViewer() { + given().pathParam("project", "1") + .body(new Topology.Update(List.of())) + .contentType(ContentType.JSON) + .when() + .put("/1") + .then() + .statusCode(403) + .contentType(ContentType.JSON); + } + + /** + * Test to update a topology. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testUpdate() { + given().pathParam("project", "1") + .body(new Topology.Update(List.of())) + .contentType(ContentType.JSON) + .when() + .put("/1") + .then() + .statusCode(200) + .contentType(ContentType.JSON); + } + + /** + * Test to delete a non-existent topology. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testDeleteNonExistent() { + given().pathParam("project", "1").when().delete("/0").then().statusCode(404); + } + + /** + * Test to delete a topology without authorization. + */ + @Test + @TestSecurity( + user = "unknown", + roles = {"openid"}) + public void testDeleteUnauthorized() { + given().pathParam("project", "1").when().delete("/1").then().statusCode(404); + } + + /** + * Test to delete a topology as a viewer. + */ + @Test + @TestSecurity( + user = "viewer", + roles = {"openid"}) + public void testDeleteAsViewer() { + given().pathParam("project", "1").when().delete("/1").then().statusCode(403); + } + + /** + * Test to delete a topology. + */ + @Test + @TestSecurity( + user = "owner", + roles = {"openid"}) + public void testDelete() { + int number = given().pathParam("project", "1") + .body(new Topology.Create("Delete Topology", List.of())) + .contentType(ContentType.JSON) + .when() + .post() + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .extract() + .path("number"); + + given().pathParam("project", "1") + .when() + .delete("/" + number) + .then() + .statusCode(200) + .contentType(ContentType.JSON); + } +} diff --git a/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/user/UserResourceTest.kt b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/UserResourceTest.java index 36af20f4..6dcb3b4d 100644 --- a/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/user/UserResourceTest.kt +++ b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/UserResourceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 AtLarge Research + * Copyright (c) 2023 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 @@ -20,50 +20,46 @@ * SOFTWARE. */ -package org.opendc.web.server.rest.user +package org.opendc.web.server.rest.user; -import io.quarkus.test.common.http.TestHTTPEndpoint -import io.quarkus.test.junit.QuarkusTest -import io.quarkus.test.security.TestSecurity -import io.restassured.http.ContentType -import io.restassured.module.kotlin.extensions.Then -import io.restassured.module.kotlin.extensions.When -import org.hamcrest.Matchers -import org.junit.jupiter.api.Test +import static io.restassured.RestAssured.when; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; + +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.Test; /** * Test suite for [UserResource]. */ @QuarkusTest -@TestHTTPEndpoint(UserResource::class) -class UserResourceTest { +@TestHTTPEndpoint(UserResource.class) +public final class UserResourceTest { /** * Test that tries to obtain the profile of the active user. */ @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testMe() { - When { - get("me") - } Then { - statusCode(200) - contentType(ContentType.JSON) - - body("userId", Matchers.equalTo("testUser")) - body("accounting.simulationTime", Matchers.equalTo(0)) - body("accounting.simulationTimeBudget", Matchers.greaterThan(0)) - } + @TestSecurity( + user = "testUser", + roles = {"openid"}) + public void testMe() { + when().get("me") + .then() + .statusCode(200) + .contentType(ContentType.JSON) + .body("userId", equalTo("testUser")) + .body("accounting.simulationTime", equalTo(0)) + .body("accounting.simulationTimeBudget", greaterThan(0)); } /** * Test that tries to obtain the profile of the active user without authorization. */ @Test - fun testMeUnauthorized() { - When { - get("me") - } Then { - statusCode(401) - } + public void testMeUnauthorized() { + when().get("me").then().statusCode(401); } } diff --git a/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/service/JobServiceTest.java b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/service/JobServiceTest.java new file mode 100644 index 00000000..f6d871c0 --- /dev/null +++ b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/service/JobServiceTest.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2023 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.server.service; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; + +import io.quarkus.test.junit.QuarkusTest; +import java.time.Instant; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.opendc.web.proto.JobState; +import org.opendc.web.server.model.Job; + +/** + * Test suite for the {@link JobService}. + */ +@QuarkusTest +public class JobServiceTest { + /** + * The {@link JobService} instance under test. + */ + private JobService service; + + /** + * The mock {@link UserAccountingService}. + */ + private UserAccountingService mockAccountingService; + + @BeforeEach + public void setUp() { + mockAccountingService = Mockito.mock(UserAccountingService.class); + service = new JobService(mockAccountingService); + } + + @Test + public void testUpdateInvalidTransition() { + Job job = new Job(null, "test", Instant.now(), 1); + job.state = JobState.RUNNING; + + assertThrows(IllegalArgumentException.class, () -> service.updateJob(job, JobState.CLAIMED, 0, null)); + + Mockito.verifyNoInteractions(mockAccountingService); + } + + @Test + public void testUpdateNoBudget() { + Job job = Mockito.spy(new Job(null, "test", Instant.now(), 1)); + job.state = JobState.RUNNING; + + Mockito.when(mockAccountingService.consumeSimulationBudget(any(), anyInt())) + .thenReturn(true); + Mockito.doReturn(true).when(job).updateAtomically(any(), any(), anyInt(), any()); + + service.updateJob(job, JobState.RUNNING, 0, null); + + Mockito.verify(job).updateAtomically(eq(JobState.FAILED), any(), anyInt(), any()); + } + + @Test + public void testUpdateNoBudgetWhenFinishing() { + Job job = Mockito.spy(new Job(null, "test", Instant.now(), 1)); + job.state = JobState.RUNNING; + + Mockito.when(mockAccountingService.consumeSimulationBudget(any(), anyInt())) + .thenReturn(true); + Mockito.doReturn(true).when(job).updateAtomically(any(), any(), anyInt(), any()); + + service.updateJob(job, JobState.FINISHED, 0, null); + + Mockito.verify(job).updateAtomically(eq(JobState.FINISHED), any(), anyInt(), any()); + } + + @Test + public void testUpdateSuccess() { + Job job = Mockito.spy(new Job(null, "test", Instant.now(), 1)); + job.state = JobState.RUNNING; + + Mockito.when(mockAccountingService.consumeSimulationBudget(any(), anyInt())) + .thenReturn(false); + Mockito.doReturn(true).when(job).updateAtomically(any(), any(), anyInt(), any()); + + service.updateJob(job, JobState.FINISHED, 0, null); + + Mockito.verify(job).updateAtomically(eq(JobState.FINISHED), any(), anyInt(), any()); + } + + @Test + public void testUpdateConflict() { + Job job = Mockito.spy(new Job(null, "test", Instant.now(), 1)); + job.state = JobState.RUNNING; + + Mockito.when(mockAccountingService.consumeSimulationBudget(any(), anyInt())) + .thenReturn(false); + Mockito.doReturn(false).when(job).updateAtomically(any(), any(), anyInt(), any()); + + assertThrows(IllegalStateException.class, () -> service.updateJob(job, JobState.FINISHED, 0, null)); + + Mockito.verify(job).updateAtomically(eq(JobState.FINISHED), any(), anyInt(), any()); + } +} diff --git a/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/service/UserAccountingServiceTest.java b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/service/UserAccountingServiceTest.java new file mode 100644 index 00000000..d1d82097 --- /dev/null +++ b/opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/service/UserAccountingServiceTest.java @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2023 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.server.service; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; + +import io.quarkus.panache.mock.PanacheMock; +import io.quarkus.test.junit.QuarkusTest; +import java.time.Duration; +import java.time.LocalDate; +import javax.persistence.EntityExistsException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.opendc.web.server.model.UserAccounting; + +/** + * Test suite for the {@link UserAccountingService}. + */ +@QuarkusTest +public class UserAccountingServiceTest { + /** + * The {@link UserAccountingService} instance under test. + */ + private UserAccountingService service; + + /** + * The user id to test with + */ + private final String userId = "test"; + + @BeforeEach + public void setUp() { + PanacheMock.mock(UserAccounting.class); + service = new UserAccountingService(Duration.ofHours(1)); + } + + @Test + public void testGetUserDoesNotExist() { + Mockito.when(UserAccounting.findByUser(userId)).thenReturn(null); + + var accounting = service.getAccounting(userId); + + assertTrue(accounting.getPeriodEnd().isAfter(LocalDate.now())); + assertEquals(0, accounting.getSimulationTime()); + } + + @Test + public void testGetUserDoesExist() { + var now = LocalDate.now(); + var periodEnd = now.plusMonths(1); + + var mockAccounting = new UserAccounting(userId, periodEnd, 3600); + mockAccounting.simulationTime = 32; + + Mockito.when(UserAccounting.findByUser(userId)).thenReturn(mockAccounting); + + var accounting = service.getAccounting(userId); + + assertAll( + () -> assertEquals(periodEnd, accounting.getPeriodEnd()), + () -> assertEquals(32, accounting.getSimulationTime()), + () -> assertEquals(3600, accounting.getSimulationTimeBudget())); + } + + @Test + public void testHasBudgetUserDoesNotExist() { + Mockito.when(UserAccounting.findByUser(userId)).thenReturn(null); + + assertTrue(service.hasSimulationBudget(userId)); + } + + @Test + public void testHasBudget() { + var periodEnd = LocalDate.now().plusMonths(2); + + var mockAccounting = new UserAccounting(userId, periodEnd, 3600); + Mockito.when(UserAccounting.findByUser(userId)).thenReturn(mockAccounting); + + assertTrue(service.hasSimulationBudget(userId)); + } + + @Test + public void testHasBudgetExceededButPeriodExpired() { + var periodEnd = LocalDate.now().minusMonths(2); + + var mockAccounting = new UserAccounting(userId, periodEnd, 3600); + mockAccounting.simulationTime = 3900; + Mockito.when(UserAccounting.findByUser(userId)).thenReturn(mockAccounting); + + assertTrue(service.hasSimulationBudget(userId)); + } + + @Test + public void testHasBudgetPeriodExpired() { + var periodEnd = LocalDate.now().minusMonths(2); + + var mockAccounting = new UserAccounting(userId, periodEnd, 3600); + Mockito.when(UserAccounting.findByUser(userId)).thenReturn(mockAccounting); + + assertTrue(service.hasSimulationBudget(userId)); + } + + @Test + public void testHasBudgetExceeded() { + var periodEnd = LocalDate.now().plusMonths(1); + + var mockAccounting = new UserAccounting(userId, periodEnd, 3600); + mockAccounting.simulationTime = 3900; + Mockito.when(UserAccounting.findByUser(userId)).thenReturn(mockAccounting); + + assertFalse(service.hasSimulationBudget(userId)); + } + + @Test + public void testConsumeBudgetNewUser() { + Mockito.when(UserAccounting.findByUser(userId)).thenReturn(null); + Mockito.when(UserAccounting.create(anyString(), any(), anyInt(), anyInt())) + .thenAnswer((i) -> { + var accounting = new UserAccounting(i.getArgument(0), i.getArgument(1), i.getArgument(2)); + accounting.simulationTime = i.getArgument(3); + return accounting; + }); + + assertFalse(service.consumeSimulationBudget(userId, 10)); + } + + @Test + public void testConsumeBudgetNewUserExceeded() { + Mockito.when(UserAccounting.findByUser(userId)).thenReturn(null); + Mockito.when(UserAccounting.create(anyString(), any(), anyInt(), anyInt())) + .thenAnswer((i) -> { + var accounting = new UserAccounting(i.getArgument(0), i.getArgument(1), i.getArgument(2)); + accounting.simulationTime = i.getArgument(3); + return accounting; + }); + + assertTrue(service.consumeSimulationBudget(userId, 4000)); + } + + @Test + public void testConsumeBudgetNewUserConflict() { + var periodEnd = LocalDate.now().plusMonths(1); + var accountingMock = Mockito.spy(new UserAccounting(userId, periodEnd, 3600)); + + Mockito.when(UserAccounting.findByUser(userId)).thenReturn(null).thenReturn(accountingMock); + Mockito.when(UserAccounting.create(anyString(), any(), anyInt(), anyInt())) + .thenThrow(new EntityExistsException()); + Mockito.when(accountingMock.consumeBudget(anyInt())).thenAnswer((i) -> { + accountingMock.simulationTime += i.<Integer>getArgument(0); + return true; + }); + + assertFalse(service.consumeSimulationBudget(userId, 10)); + } + + @Test + public void testConsumeBudgetResetSuccess() { + var periodEnd = LocalDate.now().minusMonths(2); + var accountingMock = Mockito.spy(new UserAccounting(userId, periodEnd, 3600)); + accountingMock.simulationTime = 3900; + + Mockito.when(UserAccounting.findByUser(userId)).thenReturn(accountingMock); + Mockito.when(accountingMock.resetBudget(any(), anyInt())).thenAnswer((i) -> { + accountingMock.periodEnd = i.getArgument(0); + accountingMock.simulationTime += i.<Integer>getArgument(1); + return true; + }); + + assertTrue(service.consumeSimulationBudget(userId, 4000)); + } + + @Test + public void testInfiniteConflict() { + var periodEnd = LocalDate.now().plusMonths(1); + var accountingMock = Mockito.spy(new UserAccounting(userId, periodEnd, 3600)); + + Mockito.when(UserAccounting.findByUser(userId)).thenReturn(accountingMock); + Mockito.when(accountingMock.consumeBudget(anyInt())).thenAnswer((i) -> { + accountingMock.simulationTime += i.<Integer>getArgument(0); + return false; + }); + + assertThrows(IllegalStateException.class, () -> service.consumeSimulationBudget(userId, 10)); + } +} diff --git a/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/TraceResourceTest.kt b/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/TraceResourceTest.kt deleted file mode 100644 index 2490cf46..00000000 --- a/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/TraceResourceTest.kt +++ /dev/null @@ -1,100 +0,0 @@ -/* - * 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.server.rest - -import io.mockk.every -import io.quarkiverse.test.junit.mockk.InjectMock -import io.quarkus.test.common.http.TestHTTPEndpoint -import io.quarkus.test.junit.QuarkusMock -import io.quarkus.test.junit.QuarkusTest -import io.restassured.http.ContentType -import io.restassured.module.kotlin.extensions.Then -import io.restassured.module.kotlin.extensions.When -import org.hamcrest.Matchers -import org.hamcrest.Matchers.equalTo -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.opendc.web.proto.Trace -import org.opendc.web.server.service.TraceService - -/** - * Test suite for [TraceResource]. - */ -@QuarkusTest -@TestHTTPEndpoint(TraceResource::class) -class TraceResourceTest { - @InjectMock - private lateinit var traceService: TraceService - - @BeforeEach - fun setUp() { - QuarkusMock.installMockForType(traceService, TraceService::class.java) - } - - /** - * Test that tries to obtain all traces (empty response). - */ - @Test - fun testGetAllEmpy() { - every { traceService.findAll() } returns emptyList() - - When { - get() - } Then { - statusCode(200) - contentType(ContentType.JSON) - body("", Matchers.empty<String>()) - } - } - - /** - * Test that tries to obtain a non-existent trace. - */ - @Test - fun testGetNonExisting() { - every { traceService.findById("bitbrains") } returns null - - When { - get("/bitbrains") - } Then { - statusCode(404) - contentType(ContentType.JSON) - } - } - - /** - * Test that tries to obtain an existing trace. - */ - @Test - fun testGetExisting() { - every { traceService.findById("bitbrains") } returns Trace("bitbrains", "Bitbrains", "VM") - - When { - get("/bitbrains") - } Then { - statusCode(200) - contentType(ContentType.JSON) - body("name", equalTo("Bitbrains")) - } - } -} diff --git a/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/runner/JobResourceTest.kt b/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/runner/JobResourceTest.kt deleted file mode 100644 index 4a86c928..00000000 --- a/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/runner/JobResourceTest.kt +++ /dev/null @@ -1,203 +0,0 @@ -/* - * 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.server.rest.runner - -import io.mockk.every -import io.quarkiverse.test.junit.mockk.InjectMock -import io.quarkus.test.common.http.TestHTTPEndpoint -import io.quarkus.test.junit.QuarkusMock -import io.quarkus.test.junit.QuarkusTest -import io.quarkus.test.security.TestSecurity -import io.restassured.http.ContentType -import io.restassured.module.kotlin.extensions.Given -import io.restassured.module.kotlin.extensions.Then -import io.restassured.module.kotlin.extensions.When -import org.hamcrest.Matchers.equalTo -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.opendc.web.proto.JobState -import org.opendc.web.proto.OperationalPhenomena -import org.opendc.web.proto.Targets -import org.opendc.web.proto.Trace -import org.opendc.web.proto.Workload -import org.opendc.web.proto.runner.Job -import org.opendc.web.proto.runner.Portfolio -import org.opendc.web.proto.runner.Scenario -import org.opendc.web.proto.runner.Topology -import org.opendc.web.server.service.JobService -import java.time.Instant - -/** - * Test suite for [JobResource]. - */ -@QuarkusTest -@TestHTTPEndpoint(JobResource::class) -class JobResourceTest { - @InjectMock - private lateinit var jobService: JobService - - /** - * Dummy values - */ - private val dummyPortfolio = Portfolio(1, 1, "test", Targets(emptySet())) - private val dummyTopology = Topology(1, 1, "test", emptyList(), Instant.now(), Instant.now()) - private val dummyTrace = Trace("bitbrains", "Bitbrains", "vm") - private val dummyScenario = Scenario(1, 1, dummyPortfolio, "test", Workload(dummyTrace, 1.0), dummyTopology, OperationalPhenomena(false, false), "test") - private val dummyJob = Job(1, dummyScenario, JobState.PENDING, Instant.now(), Instant.now(), 0) - - @BeforeEach - fun setUp() { - QuarkusMock.installMockForType(jobService, JobService::class.java) - } - - /** - * Test that tries to query the pending jobs without token. - */ - @Test - fun testQueryWithoutToken() { - When { - get() - } Then { - statusCode(401) - } - } - - /** - * Test that tries to query the pending jobs for a user. - */ - @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testQueryInvalidScope() { - When { - get() - } Then { - statusCode(403) - } - } - - /** - * Test that tries to query the pending jobs for a runner. - */ - @Test - @TestSecurity(user = "testUser", roles = ["runner"]) - fun testQuery() { - every { jobService.queryPending() } returns listOf(dummyJob) - - When { - get() - } Then { - statusCode(200) - contentType(ContentType.JSON) - body("get(0).id", equalTo(1)) - } - } - - /** - * Test that tries to obtain a non-existent job. - */ - @Test - @TestSecurity(user = "testUser", roles = ["runner"]) - fun testGetNonExisting() { - every { jobService.findById(1) } returns null - - When { - get("/1") - } Then { - statusCode(404) - contentType(ContentType.JSON) - } - } - - /** - * Test that tries to obtain a job. - */ - @Test - @TestSecurity(user = "testUser", roles = ["runner"]) - fun testGetExisting() { - every { jobService.findById(1) } returns dummyJob - - When { - get("/1") - } Then { - statusCode(200) - contentType(ContentType.JSON) - body("id", equalTo(1)) - } - } - - /** - * Test that tries to update a non-existent job. - */ - @Test - @TestSecurity(user = "testUser", roles = ["runner"]) - fun testUpdateNonExistent() { - every { jobService.updateState(1, any(), any(), any()) } returns null - - Given { - body(Job.Update(JobState.PENDING, 0)) - contentType(ContentType.JSON) - } When { - post("/1") - } Then { - statusCode(404) - contentType(ContentType.JSON) - } - } - - /** - * Test that tries to update a job. - */ - @Test - @TestSecurity(user = "testUser", roles = ["runner"]) - fun testUpdateState() { - every { jobService.updateState(1, any(), any(), any()) } returns dummyJob.copy(state = JobState.CLAIMED) - - Given { - body(Job.Update(JobState.CLAIMED, 0)) - contentType(ContentType.JSON) - } When { - post("/1") - } Then { - statusCode(200) - contentType(ContentType.JSON) - body("state", equalTo(JobState.CLAIMED.toString())) - } - } - - /** - * Test that tries to update a job with invalid input. - */ - @Test - @TestSecurity(user = "testUser", roles = ["runner"]) - fun testUpdateInvalidInput() { - Given { - body("""{ "test": "test" }""") - contentType(ContentType.JSON) - } When { - post("/1") - } Then { - statusCode(400) - contentType(ContentType.JSON) - } - } -} diff --git a/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/user/PortfolioResourceTest.kt b/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/user/PortfolioResourceTest.kt deleted file mode 100644 index 5798d2e7..00000000 --- a/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/user/PortfolioResourceTest.kt +++ /dev/null @@ -1,265 +0,0 @@ -/* - * 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.server.rest.user - -import io.mockk.every -import io.quarkiverse.test.junit.mockk.InjectMock -import io.quarkus.test.common.http.TestHTTPEndpoint -import io.quarkus.test.junit.QuarkusMock -import io.quarkus.test.junit.QuarkusTest -import io.quarkus.test.security.TestSecurity -import io.restassured.http.ContentType -import io.restassured.module.kotlin.extensions.Given -import io.restassured.module.kotlin.extensions.Then -import io.restassured.module.kotlin.extensions.When -import org.hamcrest.Matchers -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.opendc.web.proto.Targets -import org.opendc.web.proto.user.Portfolio -import org.opendc.web.proto.user.Project -import org.opendc.web.proto.user.ProjectRole -import org.opendc.web.server.service.PortfolioService -import java.time.Instant - -/** - * Test suite for [PortfolioResource]. - */ -@QuarkusTest -@TestHTTPEndpoint(PortfolioResource::class) -class PortfolioResourceTest { - @InjectMock - private lateinit var portfolioService: PortfolioService - - /** - * Dummy project and portfolio - */ - private val dummyProject = Project(1, "test", Instant.now(), Instant.now(), ProjectRole.OWNER) - private val dummyPortfolio = Portfolio(1, 1, dummyProject, "test", Targets(emptySet(), 1), emptyList()) - - @BeforeEach - fun setUp() { - QuarkusMock.installMockForType(portfolioService, PortfolioService::class.java) - } - - /** - * Test that tries to obtain the list of portfolios belonging to a project. - */ - @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testGetForProject() { - every { portfolioService.findAll("testUser", 1) } returns emptyList() - - Given { - pathParam("project", "1") - } When { - get() - } Then { - statusCode(200) - contentType(ContentType.JSON) - } - } - - /** - * Test that tries to create a topology for a project. - */ - @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testCreateNonExistent() { - every { portfolioService.create("testUser", 1, any()) } returns null - - Given { - pathParam("project", "1") - - body(Portfolio.Create("test", Targets(emptySet(), 1))) - contentType(ContentType.JSON) - } When { - post() - } Then { - statusCode(404) - contentType(ContentType.JSON) - } - } - - /** - * Test that tries to create a portfolio for a scenario. - */ - @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testCreate() { - every { portfolioService.create("testUser", 1, any()) } returns dummyPortfolio - - Given { - pathParam("project", "1") - - body(Portfolio.Create("test", Targets(emptySet(), 1))) - contentType(ContentType.JSON) - } When { - post() - } Then { - statusCode(200) - contentType(ContentType.JSON) - body("id", Matchers.equalTo(1)) - body("name", Matchers.equalTo("test")) - } - } - - /** - * Test to create a portfolio with an empty body. - */ - @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testCreateEmpty() { - Given { - pathParam("project", "1") - - body("{}") - contentType(ContentType.JSON) - } When { - post() - } Then { - statusCode(400) - contentType(ContentType.JSON) - } - } - - /** - * Test to create a portfolio with a blank name. - */ - @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testCreateBlankName() { - Given { - pathParam("project", "1") - - body(Portfolio.Create("", Targets(emptySet(), 1))) - contentType(ContentType.JSON) - } When { - post() - } Then { - statusCode(400) - contentType(ContentType.JSON) - } - } - - /** - * Test that tries to obtain a portfolio without token. - */ - @Test - fun testGetWithoutToken() { - Given { - pathParam("project", "1") - } When { - get("/1") - } Then { - statusCode(401) - } - } - - /** - * Test that tries to obtain a portfolio with an invalid scope. - */ - @Test - @TestSecurity(user = "testUser", roles = ["runner"]) - fun testGetInvalidToken() { - Given { - pathParam("project", "1") - } When { - get("/1") - } Then { - statusCode(403) - } - } - - /** - * Test that tries to obtain a non-existent portfolio. - */ - @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testGetNonExisting() { - every { portfolioService.findOne("testUser", 1, 1) } returns null - - Given { - pathParam("project", "1") - } When { - get("/1") - } Then { - statusCode(404) - contentType(ContentType.JSON) - } - } - - /** - * Test that tries to obtain a portfolio. - */ - @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testGetExisting() { - every { portfolioService.findOne("testUser", 1, 1) } returns dummyPortfolio - - Given { - pathParam("project", "1") - } When { - get("/1") - } Then { - statusCode(200) - contentType(ContentType.JSON) - body("id", Matchers.equalTo(1)) - } - } - - /** - * Test to delete a non-existent portfolio. - */ - @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testDeleteNonExistent() { - every { portfolioService.delete("testUser", 1, 1) } returns null - - Given { - pathParam("project", "1") - } When { - delete("/1") - } Then { - statusCode(404) - } - } - - /** - * Test to delete a portfolio. - */ - @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testDelete() { - every { portfolioService.delete("testUser", 1, 1) } returns dummyPortfolio - - Given { - pathParam("project", "1") - } When { - delete("/1") - } Then { - statusCode(200) - contentType(ContentType.JSON) - } - } -} diff --git a/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/user/PortfolioScenarioResourceTest.kt b/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/user/PortfolioScenarioResourceTest.kt deleted file mode 100644 index 676a43dc..00000000 --- a/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/user/PortfolioScenarioResourceTest.kt +++ /dev/null @@ -1,222 +0,0 @@ -/* - * 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.server.rest.user - -import io.mockk.every -import io.quarkiverse.test.junit.mockk.InjectMock -import io.quarkus.test.common.http.TestHTTPEndpoint -import io.quarkus.test.junit.QuarkusMock -import io.quarkus.test.junit.QuarkusTest -import io.quarkus.test.security.TestSecurity -import io.restassured.http.ContentType -import io.restassured.module.kotlin.extensions.Given -import io.restassured.module.kotlin.extensions.Then -import io.restassured.module.kotlin.extensions.When -import org.hamcrest.Matchers -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.opendc.web.proto.JobState -import org.opendc.web.proto.OperationalPhenomena -import org.opendc.web.proto.Targets -import org.opendc.web.proto.Trace -import org.opendc.web.proto.Workload -import org.opendc.web.proto.user.Job -import org.opendc.web.proto.user.Portfolio -import org.opendc.web.proto.user.Project -import org.opendc.web.proto.user.ProjectRole -import org.opendc.web.proto.user.Scenario -import org.opendc.web.proto.user.Topology -import org.opendc.web.server.service.ScenarioService -import java.time.Instant - -/** - * Test suite for [PortfolioScenarioResource]. - */ -@QuarkusTest -@TestHTTPEndpoint(PortfolioScenarioResource::class) -class PortfolioScenarioResourceTest { - @InjectMock - private lateinit var scenarioService: ScenarioService - - /** - * Dummy values - */ - private val dummyProject = Project(0, "test", Instant.now(), Instant.now(), ProjectRole.OWNER) - private val dummyPortfolio = Portfolio.Summary(1, 1, "test", Targets(emptySet())) - private val dummyJob = Job(1, JobState.PENDING, Instant.now(), Instant.now(), null) - private val dummyTrace = Trace("bitbrains", "Bitbrains", "vm") - private val dummyTopology = Topology.Summary(1, 1, "test", Instant.now(), Instant.now()) - private val dummyScenario = Scenario( - 1, - 1, - dummyProject, - dummyPortfolio, - "test", - Workload(dummyTrace, 1.0), - dummyTopology, - OperationalPhenomena(false, false), - "test", - dummyJob - ) - - @BeforeEach - fun setUp() { - QuarkusMock.installMockForType(scenarioService, ScenarioService::class.java) - } - - /** - * Test that tries to obtain a portfolio without token. - */ - @Test - fun testGetWithoutToken() { - Given { - pathParam("project", "1") - pathParam("portfolio", "1") - } When { - get() - } Then { - statusCode(401) - } - } - - /** - * Test that tries to obtain a portfolio with an invalid scope. - */ - @Test - @TestSecurity(user = "testUser", roles = ["runner"]) - fun testGetInvalidToken() { - Given { - pathParam("project", "1") - pathParam("portfolio", "1") - } When { - get() - } Then { - statusCode(403) - } - } - - /** - * Test that tries to obtain a non-existent portfolio. - */ - @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testGet() { - every { scenarioService.findAll("testUser", 1, 1) } returns emptyList() - - Given { - pathParam("project", "1") - pathParam("portfolio", "1") - } When { - get() - } Then { - statusCode(200) - contentType(ContentType.JSON) - } - } - - /** - * Test that tries to create a scenario for a portfolio. - */ - @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testCreateNonExistent() { - every { scenarioService.create("testUser", 1, any(), any()) } returns null - - Given { - pathParam("project", "1") - pathParam("portfolio", "1") - - body(Scenario.Create("test", Workload.Spec("test", 1.0), 1, OperationalPhenomena(false, false), "test")) - contentType(ContentType.JSON) - } When { - post() - } Then { - statusCode(404) - contentType(ContentType.JSON) - } - } - - /** - * Test that tries to create a scenario for a portfolio. - */ - @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testCreate() { - every { scenarioService.create("testUser", 1, 1, any()) } returns dummyScenario - - Given { - pathParam("project", "1") - pathParam("portfolio", "1") - - body(Scenario.Create("test", Workload.Spec("test", 1.0), 1, OperationalPhenomena(false, false), "test")) - contentType(ContentType.JSON) - } When { - post() - } Then { - statusCode(200) - contentType(ContentType.JSON) - body("id", Matchers.equalTo(1)) - body("name", Matchers.equalTo("test")) - } - } - - /** - * Test to create a project with an empty body. - */ - @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testCreateEmpty() { - Given { - pathParam("project", "1") - pathParam("portfolio", "1") - - body("{}") - contentType(ContentType.JSON) - } When { - post() - } Then { - statusCode(400) - contentType(ContentType.JSON) - } - } - - /** - * Test to create a project with a blank name. - */ - @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testCreateBlankName() { - Given { - pathParam("project", "1") - pathParam("portfolio", "1") - - body(Scenario.Create("", Workload.Spec("test", 1.0), 1, OperationalPhenomena(false, false), "test")) - contentType(ContentType.JSON) - } When { - post() - } Then { - statusCode(400) - contentType(ContentType.JSON) - } - } -} diff --git a/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/user/ProjectResourceTest.kt b/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/user/ProjectResourceTest.kt deleted file mode 100644 index fec8759c..00000000 --- a/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/user/ProjectResourceTest.kt +++ /dev/null @@ -1,240 +0,0 @@ -/* - * 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.server.rest.user - -import io.mockk.every -import io.quarkiverse.test.junit.mockk.InjectMock -import io.quarkus.test.common.http.TestHTTPEndpoint -import io.quarkus.test.junit.QuarkusMock -import io.quarkus.test.junit.QuarkusTest -import io.quarkus.test.security.TestSecurity -import io.restassured.http.ContentType -import io.restassured.module.kotlin.extensions.Given -import io.restassured.module.kotlin.extensions.Then -import io.restassured.module.kotlin.extensions.When -import org.hamcrest.Matchers.equalTo -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.opendc.web.proto.user.Project -import org.opendc.web.proto.user.ProjectRole -import org.opendc.web.server.service.ProjectService -import java.time.Instant - -/** - * Test suite for [ProjectResource]. - */ -@QuarkusTest -@TestHTTPEndpoint(ProjectResource::class) -class ProjectResourceTest { - @InjectMock - private lateinit var projectService: ProjectService - - /** - * Dummy values. - */ - private val dummyProject = Project(0, "test", Instant.now(), Instant.now(), ProjectRole.OWNER) - - @BeforeEach - fun setUp() { - QuarkusMock.installMockForType(projectService, ProjectService::class.java) - } - - /** - * Test that tries to obtain all projects without token. - */ - @Test - fun testGetAllWithoutToken() { - When { - get() - } Then { - statusCode(401) - } - } - - /** - * Test that tries to obtain all projects with an invalid scope. - */ - @Test - @TestSecurity(user = "testUser", roles = ["runner"]) - fun testGetAllWithInvalidScope() { - When { - get() - } Then { - statusCode(403) - } - } - - /** - * Test that tries to obtain all project for a user. - */ - @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testGetAll() { - val projects = listOf(dummyProject) - every { projectService.findWithUser("testUser") } returns projects - - When { - get() - } Then { - statusCode(200) - contentType(ContentType.JSON) - body("get(0).name", equalTo("test")) - } - } - - /** - * Test that tries to obtain a non-existent project. - */ - @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testGetNonExisting() { - every { projectService.findWithUser("testUser", 1) } returns null - - When { - get("/1") - } Then { - statusCode(404) - contentType(ContentType.JSON) - } - } - - /** - * Test that tries to obtain a job. - */ - @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testGetExisting() { - every { projectService.findWithUser("testUser", 1) } returns dummyProject - - When { - get("/1") - } Then { - statusCode(200) - contentType(ContentType.JSON) - body("id", equalTo(0)) - } - } - - /** - * Test that tries to create a project. - */ - @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testCreate() { - every { projectService.createForUser("testUser", "test") } returns dummyProject - - Given { - body(Project.Create("test")) - contentType(ContentType.JSON) - } When { - post() - } Then { - statusCode(200) - contentType(ContentType.JSON) - body("id", equalTo(0)) - body("name", equalTo("test")) - } - } - - /** - * Test to create a project with an empty body. - */ - @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testCreateEmpty() { - Given { - body("{}") - contentType(ContentType.JSON) - } When { - post() - } Then { - statusCode(400) - contentType(ContentType.JSON) - } - } - - /** - * Test to create a project with a blank name. - */ - @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testCreateBlankName() { - Given { - body(Project.Create("")) - contentType(ContentType.JSON) - } When { - post() - } Then { - statusCode(400) - contentType(ContentType.JSON) - } - } - - /** - * Test to delete a non-existent project. - */ - @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testDeleteNonExistent() { - every { projectService.deleteWithUser("testUser", 1) } returns null - - When { - delete("/1") - } Then { - statusCode(404) - contentType(ContentType.JSON) - } - } - - /** - * Test to delete a project. - */ - @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testDelete() { - every { projectService.deleteWithUser("testUser", 1) } returns dummyProject - - When { - delete("/1") - } Then { - statusCode(200) - contentType(ContentType.JSON) - } - } - - /** - * Test to delete a project which the user does not own. - */ - @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testDeleteNonOwner() { - every { projectService.deleteWithUser("testUser", 1) } throws IllegalArgumentException("User does not own project") - - When { - delete("/1") - } Then { - statusCode(403) - contentType(ContentType.JSON) - } - } -} diff --git a/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/user/ScenarioResourceTest.kt b/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/user/ScenarioResourceTest.kt deleted file mode 100644 index 2e080971..00000000 --- a/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/user/ScenarioResourceTest.kt +++ /dev/null @@ -1,187 +0,0 @@ -/* - * 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.server.rest.user - -import io.mockk.every -import io.quarkiverse.test.junit.mockk.InjectMock -import io.quarkus.test.common.http.TestHTTPEndpoint -import io.quarkus.test.junit.QuarkusMock -import io.quarkus.test.junit.QuarkusTest -import io.quarkus.test.security.TestSecurity -import io.restassured.http.ContentType -import io.restassured.module.kotlin.extensions.Given -import io.restassured.module.kotlin.extensions.Then -import io.restassured.module.kotlin.extensions.When -import org.hamcrest.Matchers -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.opendc.web.proto.JobState -import org.opendc.web.proto.OperationalPhenomena -import org.opendc.web.proto.Targets -import org.opendc.web.proto.Trace -import org.opendc.web.proto.Workload -import org.opendc.web.proto.user.Job -import org.opendc.web.proto.user.Portfolio -import org.opendc.web.proto.user.Project -import org.opendc.web.proto.user.ProjectRole -import org.opendc.web.proto.user.Scenario -import org.opendc.web.proto.user.Topology -import org.opendc.web.server.service.ScenarioService -import java.time.Instant - -/** - * Test suite for [ScenarioResource]. - */ -@QuarkusTest -@TestHTTPEndpoint(ScenarioResource::class) -class ScenarioResourceTest { - @InjectMock - private lateinit var scenarioService: ScenarioService - - /** - * Dummy values - */ - private val dummyProject = Project(0, "test", Instant.now(), Instant.now(), ProjectRole.OWNER) - private val dummyPortfolio = Portfolio.Summary(1, 1, "test", Targets(emptySet())) - private val dummyJob = Job(1, JobState.PENDING, Instant.now(), Instant.now(), null) - private val dummyTrace = Trace("bitbrains", "Bitbrains", "vm") - private val dummyTopology = Topology.Summary(1, 1, "test", Instant.now(), Instant.now()) - private val dummyScenario = Scenario( - 1, - 1, - dummyProject, - dummyPortfolio, - "test", - Workload(dummyTrace, 1.0), - dummyTopology, - OperationalPhenomena(false, false), - "test", - dummyJob - ) - - @BeforeEach - fun setUp() { - QuarkusMock.installMockForType(scenarioService, ScenarioService::class.java) - } - - /** - * Test that tries to obtain a scenario without token. - */ - @Test - fun testGetWithoutToken() { - Given { - pathParam("project", "1") - } When { - get("/1") - } Then { - statusCode(401) - } - } - - /** - * Test that tries to obtain a scenario with an invalid scope. - */ - @Test - @TestSecurity(user = "testUser", roles = ["runner"]) - fun testGetInvalidToken() { - Given { - pathParam("project", "1") - } When { - get("/1") - } Then { - statusCode(403) - } - } - - /** - * Test that tries to obtain a non-existent scenario. - */ - @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testGetNonExisting() { - every { scenarioService.findOne("testUser", 1, 1) } returns null - - Given { - pathParam("project", "1") - } When { - get("/1") - } Then { - statusCode(404) - contentType(ContentType.JSON) - } - } - - /** - * Test that tries to obtain a scenario. - */ - @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testGetExisting() { - every { scenarioService.findOne("testUser", 1, 1) } returns dummyScenario - - Given { - pathParam("project", "1") - } When { - get("/1") - } Then { - statusCode(200) - contentType(ContentType.JSON) - body("id", Matchers.equalTo(1)) - } - } - - /** - * Test to delete a non-existent scenario. - */ - @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testDeleteNonExistent() { - every { scenarioService.delete("testUser", 1, 1) } returns null - - Given { - pathParam("project", "1") - } When { - delete("/1") - } Then { - statusCode(404) - } - } - - /** - * Test to delete a scenario. - */ - @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testDelete() { - every { scenarioService.delete("testUser", 1, 1) } returns dummyScenario - - Given { - pathParam("project", "1") - } When { - delete("/1") - } Then { - statusCode(200) - contentType(ContentType.JSON) - } - } -} diff --git a/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/user/TopologyResourceTest.kt b/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/user/TopologyResourceTest.kt deleted file mode 100644 index 8a542d33..00000000 --- a/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/rest/user/TopologyResourceTest.kt +++ /dev/null @@ -1,304 +0,0 @@ -/* - * 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.server.rest.user - -import io.mockk.every -import io.quarkiverse.test.junit.mockk.InjectMock -import io.quarkus.test.common.http.TestHTTPEndpoint -import io.quarkus.test.junit.QuarkusMock -import io.quarkus.test.junit.QuarkusTest -import io.quarkus.test.security.TestSecurity -import io.restassured.http.ContentType -import io.restassured.module.kotlin.extensions.Given -import io.restassured.module.kotlin.extensions.Then -import io.restassured.module.kotlin.extensions.When -import org.hamcrest.Matchers -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.opendc.web.proto.user.Project -import org.opendc.web.proto.user.ProjectRole -import org.opendc.web.proto.user.Topology -import org.opendc.web.server.service.TopologyService -import java.time.Instant - -/** - * Test suite for [TopologyResource]. - */ -@QuarkusTest -@TestHTTPEndpoint(TopologyResource::class) -class TopologyResourceTest { - @InjectMock - private lateinit var topologyService: TopologyService - - /** - * Dummy project and topology. - */ - private val dummyProject = Project(1, "test", Instant.now(), Instant.now(), ProjectRole.OWNER) - private val dummyTopology = Topology(1, 1, dummyProject, "test", emptyList(), Instant.now(), Instant.now()) - - @BeforeEach - fun setUp() { - QuarkusMock.installMockForType(topologyService, TopologyService::class.java) - } - - /** - * Test that tries to obtain the list of topologies belonging to a project. - */ - @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testGetForProject() { - every { topologyService.findAll("testUser", 1) } returns emptyList() - - Given { - pathParam("project", "1") - } When { - get() - } Then { - statusCode(200) - contentType(ContentType.JSON) - } - } - - /** - * Test that tries to create a topology for a project. - */ - @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testCreateNonExistent() { - every { topologyService.create("testUser", 1, any()) } returns null - - Given { - pathParam("project", "1") - - body(Topology.Create("test", emptyList())) - contentType(ContentType.JSON) - } When { - post() - } Then { - statusCode(404) - contentType(ContentType.JSON) - } - } - - /** - * Test that tries to create a topology for a project. - */ - @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testCreate() { - every { topologyService.create("testUser", 1, any()) } returns dummyTopology - - Given { - pathParam("project", "1") - - body(Topology.Create("test", emptyList())) - contentType(ContentType.JSON) - } When { - post() - } Then { - statusCode(200) - contentType(ContentType.JSON) - body("id", Matchers.equalTo(1)) - body("name", Matchers.equalTo("test")) - } - } - - /** - * Test to create a topology with an empty body. - */ - @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testCreateEmpty() { - Given { - pathParam("project", "1") - - body("{}") - contentType(ContentType.JSON) - } When { - post() - } Then { - statusCode(400) - contentType(ContentType.JSON) - } - } - - /** - * Test to create a topology with a blank name. - */ - @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testCreateBlankName() { - Given { - pathParam("project", "1") - - body(Topology.Create("", emptyList())) - contentType(ContentType.JSON) - } When { - post() - } Then { - statusCode(400) - contentType(ContentType.JSON) - } - } - - /** - * Test that tries to obtain a topology without token. - */ - @Test - fun testGetWithoutToken() { - Given { - pathParam("project", "1") - } When { - get("/1") - } Then { - statusCode(401) - } - } - - /** - * Test that tries to obtain a topology with an invalid scope. - */ - @Test - @TestSecurity(user = "testUser", roles = ["runner"]) - fun testGetInvalidToken() { - Given { - pathParam("project", "1") - } When { - get("/1") - } Then { - statusCode(403) - } - } - - /** - * Test that tries to obtain a non-existent topology. - */ - @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testGetNonExisting() { - every { topologyService.findOne("testUser", 1, 1) } returns null - - Given { - pathParam("project", "1") - } When { - get("/1") - } Then { - statusCode(404) - contentType(ContentType.JSON) - } - } - - /** - * Test that tries to obtain a topology. - */ - @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testGetExisting() { - every { topologyService.findOne("testUser", 1, 1) } returns dummyTopology - - Given { - pathParam("project", "1") - } When { - get("/1") - } Then { - statusCode(200) - contentType(ContentType.JSON) - body("id", Matchers.equalTo(1)) - println(extract().asPrettyString()) - } - } - - /** - * Test to delete a non-existent topology. - */ - @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testUpdateNonExistent() { - every { topologyService.update("testUser", any(), any(), any()) } returns null - - Given { - pathParam("project", "1") - body(Topology.Update(emptyList())) - contentType(ContentType.JSON) - } When { - put("/1") - } Then { - statusCode(404) - } - } - - /** - * Test to update a topology. - */ - @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testUpdate() { - every { topologyService.update("testUser", any(), any(), any()) } returns dummyTopology - - Given { - pathParam("project", "1") - body(Topology.Update(emptyList())) - contentType(ContentType.JSON) - } When { - put("/1") - } Then { - statusCode(200) - contentType(ContentType.JSON) - } - } - - /** - * Test to delete a non-existent topology. - */ - @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testDeleteNonExistent() { - every { topologyService.delete("testUser", 1, 1) } returns null - - Given { - pathParam("project", "1") - } When { - delete("/1") - } Then { - statusCode(404) - } - } - - /** - * Test to delete a topology. - */ - @Test - @TestSecurity(user = "testUser", roles = ["openid"]) - fun testDelete() { - every { topologyService.delete("testUser", 1, 1) } returns dummyTopology - - Given { - pathParam("project", "1") - } When { - delete("/1") - } Then { - statusCode(200) - contentType(ContentType.JSON) - } - } -} diff --git a/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/service/UserAccountingServiceTest.kt b/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/service/UserAccountingServiceTest.kt deleted file mode 100644 index fdf04787..00000000 --- a/opendc-web/opendc-web-server/src/test/kotlin/org/opendc/web/server/service/UserAccountingServiceTest.kt +++ /dev/null @@ -1,203 +0,0 @@ -/* - * 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.server.service - -import io.mockk.every -import io.mockk.mockk -import io.quarkus.test.junit.QuarkusTest -import org.junit.jupiter.api.Assertions.assertAll -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertFalse -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import org.opendc.web.server.model.UserAccounting -import org.opendc.web.server.repository.UserAccountingRepository -import java.time.Duration -import java.time.LocalDate -import javax.persistence.EntityExistsException - -/** - * Test suite for the [UserAccountingService]. - */ -@QuarkusTest -class UserAccountingServiceTest { - /** - * The [UserAccountingRepository] that is mocked. - */ - private val repository: UserAccountingRepository = mockk() - - /** - * The [UserAccountingService] instance under test. - */ - private val service: UserAccountingService = UserAccountingService(repository, Duration.ofHours(1)) - - @Test - fun testGetUserDoesNotExist() { - val userId = "test" - - every { repository.findForUser(userId) } returns null - - val accounting = service.getAccounting(userId) - - assertTrue(accounting.periodEnd.isAfter(LocalDate.now())) - assertEquals(0, accounting.simulationTime) - } - - @Test - fun testGetUserDoesExist() { - val userId = "test" - - val now = LocalDate.now() - val periodEnd = now.plusMonths(1) - - every { repository.findForUser(userId) } returns UserAccounting(userId, periodEnd, 3600).also { it.simulationTime = 32 } - - val accounting = service.getAccounting(userId) - - assertAll( - { assertEquals(periodEnd, accounting.periodEnd) }, - { assertEquals(32, accounting.simulationTime) }, - { assertEquals(3600, accounting.simulationTimeBudget) } - ) - } - - @Test - fun testHasBudgetUserDoesNotExist() { - val userId = "test" - - every { repository.findForUser(userId) } returns null - - assertTrue(service.hasSimulationBudget(userId)) - } - - @Test - fun testHasBudget() { - val userId = "test" - val periodEnd = LocalDate.now().plusMonths(2) - - every { repository.findForUser(userId) } returns UserAccounting(userId, periodEnd, 3600) - - assertTrue(service.hasSimulationBudget(userId)) - } - - @Test - fun testHasBudgetExceededButPeriodExpired() { - val userId = "test" - val periodEnd = LocalDate.now().minusMonths(2) - - every { repository.findForUser(userId) } returns UserAccounting(userId, periodEnd, 3600).also { it.simulationTime = 3900 } - - assertTrue(service.hasSimulationBudget(userId)) - } - - @Test - fun testHasBudgetPeriodExpired() { - val userId = "test" - val periodEnd = LocalDate.now().minusMonths(2) - - every { repository.findForUser(userId) } returns UserAccounting(userId, periodEnd, 3600) - - assertTrue(service.hasSimulationBudget(userId)) - } - - @Test - fun testHasBudgetExceeded() { - val userId = "test" - val periodEnd = LocalDate.now().plusMonths(1) - - every { repository.findForUser(userId) } returns UserAccounting(userId, periodEnd, 3600).also { it.simulationTime = 3900 } - - assertFalse(service.hasSimulationBudget(userId)) - } - - @Test - fun testConsumeBudgetNewUser() { - val userId = "test" - - every { repository.findForUser(userId) } returns null - every { repository.save(any()) } returns Unit - - assertFalse(service.consumeSimulationBudget(userId, 10)) - } - - @Test - fun testConsumeBudgetNewUserExceeded() { - val userId = "test" - - every { repository.findForUser(userId) } returns null - every { repository.save(any()) } returns Unit - - assertTrue(service.consumeSimulationBudget(userId, 4000)) - } - - @Test - fun testConsumeBudgetNewUserConflict() { - val userId = "test" - - val periodEnd = LocalDate.now().plusMonths(1) - - every { repository.findForUser(userId) } returns null andThen UserAccounting(userId, periodEnd, 3600) - every { repository.save(any()) } throws EntityExistsException() - every { repository.consumeBudget(any(), any()) } answers { - val accounting = it.invocation.args[0] as UserAccounting - accounting.simulationTime -= it.invocation.args[1] as Int - true - } - - assertFalse(service.consumeSimulationBudget(userId, 10)) - } - - @Test - fun testConsumeBudgetResetSuccess() { - val userId = "test" - - val periodEnd = LocalDate.now().minusMonths(2) - - every { repository.findForUser(userId) } returns UserAccounting(userId, periodEnd, 3600).also { it.simulationTime = 3900 } - every { repository.resetBudget(any(), any(), any()) } answers { - val accounting = it.invocation.args[0] as UserAccounting - accounting.periodEnd = it.invocation.args[1] as LocalDate - accounting.simulationTime = it.invocation.args[2] as Int - true - } - - assertTrue(service.consumeSimulationBudget(userId, 4000)) - } - - @Test - fun testInfiniteConflict() { - val userId = "test" - - val periodEnd = LocalDate.now().plusMonths(1) - - every { repository.findForUser(userId) } returns UserAccounting(userId, periodEnd, 3600) - every { repository.consumeBudget(any(), any()) } answers { - val accounting = it.invocation.args[0] as UserAccounting - accounting.simulationTime -= it.invocation.args[1] as Int - false - } - - assertThrows<IllegalStateException> { service.consumeSimulationBudget(userId, 10) } - } -} diff --git a/opendc-web/opendc-web-ui/src/components/portfolios/PortfolioResults.js b/opendc-web/opendc-web-ui/src/components/portfolios/PortfolioResults.js index f50105ed..62150fa7 100644 --- a/opendc-web/opendc-web-ui/src/components/portfolios/PortfolioResults.js +++ b/opendc-web/opendc-web-ui/src/components/portfolios/PortfolioResults.js @@ -57,14 +57,17 @@ function PortfolioResults({ projectId, portfolioId }) { const dataPerMetric = {} AVAILABLE_METRICS.forEach((metric) => { dataPerMetric[metric] = scenarios - .filter((scenario) => scenario.job?.results) - .map((scenario) => ({ - metric, - x: scenario.name, - y: mean(scenario.job.results[metric]), - errorY: std(scenario.job.results[metric]), - label, - })) + .filter((scenario) => scenario.jobs && scenario.jobs[scenario.jobs.length - 1].results) + .map((scenario) => { + const job = scenario.jobs[scenario.jobs.length - 1] + return { + metric, + x: scenario.name, + y: mean(job.results[metric]), + errorY: std(job.results[metric]), + label, + } + }) }) return dataPerMetric }, [scenarios]) diff --git a/opendc-web/opendc-web-ui/src/components/portfolios/ScenarioTable.js b/opendc-web/opendc-web-ui/src/components/portfolios/ScenarioTable.js index 5fd2a1da..b068d045 100644 --- a/opendc-web/opendc-web-ui/src/components/portfolios/ScenarioTable.js +++ b/opendc-web/opendc-web-ui/src/components/portfolios/ScenarioTable.js @@ -59,7 +59,7 @@ function ScenarioTable({ portfolio, status }) { <Td dataLabel="Topology"> {scenario.topology ? ( <Link href={`/projects/${projectId}/topologies/${scenario.topology.number}`}> - scenario.topology.name + {scenario.topology.name} </Link> ) : ( 'Unknown Topology' @@ -69,7 +69,7 @@ function ScenarioTable({ portfolio, status }) { scenario.workload.samplingFraction * 100 }%)`}</Td> <Td dataLabel="State"> - <ScenarioState state={scenario.job.state} /> + <ScenarioState state={scenario.jobs[scenario.jobs.length - 1].state} /> </Td> <Td isActionCell> <ActionsColumn items={actions(scenario)} /> diff --git a/opendc-web/opendc-web-ui/src/shapes.js b/opendc-web/opendc-web-ui/src/shapes.js index 6c93f458..50b82361 100644 --- a/opendc-web/opendc-web-ui/src/shapes.js +++ b/opendc-web/opendc-web-ui/src/shapes.js @@ -159,7 +159,7 @@ export const Scenario = PropTypes.shape({ topology: TopologySummary.isRequired, phenomena: Phenomena.isRequired, schedulerName: PropTypes.string.isRequired, - job: Job.isRequired, + jobs: PropTypes.arrayOf(Job).isRequired, }) export const Portfolio = PropTypes.shape({ |
