diff options
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({ |
