diff options
Diffstat (limited to 'controller-server/src')
6 files changed, 184 insertions, 2 deletions
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java index eb86f0c2919..56b7ee44a66 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java @@ -354,6 +354,7 @@ public class ApplicationController { // Update application with information from application package if ( ! preferOldestVersion && ! application.get().deploymentJobs().deployedInternally()) + // TODO jvenstad: Store only on submissions (not on deployments to dev!!) application = storeWithUpdatedConfig(application, applicationPackage); // Assign global rotation diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Badges.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Badges.java new file mode 100644 index 00000000000..8737220efc4 --- /dev/null +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Badges.java @@ -0,0 +1,63 @@ +package com.yahoo.vespa.hosted.controller.deployment; + +import com.yahoo.config.provision.ApplicationId; + +import java.net.URI; +import java.util.List; + +/** + * URLs for deployment job badges using <a href="https://github.com/yahoo/badge-up">badge-up</a>. + * + * @author jonmv + */ +class Badges { + + static final String dark = "555555", + blue = "4477DD", + red = "DD4444", + purple = "AA11CC", + yellow = "DDAA11", + white = "FFFFFF"; + + private final URI badgeApi; + + Badges(URI badgeApi) { + this.badgeApi = badgeApi; + } + + /** Returns a URI which gives a history badge for the given runs. */ + URI historic(ApplicationId id, List<Run> runs) { + StringBuilder path = new StringBuilder(id + ";" + dark); + + if ( ! runs.isEmpty()) { + Run lastCompleted = runs.get(runs.size() - 1); + if (runs.size() > 1 && !lastCompleted.hasEnded()) + lastCompleted = runs.get(runs.size() - 2); + + path.append("/").append(lastCompleted.id().type().jobName()).append(";").append(colorOf(lastCompleted)); + for (Run run : runs) + path.append("/%20;").append(colorOf(run)).append(";s%7B").append(white).append("%7D"); + } + + return badgeApi.resolve(path.toString()); + } + + /** Returns a URI which gives an overview badge for the given runs. */ + URI overview(ApplicationId id, List<Run> runs) { + StringBuilder path = new StringBuilder(id + ";" + dark); + for (Run run : runs) + path.append("/").append(run.id().type().jobName()).append(";").append(colorOf(run)); + + return badgeApi.resolve(path.toString()); + } + + private static String colorOf(Run run) { + switch (run.status()) { + case success: return blue; + case running: return purple; + case aborted: return yellow; + default: return red; + } + } + +} diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java index 9659022e258..1a2d7de8185 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java @@ -44,6 +44,7 @@ import java.util.stream.Stream; import static com.google.common.collect.ImmutableList.copyOf; import static com.yahoo.vespa.hosted.controller.deployment.Step.deactivateTester; import static com.yahoo.vespa.hosted.controller.deployment.Step.endTests; +import static java.util.stream.Collectors.toList; /** * A singleton owned by the controller, which contains the state and methods for controlling deployment jobs. @@ -66,12 +67,14 @@ public class JobController { private final CuratorDb curator; private final BufferedLogStore logs; private final TesterCloud cloud; + private final Badges badges; public JobController(Controller controller, RunDataStore runDataStore, TesterCloud testerCloud) { this.controller = controller; this.curator = controller.curator(); this.logs = new BufferedLogStore(curator, runDataStore); this.cloud = testerCloud; + this.badges = new Badges(controller.zoneRegistry().badgeUrl()); } public TesterCloud cloud() { return cloud; } @@ -110,7 +113,7 @@ public class JobController { locked(id, __ -> { List<LogEntry> entries = messages.stream() .map(message -> new LogEntry(0, controller.clock().millis(), LogEntry.typeOf(level), message)) - .collect(Collectors.toList()); + .collect(toList()); logs.append(id.application(), id.type(), step, entries); return __; }); @@ -157,7 +160,7 @@ public class JobController { /** Returns an immutable map of all known runs for the given application and job type. */ public Map<RunId, Run> runs(ApplicationId id, JobType type) { - Map<RunId, Run> runs = curator.readHistoricRuns(id, type); + SortedMap<RunId, Run> runs = curator.readHistoricRuns(id, type); last(id, type).ifPresent(run -> runs.putIfAbsent(run.id(), run)); return ImmutableMap.copyOf(runs); } @@ -314,6 +317,25 @@ public class JobController { } } + /** Returns a URI which points at a badge showing historic status of given length for the given job type for the given application. */ + public URI historicBadge(ApplicationId id, JobType type, int historyLength) { + Map<RunId, Run> runs = runs(id, type); + return badges.historic(id, + runs.values().stream() + .skip(runs.size() - historyLength) + .collect(toList())); + } + + /** Returns a URI which points at a badge showing current status for all jobs for the given application. */ + public URI overviewBadge(ApplicationId id) { + DeploymentSteps steps = new DeploymentSteps(controller.applications().require(id).deploymentSpec(), controller::system); + return badges.overview(id, + steps.jobs().stream() + .map(type -> last(id, type)) + .filter(Optional::isPresent).map(Optional::get) + .collect(toList())); + } + /** Returns a URI of the tester endpoint retrieved from the routing generator, provided it matches an expected form. */ Optional<URI> testerEndpoint(RunId id) { ApplicationId tester = id.tester().id(); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java index cfd36a25de8..b76e41971cf 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java @@ -14,6 +14,7 @@ import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.container.jdisc.HttpResponse; import com.yahoo.container.jdisc.LoggingRequestHandler; import com.yahoo.io.IOUtils; +import com.yahoo.jdisc.Response; import com.yahoo.log.LogLevel; import com.yahoo.restapi.Path; import com.yahoo.slime.Cursor; @@ -83,6 +84,7 @@ import javax.ws.rs.InternalServerErrorException; import javax.ws.rs.NotAuthorizedException; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.net.URI; import java.net.URISyntaxException; import java.security.Principal; @@ -172,6 +174,8 @@ public class ApplicationApiHandler extends LoggingRequestHandler { if (path.matches("/application/v4/tenant/{tenant}/application")) return applications(path.get("tenant"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}")) return application(path.get("tenant"), path.get("application"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/logs")) return logs(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request.getUri().getQuery()); + if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/badge")) return badge(path.get("tenant"), path.get("application"), path.get("instance")); + if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/badge/{badgetype}")) return badge(path.get("tenant"), path.get("application"), path.get("instance"), path.get("job"), request.getProperty("historyLength")); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/job")) return JobControllerApiHandlerHelper.jobTypeResponse(controller, appIdFromPath(path), request.getUri()); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/job/{jobtype}")) return JobControllerApiHandlerHelper.runResponse(controller.jobController().runs(appIdFromPath(path), jobTypeFromPath(path)), request.getUri()); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/job/{jobtype}/run/{number}")) return JobControllerApiHandlerHelper.runDetailsResponse(controller.jobController(), runIdFromPath(path), request.getProperty("after")); @@ -948,6 +952,28 @@ public class ApplicationApiHandler extends LoggingRequestHandler { } } + /** Returns a URI which points to an overview badge for the given application. */ + private HttpResponse badge(String tenant, String application, String instance) { + URI location = controller.jobController().overviewBadge(ApplicationId.from(tenant, application, instance)); + return redirect(location); + } + + /** Returns a URI which points to a history badge for the given application and job type. */ + private HttpResponse badge(String tenant, String application, String instance, String jobName, String historyLength) { + URI location = controller.jobController().historicBadge(ApplicationId.from(tenant, application, instance), + JobType.fromJobName(jobName), + historyLength == null ? 5 : Integer.parseInt(historyLength)); + return redirect(location); + } + + private static HttpResponse redirect(URI location) { + HttpResponse httpResponse = new HttpResponse(Response.Status.FOUND) { + @Override public void render(OutputStream outputStream) { } + }; + httpResponse.headers().add("Location", location.toString()); + return httpResponse; + } + private static DeploymentJobs.JobReport toJobReport(String tenantName, String applicationName, Inspector report) { Optional<DeploymentJobs.JobError> jobError = Optional.empty(); if (report.field("jobError").valid()) { diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/BadgesTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/BadgesTest.java new file mode 100644 index 00000000000..2c89dec61a8 --- /dev/null +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/BadgesTest.java @@ -0,0 +1,66 @@ +package com.yahoo.vespa.hosted.controller.deployment; + +import com.google.common.collect.ImmutableMap; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; +import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId; +import org.junit.Assert; +import org.junit.Test; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumMap; +import java.util.Optional; + +import static com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType.stagingTest; +import static com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType.systemTest; +import static com.yahoo.vespa.hosted.controller.deployment.Step.report; +import static java.time.Instant.now; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +/** + * @author jonmv + */ +public class BadgesTest { + + private static final ApplicationId id = ApplicationId.from("tenant", "application", "default"); + private static final Run success = new Run(new RunId(id, systemTest, 3), ImmutableMap.of(report, Step.Status.succeeded), + null, null, Optional.of(now()), RunStatus.success, 0); + + private static final Run running = new Run(new RunId(id, systemTest, 4), ImmutableMap.of(report, Step.Status.succeeded), + null, null, Optional.empty(), RunStatus.running, 0); + + private static final Run failure = new Run(new RunId(id, JobType.stagingTest, 2), ImmutableMap.of(report, Step.Status.succeeded), + null, null, Optional.of(now()), RunStatus.testFailure, 0); + + @Test + public void test() { + Badges badges = new Badges(URI.create("https://badges.tld/api/")); + + assertEquals(URI.create("https://badges.tld/api/tenant.application;" + Badges.dark), + badges.historic(id, Collections.emptyList())); + + assertEquals(URI.create("https://badges.tld/api/tenant.application;" + Badges.dark + + "/" + systemTest.jobName() + ";" + Badges.purple + + "/%20;" + Badges.purple + ";s%7B" + Badges.white + "%7D"), + badges.historic(id, Collections.singletonList(running))); + + assertEquals(URI.create("https://badges.tld/api/tenant.application;" + Badges.dark + + "/" + systemTest.jobName() + ";" + Badges.blue + + "/%20;" + Badges.blue + ";s%7B" + Badges.white + "%7D" + + "/%20;" + Badges.purple + ";s%7B" + Badges.white + "%7D"), + badges.historic(id, Arrays.asList(success, running))); + + assertEquals(URI.create("https://badges.tld/api/tenant.application;" + Badges.dark + + "/" + systemTest.jobName() + ";" + Badges.purple + + "/" + stagingTest.jobName() + ";" + Badges.red), + badges.overview(id, Arrays.asList(running, failure))); + } + +} diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java index 22ef61839be..adc3594c05f 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java @@ -457,6 +457,10 @@ public class ApplicationApiTest extends ControllerContainerTest { .data(createApplicationSubmissionData(packageWithService)), "{\"version\":\"1.0.43-d00d\"}"); + tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/default/badge", GET) + .userIdentity(USER_ID), + "", 302); + ApplicationId app1 = ApplicationId.from("tenant1", "application1", "default"); tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/jobreport", POST) .screwdriverIdentity(SCREWDRIVER_ID) |