summaryrefslogtreecommitdiff
path: root/opendc-web
diff options
context:
space:
mode:
Diffstat (limited to 'opendc-web')
-rw-r--r--opendc-web/opendc-web-proto/src/main/kotlin/org/opendc/web/proto/user/Scenario.kt4
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Job.java43
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Portfolio.java17
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/ProjectAuthorization.java27
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Scenario.java42
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Topology.java16
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Trace.java2
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/BaseProtocol.java50
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/TraceResource.java6
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/JobResource.java29
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/RunnerProtocol.java78
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioResource.java77
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/PortfolioScenarioResource.java94
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/ProjectResource.java57
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/ScenarioResource.java61
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/TopologyResource.java94
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/user/UserProtocol.java132
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/JobService.java97
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/PortfolioService.java148
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ProjectService.java106
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ScenarioService.java231
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/TopologyService.java178
-rw-r--r--opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/runner/QuarkusJobManager.java63
-rw-r--r--opendc-web/opendc-web-server/src/main/resources/application-test.properties1
-rw-r--r--opendc-web/opendc-web-server/src/main/resources/application.properties1
-rw-r--r--opendc-web/opendc-web-server/src/main/resources/db/migration/V3.0__core.sql (renamed from opendc-web/opendc-web-server/src/main/resources/db/migration/V1.0.0__core.sql)74
-rw-r--r--opendc-web/opendc-web-server/src/main/resources/db/testing/V3.0.1__entities.sql24
-rw-r--r--opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/TraceResourceTest.java30
-rw-r--r--opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/runner/JobResourceTest.java75
-rw-r--r--opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/PortfolioResourceTest.java145
-rw-r--r--opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/PortfolioScenarioResourceTest.java171
-rw-r--r--opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/ProjectResourceTest.java75
-rw-r--r--opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/ScenarioResourceTest.java127
-rw-r--r--opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/rest/user/TopologyResourceTest.java175
-rw-r--r--opendc-web/opendc-web-server/src/test/java/org/opendc/web/server/service/JobServiceTest.java124
-rw-r--r--opendc-web/opendc-web-ui/src/components/portfolios/PortfolioResults.js19
-rw-r--r--opendc-web/opendc-web-ui/src/components/portfolios/ScenarioTable.js4
-rw-r--r--opendc-web/opendc-web-ui/src/shapes.js2
38 files changed, 1409 insertions, 1290 deletions
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/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
index 14fd3e2a..c5fb208e 100644
--- 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
@@ -22,18 +22,22 @@
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.List;
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.OneToOne;
import javax.persistence.Table;
import org.hibernate.annotations.Type;
import org.opendc.web.proto.JobState;
@@ -54,8 +58,8 @@ import org.opendc.web.proto.JobState;
""")
})
public class Job extends PanacheEntity {
- @OneToOne(optional = false, mappedBy = "job", fetch = FetchType.EAGER)
- @JoinColumn(name = "scenario_id", nullable = false)
+ @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)
@@ -74,12 +78,14 @@ public class Job extends PanacheEntity {
* The instant at which the job was updated.
*/
@Column(name = "updated_at", nullable = false)
- public Instant updatedAt = createdAt;
+ public Instant updatedAt;
/**
* The state of the job.
*/
- @Column(nullable = false)
+ @Type(type = "io.hypersistence.utils.hibernate.type.basic.PostgreSQLEnumType")
+ @Column(nullable = false, columnDefinition = "enum")
+ @Enumerated(EnumType.STRING)
public JobState state = JobState.PENDING;
/**
@@ -102,6 +108,7 @@ public class Job extends PanacheEntity {
this.createdBy = createdBy;
this.scenario = scenario;
this.createdAt = createdAt;
+ this.updatedAt = createdAt;
this.repeats = repeats;
}
@@ -114,10 +121,10 @@ public class Job extends PanacheEntity {
* Find {@link Job}s in the specified {@link JobState}.
*
* @param state The state of the jobs to find.
- * @return The list of jobs that are in the specified state.
+ * @return A query for jobs that are in the specified state.
*/
- public static List<Job> findByState(JobState state) {
- return find("state", state).list();
+ public static PanacheQuery<Job> findByState(JobState state) {
+ return find("state", state);
}
/**
@@ -137,6 +144,24 @@ public class Job extends PanacheEntity {
.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
index 4c3af570..3a406683 100644
--- 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
@@ -23,9 +23,9 @@
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.List;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Column;
@@ -48,8 +48,12 @@ import org.opendc.web.proto.Targets;
@Entity
@Table(
name = "portfolios",
- uniqueConstraints = {@UniqueConstraint(columnNames = {"project_id", "number"})},
- indexes = {@Index(name = "fn_portfolios_number", columnList = "project_id, number")})
+ 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(
@@ -112,11 +116,10 @@ public class Portfolio extends PanacheEntity {
* Find all {@link Portfolio}s that belong to the specified project
*
* @param projectId The unique identifier of the project.
- * @return The list of portfolios that belong to the specified project.
+ * @return The query of portfolios that belong to the specified project.
*/
- public static List<Portfolio> findByProject(long projectId) {
- return find("#Portfolio.findByProject", Parameters.with("projectId", projectId))
- .list();
+ public static PanacheQuery<Portfolio> findByProject(long projectId) {
+ return find("#Portfolio.findByProject", Parameters.with("projectId", projectId));
}
/**
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
index c10fcc64..1238f58d 100644
--- 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
@@ -23,20 +23,25 @@
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.List;
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;
/**
@@ -64,15 +69,22 @@ public class ProjectAuthorization extends PanacheEntityBase {
/**
* The project that the user is authorized to participate in.
*/
- @ManyToOne(optional = false)
+ @ManyToOne(optional = false, fetch = FetchType.LAZY)
@MapsId("projectId")
- @JoinColumn(name = "project_id", updatable = false, insertable = false, nullable = false)
+ @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.
*/
- @Column(nullable = false)
+ @Type(type = "io.hypersistence.utils.hibernate.type.basic.PostgreSQLEnumType")
+ @Column(nullable = false, columnDefinition = "enum")
+ @Enumerated(EnumType.STRING)
public ProjectRole role;
/**
@@ -93,11 +105,10 @@ public class ProjectAuthorization extends PanacheEntityBase {
* 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 list of projects that the user has received authorization for.
+ * @return A query returning projects that the user has received authorization for.
*/
- public static List<ProjectAuthorization> findByUser(String userId) {
- return find("#ProjectAuthorization.findByUser", Parameters.with("userId", userId))
- .list();
+ public static PanacheQuery<ProjectAuthorization> findByUser(String userId) {
+ return find("#ProjectAuthorization.findByUser", Parameters.with("userId", 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
index 9381f9be..016e931b 100644
--- 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
@@ -23,18 +23,22 @@
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.OneToOne;
+import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
import org.hibernate.annotations.Type;
@@ -46,8 +50,12 @@ import org.opendc.web.proto.OperationalPhenomena;
@Entity
@Table(
name = "scenarios",
- uniqueConstraints = {@UniqueConstraint(columnNames = {"project_id", "number"})},
- indexes = {@Index(name = "fn_scenarios_number", columnList = "project_id, number")})
+ 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(
@@ -68,14 +76,14 @@ public class Scenario extends PanacheEntity {
* The {@link Project} to which this scenario belongs.
*/
@ManyToOne(optional = false)
- @JoinColumn(name = "project_id", nullable = 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)
+ @JoinColumn(name = "portfolio_id", nullable = false, foreignKey = @ForeignKey(name = "fk_scenarios_portfolio"))
public Portfolio portfolio;
/**
@@ -100,6 +108,7 @@ public class Scenario extends PanacheEntity {
* Topology details of the scenario.
*/
@ManyToOne(optional = false)
+ @JoinColumn(name = "topology_id", nullable = false, foreignKey = @ForeignKey(name = "fk_scenarios_topology"))
public Topology topology;
/**
@@ -118,8 +127,11 @@ public class Scenario extends PanacheEntity {
/**
* The {@link Job} associated with the scenario.
*/
- @OneToOne(cascade = {CascadeType.ALL})
- public Job job;
+ @OneToMany(
+ cascade = {CascadeType.ALL},
+ mappedBy = "scenario",
+ fetch = FetchType.LAZY)
+ public List<Job> jobs = new ArrayList<>();
/**
* Construct a {@link Scenario} object.
@@ -152,11 +164,10 @@ public class Scenario extends PanacheEntity {
* Find all {@link Scenario}s that belong to the specified project
*
* @param projectId The unique identifier of the project.
- * @return The list of scenarios that belong to the specified project.
+ * @return The query of scenarios that belong to the specified project.
*/
- public static List<Scenario> findByProject(long projectId) {
- return find("#Scenario.findByProject", Parameters.with("projectId", projectId))
- .list();
+ public static PanacheQuery<Scenario> findByProject(long projectId) {
+ return find("#Scenario.findByProject", Parameters.with("projectId", projectId));
}
/**
@@ -164,13 +175,12 @@ public class Scenario extends PanacheEntity {
*
* @param projectId The unique identifier of the project.
* @param number The number of the portfolio.
- * @return The list of scenarios that belong to the specified project and portfolio..
+ * @return The query of scenarios that belong to the specified project and portfolio..
*/
- public static List<Scenario> findByPortfolio(long projectId, int number) {
+ public static PanacheQuery<Scenario> findByPortfolio(long projectId, int number) {
return find(
- "#Scenario.findByPortfolio",
- Parameters.with("projectId", projectId).and("number", number))
- .list();
+ "#Scenario.findByPortfolio",
+ Parameters.with("projectId", projectId).and("number", number));
}
/**
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
index 6ec83f78..05a1ac12 100644
--- 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
@@ -23,6 +23,7 @@
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;
@@ -44,8 +45,12 @@ import org.opendc.web.proto.Room;
@Entity
@Table(
name = "topologies",
- uniqueConstraints = {@UniqueConstraint(columnNames = {"project_id", "number"})},
- indexes = {@Index(name = "fn_topologies_number", columnList = "project_id, number")})
+ 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(
@@ -112,11 +117,10 @@ public class Topology extends PanacheEntity {
* 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.
+ * @return The query of topologies that belong to the specified project.
*/
- public static List<Topology> findByProject(long projectId) {
- return find("#Topology.findByProject", Parameters.with("projectId", projectId))
- .list();
+ public static PanacheQuery<Topology> findByProject(long projectId) {
+ return find("#Topology.findByProject", Parameters.with("projectId", projectId));
}
/**
diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Trace.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Trace.java
index f73c8494..36d27abc 100644
--- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Trace.java
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Trace.java
@@ -26,11 +26,13 @@ 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.
*/
@Entity
+@Table(name = "traces")
public class Trace extends PanacheEntityBase {
/**
* The unique identifier of the trace.
diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/BaseProtocol.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/BaseProtocol.java
new file mode 100644
index 00000000..44d2d569
--- /dev/null
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/BaseProtocol.java
@@ -0,0 +1,50 @@
+/*
+ * 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 org.opendc.web.server.model.Trace;
+import org.opendc.web.server.model.Workload;
+
+/**
+ * DTO-conversions for the base protocol.
+ */
+public final class BaseProtocol {
+ /**
+ * Private constructor to prevent instantiation of class.
+ */
+ 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);
+ }
+
+ /**
+ * 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/java/org/opendc/web/server/rest/TraceResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/TraceResource.java
index 2b1efb02..7316c93f 100644
--- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/TraceResource.java
+++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/TraceResource.java
@@ -43,7 +43,7 @@ public final class TraceResource {
@GET
public List<org.opendc.web.proto.Trace> getAll() {
Stream<Trace> entities = Trace.streamAll();
- return entities.map(TraceResource::toUserDto).toList();
+ return entities.map(TraceResource::toDto).toList();
}
/**
@@ -58,13 +58,13 @@ public final class TraceResource {
throw new WebApplicationException("Trace not found", 404);
}
- return toUserDto(trace);
+ return toDto(trace);
}
/**
* Convert a {@link Trace] entity into a {@link org.opendc.web.proto.Trace} DTO.
*/
- public static org.opendc.web.proto.Trace toUserDto(Trace trace) {
+ 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/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
index 134c6814..dff52526 100644
--- 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
@@ -33,6 +33,8 @@ 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;
/**
@@ -43,14 +45,14 @@ import org.opendc.web.server.service.JobService;
@RolesAllowed("runner")
public final class JobResource {
/**
- * The {@link JobService} responsible for managing the jobs.
+ * The {@link JobService} for helping manage the job lifecycle.
*/
private final JobService jobService;
/**
* Construct a {@link JobResource} instance.
*
- * @param jobService The {@link JobService} responsible for managing the jobs.
+ * @param jobService The {@link JobService} for managing the job lifecycle.
*/
public JobResource(JobService jobService) {
this.jobService = jobService;
@@ -61,7 +63,9 @@ public final class JobResource {
*/
@GET
public List<org.opendc.web.proto.runner.Job> queryPending() {
- return jobService.listPending();
+ return Job.findByState(JobState.PENDING).list().stream()
+ .map(RunnerProtocol::toDto)
+ .toList();
}
/**
@@ -70,12 +74,13 @@ public final class JobResource {
@GET
@Path("{job}")
public org.opendc.web.proto.runner.Job get(@PathParam("job") long id) {
- org.opendc.web.proto.runner.Job job = jobService.findById(id);
+ Job job = Job.findById(id);
+
if (job == null) {
throw new WebApplicationException("Job not found", 404);
}
- return job;
+ return RunnerProtocol.toDto(job);
}
/**
@@ -87,17 +92,19 @@ public final class JobResource {
@Transactional
public org.opendc.web.proto.runner.Job update(
@PathParam("job") long id, @Valid org.opendc.web.proto.runner.Job.Update update) {
- try {
- var job = jobService.updateState(id, update.getState(), update.getRuntime(), update.getResults());
- if (job == null) {
- throw new WebApplicationException("Job not found", 404);
- }
+ Job job = Job.findById(id);
+ if (job == null) {
+ throw new WebApplicationException("Job not found", 404);
+ }
- return job;
+ 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
index e8e05f97..d1fc980d 100644
--- 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
@@ -23,10 +23,12 @@
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;
@@ -34,7 +36,8 @@ 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.service.PortfolioService;
+import org.opendc.web.server.model.Portfolio;
+import org.opendc.web.server.model.ProjectAuthorization;
/**
* A resource representing the portfolios of a project.
@@ -44,11 +47,6 @@ import org.opendc.web.server.service.PortfolioService;
@RolesAllowed("openid")
public final class PortfolioResource {
/**
- * The service for managing the user portfolios.
- */
- private final PortfolioService portfolioService;
-
- /**
* The identity of the current user.
*/
private final SecurityIdentity identity;
@@ -56,11 +54,9 @@ public final class PortfolioResource {
/**
* Construct a {@link PortfolioResource}.
*
- * @param portfolioService The {@link PortfolioService} instance to use.
* @param identity The {@link SecurityIdentity} of the current user.
*/
- public PortfolioResource(PortfolioService portfolioService, SecurityIdentity identity) {
- this.portfolioService = portfolioService;
+ public PortfolioResource(SecurityIdentity identity) {
this.identity = identity;
}
@@ -69,7 +65,17 @@ public final class PortfolioResource {
*/
@GET
public List<org.opendc.web.proto.user.Portfolio> getAll(@PathParam("project") long projectId) {
- return portfolioService.findByUser(identity.getPrincipal().getName(), 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();
}
/**
@@ -77,14 +83,29 @@ public final class PortfolioResource {
*/
@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) {
- var portfolio = portfolioService.create(identity.getPrincipal().getName(), projectId, request);
- if (portfolio == null) {
+ // 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);
}
- return portfolio;
+ 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);
}
/**
@@ -94,12 +115,21 @@ public final class PortfolioResource {
@Path("{portfolio}")
public org.opendc.web.proto.user.Portfolio get(
@PathParam("project") long projectId, @PathParam("portfolio") int number) {
- var portfolio = portfolioService.findByUser(identity.getPrincipal().getName(), projectId, 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 portfolio;
+ return UserProtocol.toDto(portfolio, auth);
}
/**
@@ -110,11 +140,22 @@ public final class PortfolioResource {
@Transactional
public org.opendc.web.proto.user.Portfolio delete(
@PathParam("project") long projectId, @PathParam("portfolio") int number) {
- var portfolio = portfolioService.delete(identity.getPrincipal().getName(), projectId, number);
- if (portfolio == null) {
+ // 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);
}
- return portfolio;
+ 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
index a6db7c54..a058cd31 100644
--- 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
@@ -23,28 +23,39 @@
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.user.Scenario;
-import org.opendc.web.server.service.ScenarioService;
+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 scenarios.
+ * The service for managing the user accounting.
*/
- private final ScenarioService scenarioService;
+ private final UserAccountingService accountingService;
/**
* The identity of the current user.
@@ -54,11 +65,11 @@ public final class PortfolioScenarioResource {
/**
* Construct a {@link PortfolioScenarioResource}.
*
- * @param scenarioService The {@link ScenarioService} instance to use.
+ * @param accountingService The {@link UserAccountingService} instance to use.
* @param identity The {@link SecurityIdentity} of the current user.
*/
- public PortfolioScenarioResource(ScenarioService scenarioService, SecurityIdentity identity) {
- this.scenarioService = scenarioService;
+ public PortfolioScenarioResource(UserAccountingService accountingService, SecurityIdentity identity) {
+ this.accountingService = accountingService;
this.identity = identity;
}
@@ -66,8 +77,19 @@ public final class PortfolioScenarioResource {
* Get all scenarios that belong to the specified portfolio.
*/
@GET
- public List<Scenario> get(@PathParam("project") long projectId, @PathParam("portfolio") int portfolioNumber) {
- return scenarioService.findAll(identity.getPrincipal().getName(), projectId, portfolioNumber);
+ 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();
}
/**
@@ -75,15 +97,63 @@ public final class PortfolioScenarioResource {
*/
@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) {
- var scenario = scenarioService.create(identity.getPrincipal().getName(), projectId, portfolioNumber, request);
- if (scenario == null) {
+ // 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);
}
- return scenario;
+ 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
index b0b8eb4e..da47c3ff 100644
--- 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
@@ -23,6 +23,7 @@
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;
@@ -35,7 +36,9 @@ 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.service.ProjectService;
+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.
@@ -45,11 +48,6 @@ import org.opendc.web.server.service.ProjectService;
@RolesAllowed("openid")
public final class ProjectResource {
/**
- * The service for managing the user projects.
- */
- private final ProjectService projectService;
-
- /**
* The identity of the current user.
*/
private final SecurityIdentity identity;
@@ -57,11 +55,9 @@ public final class ProjectResource {
/**
* Construct a {@link ProjectResource}.
*
- * @param projectService The {@link ProjectService} instance to use.
* @param identity The {@link SecurityIdentity} of the current user.
*/
- public ProjectResource(ProjectService projectService, SecurityIdentity identity) {
- this.projectService = projectService;
+ public ProjectResource(SecurityIdentity identity) {
this.identity = identity;
}
@@ -70,7 +66,9 @@ public final class ProjectResource {
*/
@GET
public List<org.opendc.web.proto.user.Project> getAll() {
- return projectService.findByUser(identity.getPrincipal().getName());
+ return ProjectAuthorization.findByUser(identity.getPrincipal().getName()).list().stream()
+ .map(UserProtocol::toDto)
+ .toList();
}
/**
@@ -80,7 +78,17 @@ public final class ProjectResource {
@Transactional
@Consumes("application/json")
public org.opendc.web.proto.user.Project create(@Valid org.opendc.web.proto.user.Project.Create request) {
- return projectService.create(identity.getPrincipal().getName(), request.getName());
+ 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);
}
/**
@@ -89,12 +97,14 @@ public final class ProjectResource {
@GET
@Path("{project}")
public org.opendc.web.proto.user.Project get(@PathParam("project") long id) {
- var project = projectService.findByUser(identity.getPrincipal().getName(), id);
- if (project == null) {
+ ProjectAuthorization auth =
+ ProjectAuthorization.findByUser(identity.getPrincipal().getName(), id);
+
+ if (auth == null) {
throw new WebApplicationException("Project not found", 404);
}
- return project;
+ return UserProtocol.toDto(auth);
}
/**
@@ -104,15 +114,18 @@ public final class ProjectResource {
@Path("{project}")
@Transactional
public org.opendc.web.proto.user.Project delete(@PathParam("project") long id) {
- try {
- var project = projectService.delete(identity.getPrincipal().getName(), id);
- if (project == null) {
- throw new WebApplicationException("Project not found", 404);
- }
+ ProjectAuthorization auth =
+ ProjectAuthorization.findByUser(identity.getPrincipal().getName(), id);
- return project;
- } catch (IllegalArgumentException e) {
- throw new WebApplicationException(e.getMessage(), 403);
+ 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
index a6838148..cf933c32 100644
--- 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
@@ -23,6 +23,7 @@
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;
@@ -31,7 +32,8 @@ 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.service.ScenarioService;
+import org.opendc.web.server.model.ProjectAuthorization;
+import org.opendc.web.server.model.Scenario;
/**
* A resource representing the scenarios of a portfolio.
@@ -41,11 +43,6 @@ import org.opendc.web.server.service.ScenarioService;
@RolesAllowed("openid")
public final class ScenarioResource {
/**
- * The service for managing the user scenarios.
- */
- private final ScenarioService scenarioService;
-
- /**
* The identity of the current user.
*/
private final SecurityIdentity identity;
@@ -53,27 +50,52 @@ public final class ScenarioResource {
/**
* Construct a {@link ScenarioResource}.
*
- * @param scenarioService The {@link ScenarioService} instance to use.
* @param identity The {@link SecurityIdentity} of the current user.
*/
- public ScenarioResource(ScenarioService scenarioService, SecurityIdentity identity) {
- this.scenarioService = scenarioService;
+ 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) {
- var scenario = scenarioService.findOne(identity.getPrincipal().getName(), projectId, 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 scenario;
+ return UserProtocol.toDto(scenario, auth);
}
/**
@@ -84,11 +106,22 @@ public final class ScenarioResource {
@Transactional
public org.opendc.web.proto.user.Scenario delete(
@PathParam("project") long projectId, @PathParam("scenario") int number) {
- var scenario = scenarioService.delete(identity.getPrincipal().getName(), projectId, number);
- if (scenario == null) {
+ // 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);
}
- return scenario;
+ 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
index 54afc1ce..2b66b64b 100644
--- 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
@@ -23,6 +23,7 @@
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;
@@ -36,7 +37,9 @@ 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.service.TopologyService;
+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.
@@ -46,11 +49,6 @@ import org.opendc.web.server.service.TopologyService;
@RolesAllowed("openid")
public final class TopologyResource {
/**
- * The service for managing the user topologies.
- */
- private final TopologyService topologyService;
-
- /**
* The identity of the current user.
*/
private final SecurityIdentity identity;
@@ -58,11 +56,9 @@ public final class TopologyResource {
/**
* Construct a {@link TopologyResource}.
*
- * @param topologyService The {@link TopologyService} instance to use.
* @param identity The {@link SecurityIdentity} of the current user.
*/
- public TopologyResource(TopologyService topologyService, SecurityIdentity identity) {
- this.topologyService = topologyService;
+ public TopologyResource(SecurityIdentity identity) {
this.identity = identity;
}
@@ -71,7 +67,17 @@ public final class TopologyResource {
*/
@GET
public List<org.opendc.web.proto.user.Topology> getAll(@PathParam("project") long projectId) {
- return topologyService.findAll(identity.getPrincipal().getName(), 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();
}
/**
@@ -82,13 +88,26 @@ public final class TopologyResource {
@Transactional
public org.opendc.web.proto.user.Topology create(
@PathParam("project") long projectId, @Valid org.opendc.web.proto.user.Topology.Create request) {
- var topology = topologyService.create(identity.getPrincipal().getName(), projectId, request);
+ // User must have access to project
+ ProjectAuthorization auth =
+ ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId);
- if (topology == null) {
+ if (auth == null) {
throw new WebApplicationException("Topology not found", 404);
+ } else if (!auth.canEdit()) {
+ throw new WebApplicationException("Not permitted to edit project", 403);
}
- return topology;
+ 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);
}
/**
@@ -98,13 +117,21 @@ public final class TopologyResource {
@Path("{topology}")
public org.opendc.web.proto.user.Topology get(
@PathParam("project") long projectId, @PathParam("topology") int number) {
- var topology = topologyService.findOne(identity.getPrincipal().getName(), projectId, 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 topology;
+ return UserProtocol.toDto(topology, auth);
}
/**
@@ -118,13 +145,26 @@ public final class TopologyResource {
@PathParam("project") long projectId,
@PathParam("topology") int number,
@Valid org.opendc.web.proto.user.Topology.Update request) {
- var topology = topologyService.update(identity.getPrincipal().getName(), projectId, number, request);
+ // User must have access to project
+ ProjectAuthorization auth =
+ ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId);
- if (topology == null) {
+ 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);
}
- return topology;
+ entity.updatedAt = Instant.now();
+ entity.rooms = request.getRooms();
+
+ return UserProtocol.toDto(entity, auth);
}
/**
@@ -135,12 +175,24 @@ public final class TopologyResource {
@Transactional
public org.opendc.web.proto.user.Topology delete(
@PathParam("project") long projectId, @PathParam("topology") int number) {
- var topology = topologyService.delete(identity.getPrincipal().getName(), projectId, number);
+ // User must have access to project
+ ProjectAuthorization auth =
+ ProjectAuthorization.findByUser(identity.getPrincipal().getName(), projectId);
- if (topology == null) {
+ 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);
}
- return topology;
+ 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/service/JobService.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/JobService.java
index 47f44d27..ed0eaf9c 100644
--- 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
@@ -23,122 +23,59 @@
package org.opendc.web.server.service;
import java.time.Instant;
-import java.util.List;
import java.util.Map;
import javax.enterprise.context.ApplicationScoped;
import org.opendc.web.proto.JobState;
import org.opendc.web.server.model.Job;
/**
- * Service for managing {@link Job}s.
+ * 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 service for managing the user accounting.
+ * 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} instance to use.
+ * @param accountingService The {@link UserAccountingService} for accounting the simulation time of users.
*/
public JobService(UserAccountingService accountingService) {
this.accountingService = accountingService;
}
/**
- * Query the pending simulation jobs.
- */
- public List<org.opendc.web.proto.runner.Job> listPending() {
- return Job.findByState(JobState.PENDING).stream()
- .map(JobService::toRunnerDto)
- .toList();
- }
-
- /**
- * Find a job by its identifier.
- */
- public org.opendc.web.proto.runner.Job findById(long id) {
- Job job = Job.findById(id);
-
- if (job == null) {
- return null;
- }
-
- return toRunnerDto(job);
- }
-
- /**
- * Atomically update the state of a {@link Job}.
+ * Update the job state.
*
- * @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.
+ * @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 org.opendc.web.proto.runner.Job updateState(
- long id, JobState newState, int runtime, Map<String, ?> results) {
- Job entity = Job.findById(id);
- if (entity == null) {
- return null;
- }
+ public void updateJob(Job job, JobState newState, int runtime, Map<String, ?> results) {
+ JobState state = job.state;
- JobState state = entity.state;
- if (!isTransitionLegal(state, newState)) {
+ 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 - entity.runtime);
+ int consumedBudget = Math.min(1, runtime - job.runtime);
// Check whether the user still has any simulation budget left
- if (accountingService.consumeSimulationBudget(entity.createdBy, consumedBudget)
- && nextState == JobState.RUNNING) {
+ if (accountingService.consumeSimulationBudget(job.createdBy, consumedBudget) && nextState == JobState.RUNNING) {
nextState = JobState.FAILED; // User has consumed all their budget; cancel the job
}
- if (!entity.updateAtomically(nextState, now, runtime, results)) {
+ if (!job.updateAtomically(nextState, now, runtime, results)) {
throw new IllegalStateException("Conflicting update");
}
-
- return toRunnerDto(entity);
- }
-
- /**
- * Determine whether the transition from [this] to [newState] is legal.
- */
- public static boolean isTransitionLegal(JobState currentState, JobState newState) {
- // Note that we always allow transitions from the state
- return newState == currentState
- || switch (currentState) {
- 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;
- };
- }
-
- /**
- * Convert a {@link Job} entity into a {@link org.opendc.web.proto.user.Job} DTO.
- */
- public static org.opendc.web.proto.user.Job toUserDto(Job job) {
- return new org.opendc.web.proto.user.Job(job.id, job.state, job.createdAt, job.updatedAt, job.results);
- }
-
- /**
- * Convert a {@link Job} into a runner-facing DTO.
- */
- public static org.opendc.web.proto.runner.Job toRunnerDto(Job job) {
- return new org.opendc.web.proto.runner.Job(
- job.id,
- ScenarioService.toRunnerDto(job.scenario),
- job.state,
- job.createdAt,
- job.updatedAt,
- job.runtime,
- job.results);
}
}
diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/PortfolioService.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/PortfolioService.java
deleted file mode 100644
index 94da5195..00000000
--- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/PortfolioService.java
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- * 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.List;
-import javax.enterprise.context.ApplicationScoped;
-import org.opendc.web.server.model.Portfolio;
-import org.opendc.web.server.model.ProjectAuthorization;
-
-/**
- * Service for managing {@link Portfolio}s.
- */
-@ApplicationScoped
-public final class PortfolioService {
- /**
- * List all {@link Portfolio}s that belong a certain project.
- */
- public List<org.opendc.web.proto.user.Portfolio> findByUser(String userId, long projectId) {
- // User must have access to project
- ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId);
-
- if (auth == null) {
- return List.of();
- }
-
- return Portfolio.findByProject(projectId).stream()
- .map((p) -> toUserDto(p, auth))
- .toList();
- }
-
- /**
- * Find a {@link Portfolio} with the specified <code>number</code> belonging to <code>projectId</code>.
- */
- public org.opendc.web.proto.user.Portfolio findByUser(String userId, long projectId, int number) {
- // User must have access to project
- ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId);
-
- if (auth == null) {
- return null;
- }
-
- Portfolio portfolio = Portfolio.findByProject(projectId, number);
-
- if (portfolio == null) {
- return null;
- }
-
- return toUserDto(portfolio, auth);
- }
-
- /**
- * Delete the portfolio with the specified <code>number</code> belonging to <code>projectId</code>.
- */
- public org.opendc.web.proto.user.Portfolio delete(String userId, long projectId, int number) {
- // User must have access to project
- ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId);
-
- if (auth == null) {
- return null;
- } else if (!auth.canEdit()) {
- throw new IllegalStateException("Not permitted to edit project");
- }
-
- Portfolio entity = Portfolio.findByProject(projectId, number);
- if (entity == null) {
- return null;
- }
-
- entity.delete();
- return toUserDto(entity, auth);
- }
-
- /**
- * Construct a new {@link Portfolio} with the specified name.
- */
- public org.opendc.web.proto.user.Portfolio create(
- String userId, long projectId, org.opendc.web.proto.user.Portfolio.Create request) {
- // User must have access to project
- ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId);
-
- if (auth == null) {
- return null;
- } else if (!auth.canEdit()) {
- throw new IllegalStateException("Not permitted to edit project");
- }
-
- 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 toUserDto(portfolio, auth);
- }
-
- /**
- * Convert a {@link Portfolio} entity into a {@link org.opendc.web.proto.user.Portfolio} DTO.
- */
- public static org.opendc.web.proto.user.Portfolio toUserDto(Portfolio portfolio, ProjectAuthorization auth) {
- return new org.opendc.web.proto.user.Portfolio(
- portfolio.id,
- portfolio.number,
- ProjectService.toUserDto(auth),
- portfolio.name,
- portfolio.targets,
- portfolio.scenarios.stream().map(ScenarioService::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 Portfolio} into a runner-facing DTO.
- */
- public static org.opendc.web.proto.runner.Portfolio toRunnerDto(Portfolio portfolio) {
- return new org.opendc.web.proto.runner.Portfolio(
- portfolio.id, portfolio.number, portfolio.name, portfolio.targets);
- }
-}
diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ProjectService.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ProjectService.java
deleted file mode 100644
index aeef664e..00000000
--- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ProjectService.java
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * 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.List;
-import javax.enterprise.context.ApplicationScoped;
-import org.opendc.web.proto.user.ProjectRole;
-import org.opendc.web.server.model.Project;
-import org.opendc.web.server.model.ProjectAuthorization;
-
-/**
- * Service for managing {@link Project}s.
- */
-@ApplicationScoped
-public final class ProjectService {
- /**
- * List all projects for the user with the specified <code>userId</code>.
- */
- public List<org.opendc.web.proto.user.Project> findByUser(String userId) {
- return ProjectAuthorization.findByUser(userId).stream()
- .map(ProjectService::toUserDto)
- .toList();
- }
-
- /**
- * Obtain the project with the specified <code>id</code> for the user with the specified <code>userId</code>.
- */
- public org.opendc.web.proto.user.Project findByUser(String userId, long id) {
- ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, id);
-
- if (auth == null) {
- return null;
- }
-
- return toUserDto(auth);
- }
-
- /**
- * Create a new {@link Project} for the user with the specified <code>userId</code>.
- */
- public org.opendc.web.proto.user.Project create(String userId, String name) {
- Instant now = Instant.now();
- Project entity = new Project(name, now);
- entity.persist();
-
- ProjectAuthorization authorization = new ProjectAuthorization(entity, userId, ProjectRole.OWNER);
-
- entity.authorizations.add(authorization);
- authorization.persist();
-
- return toUserDto(authorization);
- }
-
- /**
- * Delete a project by its identifier.
- *
- * @param userId The user that invokes the action.
- * @param id The identifier of the project.
- */
- public org.opendc.web.proto.user.Project delete(String userId, long id) {
- ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, id);
-
- if (auth == null) {
- return null;
- }
-
- if (!auth.canDelete()) {
- throw new IllegalArgumentException("Not allowed to delete project");
- }
-
- auth.project.updatedAt = Instant.now();
- org.opendc.web.proto.user.Project project = toUserDto(auth);
- auth.project.delete();
- return project;
- }
-
- /**
- * Convert a {@link ProjectAuthorization} entity into a {@link Project} DTO.
- */
- public static org.opendc.web.proto.user.Project toUserDto(ProjectAuthorization auth) {
- Project project = auth.project;
- return new org.opendc.web.proto.user.Project(
- project.id, project.name, project.createdAt, project.updatedAt, auth.role);
- }
-}
diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ScenarioService.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ScenarioService.java
deleted file mode 100644
index 6a70db1e..00000000
--- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ScenarioService.java
+++ /dev/null
@@ -1,231 +0,0 @@
-/*
- * 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.List;
-import javax.enterprise.context.ApplicationScoped;
-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;
-
-/**
- * Service for managing {@link Scenario}s.
- */
-@ApplicationScoped
-public final class ScenarioService {
- /**
- * The service for managing the user accounting.
- */
- private final UserAccountingService accountingService;
-
- /**
- * Construct a {@link ScenarioService} instance.
- *
- * @param accountingService The {@link UserAccountingService} instance to use.
- */
- public ScenarioService(UserAccountingService accountingService) {
- this.accountingService = accountingService;
- }
-
- /**
- * List all {@link Scenario}s that belong a certain portfolio.
- */
- public List<org.opendc.web.proto.user.Scenario> findAll(String userId, long projectId, int number) {
- // User must have access to project
- ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId);
-
- if (auth == null) {
- return List.of();
- }
-
- return Scenario.findByPortfolio(projectId, number).stream()
- .map((s) -> toUserDto(s, auth))
- .toList();
- }
-
- /**
- * Obtain a {@link Scenario} by identifier.
- */
- public org.opendc.web.proto.user.Scenario findOne(String userId, long projectId, int number) {
- // User must have access to project
- ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId);
-
- if (auth == null) {
- return null;
- }
-
- Scenario scenario = Scenario.findByProject(projectId, number);
-
- if (scenario == null) {
- return null;
- }
-
- return toUserDto(scenario, auth);
- }
-
- /**
- * Delete the specified scenario.
- */
- public org.opendc.web.proto.user.Scenario delete(String userId, long projectId, int number) {
- // User must have access to project
- ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId);
-
- if (auth == null) {
- return null;
- } else if (!auth.canEdit()) {
- throw new IllegalStateException("Not permitted to edit project");
- }
-
- Scenario entity = Scenario.findByProject(projectId, number);
- if (entity == null) {
- return null;
- }
-
- entity.delete();
- return toUserDto(entity, auth);
- }
-
- /**
- * Construct a new {@link Scenario} with the specified data.
- */
- public org.opendc.web.proto.user.Scenario create(
- String userId, long projectId, int portfolioNumber, org.opendc.web.proto.user.Scenario.Create request) {
- // User must have access to project
- ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId);
-
- if (auth == null) {
- return null;
- } else if (!auth.canEdit()) {
- throw new IllegalStateException("Not permitted to edit project");
- }
-
- Portfolio portfolio = Portfolio.findByProject(projectId, portfolioNumber);
-
- if (portfolio == null) {
- return null;
- }
-
- Topology topology = Topology.findByProject(projectId, (int) request.getTopology());
- if (topology == null) {
- throw new IllegalArgumentException("Referred topology does not exist");
- }
-
- Trace trace = Trace.findById(request.getWorkload().getTrace());
- if (trace == null) {
- throw new IllegalArgumentException("Referred trace does not exist");
- }
-
- 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());
- Job job = new Job(scenario, userId, now, portfolio.targets.getRepeats());
-
- // 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);
- scenario.persist();
-
- return toUserDto(scenario, auth);
- }
-
- /**
- * Convert a {@link Scenario} entity into a {@link org.opendc.web.proto.user.Scenario} DTO.
- */
- public static org.opendc.web.proto.user.Scenario toUserDto(Scenario scenario, ProjectAuthorization auth) {
- return new org.opendc.web.proto.user.Scenario(
- scenario.id,
- scenario.number,
- ProjectService.toUserDto(auth),
- PortfolioService.toSummaryDto(scenario.portfolio),
- scenario.name,
- toDto(scenario.workload),
- TopologyService.toSummaryDto(scenario.topology),
- scenario.phenomena,
- scenario.schedulerName,
- JobService.toUserDto(scenario.job));
- }
-
- /**
- * 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,
- toDto(scenario.workload),
- TopologyService.toSummaryDto(scenario.topology),
- scenario.phenomena,
- scenario.schedulerName,
- JobService.toUserDto(scenario.job));
- }
-
- /**
- * Convert a {@link Scenario} into a runner-facing DTO.
- */
- public static org.opendc.web.proto.runner.Scenario toRunnerDto(Scenario scenario) {
- return new org.opendc.web.proto.runner.Scenario(
- scenario.id,
- scenario.number,
- PortfolioService.toRunnerDto(scenario.portfolio),
- scenario.name,
- toDto(scenario.workload),
- TopologyService.toRunnerDto(scenario.topology),
- scenario.phenomena,
- scenario.schedulerName);
- }
-
- /**
- * 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);
- }
-
- /**
- * 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/java/org/opendc/web/server/service/TopologyService.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/TopologyService.java
deleted file mode 100644
index 1961995f..00000000
--- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/TopologyService.java
+++ /dev/null
@@ -1,178 +0,0 @@
-/*
- * 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.List;
-import javax.enterprise.context.ApplicationScoped;
-import org.opendc.web.server.model.Project;
-import org.opendc.web.server.model.ProjectAuthorization;
-import org.opendc.web.server.model.Topology;
-
-/**
- * Service for managing {@link Topology}s.
- */
-@ApplicationScoped
-public final class TopologyService {
- /**
- * List all {@link Topology}s that belong a certain project.
- */
- public List<org.opendc.web.proto.user.Topology> findAll(String userId, long projectId) {
- // User must have access to project
- ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId);
-
- if (auth == null) {
- return List.of();
- }
-
- return Topology.findByProject(projectId).stream()
- .map((t) -> toUserDto(t, auth))
- .toList();
- }
-
- /**
- * Find the {@link Topology} with the specified <code>number</code> belonging to <code>projectId</code>.
- */
- public org.opendc.web.proto.user.Topology findOne(String userId, long projectId, int number) {
- // User must have access to project
- ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId);
-
- if (auth == null) {
- return null;
- }
-
- Topology topology = Topology.findByProject(projectId, number);
-
- if (topology == null) {
- return null;
- }
-
- return toUserDto(topology, auth);
- }
-
- /**
- * Delete the {@link Topology} with the specified <code>number</code> belonging to <code>projectId</code>
- */
- public org.opendc.web.proto.user.Topology delete(String userId, long projectId, int number) {
- // User must have access to project
- ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId);
-
- if (auth == null) {
- return null;
- } else if (!auth.canEdit()) {
- throw new IllegalStateException("Not permitted to edit project");
- }
-
- Topology entity = Topology.findByProject(projectId, number);
-
- if (entity == null) {
- return null;
- }
-
- entity.updatedAt = Instant.now();
- entity.delete();
- return toUserDto(entity, auth);
- }
-
- /**
- * Update a {@link Topology} with the specified <code>number</code> belonging to <code>projectId</code>.
- */
- public org.opendc.web.proto.user.Topology update(
- String userId, long projectId, int number, org.opendc.web.proto.user.Topology.Update request) {
- // User must have access to project
- ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId);
-
- if (auth == null) {
- return null;
- } else if (!auth.canEdit()) {
- throw new IllegalStateException("Not permitted to edit project");
- }
-
- Topology entity = Topology.findByProject(projectId, number);
-
- if (entity == null) {
- return null;
- }
-
- entity.updatedAt = Instant.now();
- entity.rooms = request.getRooms();
-
- return toUserDto(entity, auth);
- }
-
- /**
- * Construct a new {@link Topology} with the specified name.
- */
- public org.opendc.web.proto.user.Topology create(
- String userId, long projectId, org.opendc.web.proto.user.Topology.Create request) {
- // User must have access to project
- ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId);
-
- if (auth == null) {
- return null;
- } else if (!auth.canEdit()) {
- throw new IllegalStateException("Not permitted to edit project");
- }
-
- 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 toUserDto(topology, auth);
- }
-
- /**
- * Convert a {@link Topology} entity into a {@link org.opendc.web.proto.user.Topology} DTO.
- */
- public static org.opendc.web.proto.user.Topology toUserDto(Topology topology, ProjectAuthorization auth) {
- return new org.opendc.web.proto.user.Topology(
- topology.id,
- topology.number,
- ProjectService.toUserDto(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 Topology} into a runner-facing DTO.
- */
- public static org.opendc.web.proto.runner.Topology toRunnerDto(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/util/runner/QuarkusJobManager.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/runner/QuarkusJobManager.java
index 84ebd6e4..0331eacf 100644
--- 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
@@ -28,17 +28,26 @@ import javax.transaction.Transactional;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.opendc.web.proto.JobState;
-import org.opendc.web.proto.runner.Job;
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 {@link JobService} without overhead of the REST API.
+ * 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;
}
@@ -46,38 +55,60 @@ public class QuarkusJobManager implements JobManager {
@Transactional
@Nullable
@Override
- public Job findNext() {
- var pending = jobService.listPending();
- if (pending.isEmpty()) {
+ public org.opendc.web.proto.runner.Job findNext() {
+ var job = Job.findByState(JobState.PENDING).firstResult();
+ if (job == null) {
return null;
}
- return pending.get(0);
+ return RunnerProtocol.toDto(job);
}
+ @Transactional
@Override
public boolean claim(long id) {
- try {
- jobService.updateState(id, JobState.CLAIMED, 0, null);
- return true;
- } catch (IllegalStateException e) {
- return false;
- }
+ return updateState(id, JobState.CLAIMED, 0, null);
}
+ @Transactional
@Override
public boolean heartbeat(long id, int runtime) {
- Job res = jobService.updateState(id, JobState.RUNNING, runtime, null);
- return res != null && !res.getState().equals(JobState.FAILED);
+ return updateState(id, JobState.RUNNING, runtime, null);
}
+ @Transactional
@Override
public void fail(long id, int runtime) {
- jobService.updateState(id, JobState.FAILED, runtime, null);
+ updateState(id, JobState.FAILED, runtime, null);
}
+ @Transactional
@Override
public void finish(long id, int runtime, @NotNull Map<String, ?> results) {
- jobService.updateState(id, JobState.FINISHED, runtime, 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/resources/application-test.properties b/opendc-web/opendc-web-server/src/main/resources/application-test.properties
index 17502b6c..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
@@ -25,6 +25,7 @@ quarkus.datasource.jdbc.url=jdbc:h2:mem:default;DB_CLOSE_DELAY=-1;INIT=CREATE TY
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/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
index ebef3945..5c5976db 100644
--- 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
@@ -25,16 +25,10 @@ package org.opendc.web.server.rest;
import static io.restassured.RestAssured.when;
import static org.hamcrest.Matchers.equalTo;
-import io.quarkus.panache.mock.PanacheMock;
import io.quarkus.test.common.http.TestHTTPEndpoint;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.http.ContentType;
-import java.util.stream.Stream;
-import org.hamcrest.Matchers;
-import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import org.mockito.Mockito;
-import org.opendc.web.server.model.Trace;
/**
* Test suite for {@link TraceResource}.
@@ -43,21 +37,11 @@ import org.opendc.web.server.model.Trace;
@TestHTTPEndpoint(TraceResource.class)
public final class TraceResourceTest {
/**
- * Set up the test environment.
- */
- @BeforeEach
- public void setUp() {
- PanacheMock.mock(Trace.class);
- }
-
- /**
- * Test that tries to obtain all traces (empty response).
+ * Test that tries to obtain all traces.
*/
@Test
public void testGetAllEmpty() {
- Mockito.when(Trace.streamAll()).thenReturn(Stream.of());
-
- when().get().then().statusCode(200).contentType(ContentType.JSON).body("", Matchers.empty());
+ when().get().then().statusCode(200).contentType(ContentType.JSON);
}
/**
@@ -65,9 +49,7 @@ public final class TraceResourceTest {
*/
@Test
public void testGetNonExisting() {
- Mockito.when(Trace.findById("bitbrains")).thenReturn(null);
-
- when().get("/bitbrains").then().statusCode(404).contentType(ContentType.JSON);
+ when().get("/unknown").then().statusCode(404).contentType(ContentType.JSON);
}
/**
@@ -75,12 +57,10 @@ public final class TraceResourceTest {
*/
@Test
public void testGetExisting() {
- Mockito.when(Trace.findById("bitbrains")).thenReturn(new Trace("bitbrains", "Bitbrains", "VM"));
-
- when().get("/bitbrains")
+ when().get("/bitbrains-small")
.then()
.statusCode(200)
.contentType(ContentType.JSON)
- .body("name", equalTo("Bitbrains"));
+ .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
index a163cd29..94b2cef0 100644
--- 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
@@ -25,30 +25,13 @@ 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 static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.eq;
import io.quarkus.test.common.http.TestHTTPEndpoint;
import io.quarkus.test.junit.QuarkusTest;
-import io.quarkus.test.junit.mockito.InjectMock;
import io.quarkus.test.security.TestSecurity;
import io.restassured.http.ContentType;
-import java.time.Instant;
-import java.util.List;
-import java.util.Set;
import org.junit.jupiter.api.Test;
-import org.mockito.Mockito;
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;
/**
* Test suite for {@link JobResource}.
@@ -56,27 +39,6 @@ import org.opendc.web.server.service.JobService;
@QuarkusTest
@TestHTTPEndpoint(JobResource.class)
public final class JobResourceTest {
- @InjectMock
- private JobService jobService;
-
- /**
- * Dummy values
- */
- private final Portfolio dummyPortfolio = new Portfolio(1, 1, "test", new Targets(Set.of(), 1));
-
- private final Topology dummyTopology = new Topology(1, 1, "test", List.of(), Instant.now(), Instant.now());
- private final Trace dummyTrace = new Trace("bitbrains", "Bitbrains", "vm");
- private final Scenario dummyScenario = new Scenario(
- 1,
- 1,
- dummyPortfolio,
- "test",
- new Workload(dummyTrace, 1.0),
- dummyTopology,
- new OperationalPhenomena(false, false),
- "test");
- private final Job dummyJob = new Job(1, dummyScenario, JobState.PENDING, Instant.now(), Instant.now(), 0, null);
-
/**
* Test that tries to query the pending jobs without token.
*/
@@ -90,7 +52,7 @@ public final class JobResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "test",
roles = {"openid"})
public void testQueryInvalidScope() {
when().get().then().statusCode(403);
@@ -101,12 +63,10 @@ public final class JobResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "test",
roles = {"runner"})
public void testQuery() {
- Mockito.when(jobService.listPending()).thenReturn(List.of(dummyJob));
-
- when().get().then().statusCode(200).contentType(ContentType.JSON).body("get(0).id", equalTo(1));
+ when().get().then().statusCode(200).contentType(ContentType.JSON).body("get(0).state", equalTo("PENDING"));
}
/**
@@ -114,12 +74,10 @@ public final class JobResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "test",
roles = {"runner"})
public void testGetNonExisting() {
- Mockito.when(jobService.findById(1)).thenReturn(null);
-
- when().get("/1").then().statusCode(404).contentType(ContentType.JSON);
+ when().get("/0").then().statusCode(404).contentType(ContentType.JSON);
}
/**
@@ -127,11 +85,9 @@ public final class JobResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "test",
roles = {"runner"})
public void testGetExisting() {
- Mockito.when(jobService.findById(1)).thenReturn(dummyJob);
-
when().get("/1").then().statusCode(200).contentType(ContentType.JSON).body("id", equalTo(1));
}
@@ -140,15 +96,13 @@ public final class JobResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "test",
roles = {"runner"})
public void testUpdateNonExistent() {
- Mockito.when(jobService.updateState(eq(1L), any(), anyInt(), any())).thenReturn(null);
-
- given().body(new Job.Update(JobState.PENDING, 0, null))
+ given().body(new org.opendc.web.proto.runner.Job.Update(JobState.PENDING, 0, null))
.contentType(ContentType.JSON)
.when()
- .post("/1")
+ .post("/0")
.then()
.statusCode(404)
.contentType(ContentType.JSON);
@@ -159,16 +113,13 @@ public final class JobResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "test",
roles = {"runner"})
public void testUpdateState() {
- Mockito.when(jobService.updateState(eq(1L), any(), anyInt(), any()))
- .thenReturn(new Job(1, dummyScenario, JobState.CLAIMED, Instant.now(), Instant.now(), 0, null));
-
- given().body(new Job.Update(JobState.CLAIMED, 0, null))
+ given().body(new org.opendc.web.proto.runner.Job.Update(JobState.CLAIMED, 0, null))
.contentType(ContentType.JSON)
.when()
- .post("/1")
+ .post("/2")
.then()
.statusCode(200)
.contentType(ContentType.JSON)
@@ -180,7 +131,7 @@ public final class JobResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "test",
roles = {"runner"})
public void testUpdateInvalidInput() {
given().body("{ \"test\": \"test\" }")
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
index cc3ac978..a952d83f 100644
--- 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
@@ -24,24 +24,14 @@ package org.opendc.web.server.rest.user;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.equalTo;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
import io.quarkus.test.common.http.TestHTTPEndpoint;
import io.quarkus.test.junit.QuarkusTest;
-import io.quarkus.test.junit.mockito.InjectMock;
import io.quarkus.test.security.TestSecurity;
import io.restassured.http.ContentType;
-import java.time.Instant;
-import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.Test;
-import org.mockito.Mockito;
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;
/**
* Test suite for {@link PortfolioResource}.
@@ -49,27 +39,25 @@ import org.opendc.web.server.service.PortfolioService;
@QuarkusTest
@TestHTTPEndpoint(PortfolioResource.class)
public final class PortfolioResourceTest {
- @InjectMock
- private PortfolioService portfolioService;
-
- /**
- * Dummy project and portfolio
- */
- private final Project dummyProject = new Project(1, "test", Instant.now(), Instant.now(), ProjectRole.OWNER);
-
- private final Portfolio dummyPortfolio =
- new Portfolio(1, 1, dummyProject, "test", new Targets(Set.of(), 1), List.of());
-
/**
* Test that tries to obtain the list of portfolios belonging to a project.
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "owner",
roles = {"openid"})
public void testGetForProject() {
- Mockito.when(portfolioService.findByUser("testUser", 1)).thenReturn(List.of());
+ 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);
}
@@ -78,40 +66,53 @@ public final class PortfolioResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "owner",
roles = {"openid"})
public void testCreateNonExistent() {
- Mockito.when(portfolioService.create(eq("testUser"), eq(1), any())).thenReturn(null);
+ 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 Portfolio.Create("test", new Targets(Set.of(), 1)))
+ .body(new org.opendc.web.proto.user.Portfolio.Create("test", new Targets(Set.of(), 1)))
.contentType(ContentType.JSON)
.when()
.post()
.then()
- .statusCode(404)
+ .statusCode(403)
.contentType(ContentType.JSON);
}
/**
- * Test that tries to create a portfolio for a scenario.
+ * Test that tries to create a portfolio for a project.
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "editor",
roles = {"openid"})
public void testCreate() {
- Mockito.when(portfolioService.create(eq("testUser"), eq(1L), any())).thenReturn(dummyPortfolio);
-
given().pathParam("project", "1")
- .body(new Portfolio.Create("test", new Targets(Set.of(), 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("id", equalTo(1))
.body("name", equalTo("test"));
}
@@ -120,7 +121,7 @@ public final class PortfolioResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "editor",
roles = {"openid"})
public void testCreateEmpty() {
given().pathParam("project", "1")
@@ -138,11 +139,11 @@ public final class PortfolioResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "editor",
roles = {"openid"})
public void testCreateBlankName() {
given().pathParam("project", "1")
- .body(new Portfolio.Create("", new Targets(Set.of(), 1)))
+ .body(new org.opendc.web.proto.user.Portfolio.Create("", new Targets(Set.of(), 1)))
.contentType(ContentType.JSON)
.when()
.post()
@@ -164,7 +165,7 @@ public final class PortfolioResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "owner",
roles = {"runner"})
public void testGetInvalidToken() {
given().pathParam("project", "1").when().get("/1").then().statusCode(403);
@@ -175,13 +176,27 @@ public final class PortfolioResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "owner",
roles = {"openid"})
public void testGetNonExisting() {
- Mockito.when(portfolioService.findByUser("testUser", 1, 1)).thenReturn(null);
-
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)
@@ -193,11 +208,9 @@ public final class PortfolioResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "owner",
roles = {"openid"})
public void testGetExisting() {
- Mockito.when(portfolioService.findByUser("testUser", 1, 1)).thenReturn(dummyPortfolio);
-
given().pathParam("project", "1")
.when()
.get("/1")
@@ -212,12 +225,21 @@ public final class PortfolioResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "owner",
roles = {"openid"})
public void testDeleteNonExistent() {
- Mockito.when(portfolioService.delete("testUser", 1, 1)).thenReturn(null);
+ given().pathParam("project", "1").when().delete("/0").then().statusCode(404);
+ }
- given().pathParam("project", "1").when().delete("/1").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);
}
/**
@@ -225,16 +247,41 @@ public final class PortfolioResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "owner",
roles = {"openid"})
public void testDelete() {
- Mockito.when(portfolioService.delete("testUser", 1, 1)).thenReturn(dummyPortfolio);
+ 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("/1")
+ .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
index 8cb95a98..4f8d412c 100644
--- 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
@@ -24,32 +24,15 @@ package org.opendc.web.server.rest.user;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.equalTo;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.eq;
import io.quarkus.test.common.http.TestHTTPEndpoint;
import io.quarkus.test.junit.QuarkusTest;
-import io.quarkus.test.junit.mockito.InjectMock;
import io.quarkus.test.security.TestSecurity;
import io.restassured.http.ContentType;
-import java.time.Instant;
-import java.util.List;
-import java.util.Set;
import org.junit.jupiter.api.Test;
-import org.mockito.Mockito;
-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;
/**
* Test suite for {@link PortfolioScenarioResource}.
@@ -57,30 +40,6 @@ import org.opendc.web.server.service.ScenarioService;
@QuarkusTest
@TestHTTPEndpoint(PortfolioScenarioResource.class)
public final class PortfolioScenarioResourceTest {
- @InjectMock
- private ScenarioService scenarioService;
-
- /**
- * Dummy values
- */
- private final Project dummyProject = new Project(0, "test", Instant.now(), Instant.now(), ProjectRole.OWNER);
-
- private final Portfolio.Summary dummyPortfolio = new Portfolio.Summary(1, 1, "test", new Targets(Set.of(), 1));
- private final Job dummyJob = new Job(1, JobState.PENDING, Instant.now(), Instant.now(), null);
- private final Trace dummyTrace = new Trace("bitbrains", "Bitbrains", "vm");
- private final Topology.Summary dummyTopology = new Topology.Summary(1, 1, "test", Instant.now(), Instant.now());
- private final Scenario dummyScenario = new Scenario(
- 1,
- 1,
- dummyProject,
- dummyPortfolio,
- "test",
- new Workload(dummyTrace, 1.0),
- dummyTopology,
- new OperationalPhenomena(false, false),
- "test",
- dummyJob);
-
/**
* Test that tries to obtain a portfolio without token.
*/
@@ -99,7 +58,7 @@ public final class PortfolioScenarioResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "owner",
roles = {"runner"})
public void testGetInvalidToken() {
given().pathParam("project", "1")
@@ -111,15 +70,30 @@ public final class PortfolioScenarioResourceTest {
}
/**
- * Test that tries to obtain a non-existent portfolio.
+ * Test that tries to obtain a scenario without authorization.
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "unknown",
roles = {"openid"})
- public void testGet() {
- Mockito.when(scenarioService.findAll("testUser", 1, 1)).thenReturn(List.of());
+ 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()
@@ -134,14 +108,31 @@ public final class PortfolioScenarioResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "owner",
roles = {"openid"})
public void testCreateNonExistent() {
- Mockito.when(scenarioService.create(eq("testUser"), eq(1L), anyInt(), any()))
- .thenReturn(null);
+ 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", "1")
+ .pathParam("portfolio", "0")
.body(new Scenario.Create(
"test", new Workload.Spec("test", 1.0), 1, new OperationalPhenomena(false, false), "test"))
.contentType(ContentType.JSON)
@@ -153,27 +144,47 @@ public final class PortfolioScenarioResourceTest {
}
/**
+ * 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 = "testUser",
+ user = "owner",
roles = {"openid"})
public void testCreate() {
- Mockito.when(scenarioService.create(eq("testUser"), eq(1L), eq(1), any()))
- .thenReturn(dummyScenario);
-
given().pathParam("project", "1")
.pathParam("portfolio", "1")
.body(new Scenario.Create(
- "test", new Workload.Spec("test", 1.0), 1, new OperationalPhenomena(false, false), "test"))
+ "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("id", equalTo(1))
.body("name", equalTo("test"));
}
@@ -182,7 +193,7 @@ public final class PortfolioScenarioResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "owner",
roles = {"openid"})
public void testCreateEmpty() {
given().pathParam("project", "1")
@@ -201,7 +212,7 @@ public final class PortfolioScenarioResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "owner",
roles = {"openid"})
public void testCreateBlankName() {
given().pathParam("project", "1")
@@ -215,4 +226,48 @@ public final class PortfolioScenarioResourceTest {
.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
index 7ca314a6..8bd60808 100644
--- 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
@@ -28,16 +28,9 @@ import static org.hamcrest.Matchers.equalTo;
import io.quarkus.test.common.http.TestHTTPEndpoint;
import io.quarkus.test.junit.QuarkusTest;
-import io.quarkus.test.junit.mockito.InjectMock;
import io.quarkus.test.security.TestSecurity;
import io.restassured.http.ContentType;
-import java.time.Instant;
-import java.util.List;
import org.junit.jupiter.api.Test;
-import org.mockito.Mockito;
-import org.opendc.web.proto.user.Project;
-import org.opendc.web.proto.user.ProjectRole;
-import org.opendc.web.server.service.ProjectService;
/**
* Test suite for [ProjectResource].
@@ -45,14 +38,6 @@ import org.opendc.web.server.service.ProjectService;
@QuarkusTest
@TestHTTPEndpoint(ProjectResource.class)
public final class ProjectResourceTest {
- @InjectMock
- private ProjectService projectService;
-
- /**
- * Dummy values.
- */
- private final Project dummyProject = new Project(0, "test", Instant.now(), Instant.now(), ProjectRole.OWNER);
-
/**
* Test that tries to obtain all projects without token.
*/
@@ -66,7 +51,7 @@ public final class ProjectResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "owner",
roles = {"runner"})
public void testGetAllWithInvalidScope() {
when().get().then().statusCode(403);
@@ -77,12 +62,10 @@ public final class ProjectResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "owner",
roles = {"openid"})
public void testGetAll() {
- Mockito.when(projectService.findByUser("testUser")).thenReturn(List.of(dummyProject));
-
- when().get().then().statusCode(200).contentType(ContentType.JSON).body("get(0).name", equalTo("test"));
+ when().get().then().statusCode(200).contentType(ContentType.JSON).body("get(0).name", equalTo("Test Project"));
}
/**
@@ -90,25 +73,21 @@ public final class ProjectResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "owner",
roles = {"openid"})
public void testGetNonExisting() {
- Mockito.when(projectService.findByUser("testUser", 1)).thenReturn(null);
-
- when().get("/1").then().statusCode(404).contentType(ContentType.JSON);
+ when().get("/0").then().statusCode(404).contentType(ContentType.JSON);
}
/**
- * Test that tries to obtain a job.
+ * Test that tries to obtain a project.
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "owner",
roles = {"openid"})
public void testGetExisting() {
- Mockito.when(projectService.findByUser("testUser", 1)).thenReturn(dummyProject);
-
- when().get("/1").then().statusCode(200).contentType(ContentType.JSON).body("id", equalTo(0));
+ when().get("/1").then().statusCode(200).contentType(ContentType.JSON).body("id", equalTo(1));
}
/**
@@ -116,19 +95,16 @@ public final class ProjectResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "owner",
roles = {"openid"})
public void testCreate() {
- Mockito.when(projectService.create("testUser", "test")).thenReturn(dummyProject);
-
- given().body(new Project.Create("test"))
+ given().body(new org.opendc.web.proto.user.Project.Create("test"))
.contentType(ContentType.JSON)
.when()
.post()
.then()
.statusCode(200)
.contentType(ContentType.JSON)
- .body("id", equalTo(0))
.body("name", equalTo("test"));
}
@@ -137,7 +113,7 @@ public final class ProjectResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "owner",
roles = {"openid"})
public void testCreateEmpty() {
given().body("{}")
@@ -154,10 +130,10 @@ public final class ProjectResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "owner",
roles = {"openid"})
public void testCreateBlankName() {
- given().body(new Project.Create(""))
+ given().body(new org.opendc.web.proto.user.Project.Create(""))
.contentType(ContentType.JSON)
.when()
.post()
@@ -171,12 +147,10 @@ public final class ProjectResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "owner",
roles = {"openid"})
public void testDeleteNonExistent() {
- Mockito.when(projectService.delete("testUser", 1)).thenReturn(null);
-
- when().delete("/1").then().statusCode(404).contentType(ContentType.JSON);
+ when().delete("/0").then().statusCode(404).contentType(ContentType.JSON);
}
/**
@@ -184,12 +158,20 @@ public final class ProjectResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "owner",
roles = {"openid"})
public void testDelete() {
- Mockito.when(projectService.delete("testUser", 1)).thenReturn(dummyProject);
+ 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("/1").then().statusCode(200).contentType(ContentType.JSON);
+ when().delete("/" + id).then().statusCode(200).contentType(ContentType.JSON);
}
/**
@@ -197,12 +179,9 @@ public final class ProjectResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "viewer",
roles = {"openid"})
public void testDeleteNonOwner() {
- Mockito.when(projectService.delete("testUser", 1))
- .thenThrow(new 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/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
index 850236d6..a980e4e2 100644
--- 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
@@ -27,55 +27,42 @@ import static org.hamcrest.Matchers.equalTo;
import io.quarkus.test.common.http.TestHTTPEndpoint;
import io.quarkus.test.junit.QuarkusTest;
-import io.quarkus.test.junit.mockito.InjectMock;
import io.quarkus.test.security.TestSecurity;
+import io.restassured.builder.RequestSpecBuilder;
import io.restassured.http.ContentType;
-import java.time.Instant;
-import java.util.Set;
+import io.restassured.specification.RequestSpecification;
import org.junit.jupiter.api.Test;
-import org.mockito.Mockito;
-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;
/**
- * Test suite for [ScenarioResource].
+ * Test suite for {@link ScenarioResource}.
*/
@QuarkusTest
@TestHTTPEndpoint(ScenarioResource.class)
public final class ScenarioResourceTest {
- @InjectMock
- private ScenarioService scenarioService;
+ /**
+ * 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);
+ }
/**
- * Dummy values
+ * Test that tries to obtain all scenarios belonging to a project.
*/
- private final Project dummyProject = new Project(0, "test", Instant.now(), Instant.now(), ProjectRole.OWNER);
-
- private final Portfolio.Summary dummyPortfolio = new Portfolio.Summary(1, 1, "test", new Targets(Set.of(), 1));
- private final Job dummyJob = new Job(1, JobState.PENDING, Instant.now(), Instant.now(), null);
- private final Trace dummyTrace = new Trace("bitbrains", "Bitbrains", "vm");
- private final Topology.Summary dummyTopology = new Topology.Summary(1, 1, "test", Instant.now(), Instant.now());
- private final Scenario dummyScenario = new Scenario(
- 1,
- 1,
- dummyProject,
- dummyPortfolio,
- "test",
- new Workload(dummyTrace, 1.0),
- dummyTopology,
- new OperationalPhenomena(false, false),
- "test",
- dummyJob);
+ @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.
@@ -90,7 +77,7 @@ public final class ScenarioResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "owner",
roles = {"runner"})
public void testGetInvalidToken() {
given().pathParam("project", "1").when().get("/1").then().statusCode(403);
@@ -101,11 +88,25 @@ public final class ScenarioResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "owner",
roles = {"openid"})
public void testGetNonExisting() {
- Mockito.when(scenarioService.findOne("testUser", 1, 1)).thenReturn(null);
+ 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")
@@ -119,11 +120,9 @@ public final class ScenarioResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "owner",
roles = {"openid"})
public void testGetExisting() {
- Mockito.when(scenarioService.findOne("testUser", 1, 1)).thenReturn(dummyScenario);
-
given().pathParam("project", "1")
.when()
.get("/1")
@@ -138,27 +137,65 @@ public final class ScenarioResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "owner",
roles = {"openid"})
public void testDeleteNonExistent() {
- Mockito.when(scenarioService.delete("testUser", 1, 1)).thenReturn(null);
+ 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 = "testUser",
+ user = "owner",
roles = {"openid"})
public void testDelete() {
- Mockito.when(scenarioService.delete("testUser", 1, 1)).thenReturn(dummyScenario);
+ 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("/1")
+ .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
index 2cc6ea4b..21e35b09 100644
--- 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
@@ -24,24 +24,14 @@ package org.opendc.web.server.rest.user;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.equalTo;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.ArgumentMatchers.eq;
import io.quarkus.test.common.http.TestHTTPEndpoint;
import io.quarkus.test.junit.QuarkusTest;
-import io.quarkus.test.junit.mockito.InjectMock;
import io.quarkus.test.security.TestSecurity;
import io.restassured.http.ContentType;
-import java.time.Instant;
import java.util.List;
import org.junit.jupiter.api.Test;
-import org.mockito.Mockito;
-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;
/**
* Test suite for {@link TopologyResource}.
@@ -49,27 +39,31 @@ import org.opendc.web.server.service.TopologyService;
@QuarkusTest
@TestHTTPEndpoint(TopologyResource.class)
public final class TopologyResourceTest {
- @InjectMock
- private TopologyService topologyService;
-
/**
- * Dummy project and topology.
+ * Test that tries to obtain the list of topologies of a project without proper authorization.
*/
- private final Project dummyProject = new Project(1, "test", Instant.now(), Instant.now(), ProjectRole.OWNER);
-
- private final Topology dummyTopology =
- new Topology(1, 1, dummyProject, "test", List.of(), Instant.now(), Instant.now());
+ @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 = "testUser",
+ user = "owner",
roles = {"openid"})
- public void testGetForProject() {
- Mockito.when(topologyService.findAll("testUser", 1)).thenReturn(List.of());
-
+ public void testGetAll() {
given().pathParam("project", "1").when().get().then().statusCode(200).contentType(ContentType.JSON);
}
@@ -78,18 +72,34 @@ public final class TopologyResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "owner",
roles = {"openid"})
public void testCreateNonExistent() {
- Mockito.when(topologyService.create(eq("testUser"), eq(1L), any())).thenReturn(null);
+ 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(404)
+ .statusCode(403)
.contentType(ContentType.JSON);
}
@@ -98,11 +108,9 @@ public final class TopologyResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "owner",
roles = {"openid"})
public void testCreate() {
- Mockito.when(topologyService.create(eq("testUser"), eq(1L), any())).thenReturn(dummyTopology);
-
given().pathParam("project", "1")
.body(new Topology.Create("test", List.of()))
.contentType(ContentType.JSON)
@@ -111,7 +119,6 @@ public final class TopologyResourceTest {
.then()
.statusCode(200)
.contentType(ContentType.JSON)
- .body("id", equalTo(1))
.body("name", equalTo("test"));
}
@@ -120,7 +127,7 @@ public final class TopologyResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "owner",
roles = {"openid"})
public void testCreateEmpty() {
given().pathParam("project", "1")
@@ -138,7 +145,7 @@ public final class TopologyResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "owner",
roles = {"openid"})
public void testCreateBlankName() {
given().pathParam("project", "1")
@@ -164,7 +171,7 @@ public final class TopologyResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "owner",
roles = {"runner"})
public void testGetInvalidToken() {
given().pathParam("project", "1").when().get("/1").then().statusCode(403);
@@ -175,11 +182,25 @@ public final class TopologyResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "owner",
roles = {"openid"})
public void testGetNonExisting() {
- Mockito.when(topologyService.findOne("testUser", 1, 1)).thenReturn(null);
+ 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")
@@ -193,11 +214,9 @@ public final class TopologyResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "owner",
roles = {"openid"})
public void testGetExisting() {
- Mockito.when(topologyService.findOne("testUser", 1, 1)).thenReturn(dummyTopology);
-
given().pathParam("project", "1")
.when()
.get("/1")
@@ -212,12 +231,26 @@ public final class TopologyResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "owner",
roles = {"openid"})
public void testUpdateNonExistent() {
- Mockito.when(topologyService.update(eq("testUser"), anyLong(), anyInt(), any()))
- .thenReturn(null);
+ 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)
@@ -228,16 +261,31 @@ public final class TopologyResourceTest {
}
/**
+ * 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 = "testUser",
+ user = "owner",
roles = {"openid"})
public void testUpdate() {
- Mockito.when(topologyService.update(eq("testUser"), anyLong(), anyInt(), any()))
- .thenReturn(dummyTopology);
-
given().pathParam("project", "1")
.body(new Topology.Update(List.of()))
.contentType(ContentType.JSON)
@@ -253,27 +301,56 @@ public final class TopologyResourceTest {
*/
@Test
@TestSecurity(
- user = "testUser",
+ user = "owner",
roles = {"openid"})
public void testDeleteNonExistent() {
- Mockito.when(topologyService.delete("testUser", 1, 1)).thenReturn(null);
+ 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 = "testUser",
+ user = "owner",
roles = {"openid"})
public void testDelete() {
- Mockito.when(topologyService.delete("testUser", 1, 1)).thenReturn(dummyTopology);
+ 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("/1")
+ .delete("/" + number)
.then()
.statusCode(200)
.contentType(ContentType.JSON);
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-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({