diff options
author | Valerij Fredriksen <freva@users.noreply.github.com> | 2021-05-07 15:38:33 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-05-07 15:38:33 +0200 |
commit | cae8dc88c08cd14f7fff6eb91ec44df2ec1285d9 (patch) | |
tree | 41c3033919daeaef3e9d272385ab1e9579527a4a | |
parent | ad66501b29e1ea3edc515af871caa7c41297921a (diff) | |
parent | ca663df8b7af46fcd6b59631617e1f2fc725954c (diff) |
Merge pull request #17775 from vespa-engine/jonmv/badges
Jonmv/badges
12 files changed, 656 insertions, 178 deletions
diff --git a/container-search/src/main/java/com/yahoo/search/query/Presentation.java b/container-search/src/main/java/com/yahoo/search/query/Presentation.java index e147b14071a..b10e8442a5f 100644 --- a/container-search/src/main/java/com/yahoo/search/query/Presentation.java +++ b/container-search/src/main/java/com/yahoo/search/query/Presentation.java @@ -128,7 +128,7 @@ public class Presentation implements Cloneable { return clone; } catch (CloneNotSupportedException e) { - throw new RuntimeException("Someone inserted a noncloneable superclass",e); + throw new RuntimeException("Someone inserted a noncloneable superclass", e); } } diff --git a/container-search/src/main/java/com/yahoo/search/query/Select.java b/container-search/src/main/java/com/yahoo/search/query/Select.java index d90550084eb..a7e491f5269 100644 --- a/container-search/src/main/java/com/yahoo/search/query/Select.java +++ b/container-search/src/main/java/com/yahoo/search/query/Select.java @@ -1,7 +1,6 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.search.query; -import com.yahoo.processing.request.CompoundName; import com.yahoo.search.Query; import com.yahoo.search.grouping.GroupingRequest; import com.yahoo.search.query.parser.ParserEnvironment; @@ -12,13 +11,12 @@ import com.yahoo.search.yql.VespaGroupingStep; import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; /** - * The parameters defining the where-clause and groping of a query + * The parameters defining the where-clause and grouping of a query * * @author henrhoi */ @@ -26,7 +24,6 @@ public class Select implements Cloneable { /** The type representing the property arguments consumed by this */ private static final QueryProfileType argumentType; - private static final CompoundName argumentTypeName; public static final String SELECT = "select"; public static final String WHERE = "where"; @@ -46,7 +43,6 @@ public class Select implements Cloneable { argumentType.addField(new FieldDescription(WHERE, "string")); argumentType.addField(new FieldDescription(GROUPING, "string")); argumentType.freeze(); - argumentTypeName = new CompoundName(argumentType.getId().getName()); } public static QueryProfileType getArgumentType() { return argumentType; } diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/zone/ZoneRegistry.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/zone/ZoneRegistry.java index 76637d10a6e..7539ef3c63a 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/zone/ZoneRegistry.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/zone/ZoneRegistry.java @@ -86,9 +86,6 @@ public interface ZoneRegistry { /** Returns a URL used to request support from the Vespa team. */ URI supportUrl(); - /** Returns a URL used to generate flashy badges from strings. */ - URI badgeUrl(); - /** Returns a URL to the controller's api endpoint */ URI apiUrl(); 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 deleted file mode 100644 index f5369406f97..00000000000 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Badges.java +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.controller.deployment; - -import com.yahoo.config.provision.ApplicationId; - -import java.net.URI; -import java.util.List; -import java.util.Optional; - -/** - * 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, Optional<Run> lastCompleted, List<Run> runs) { - StringBuilder path = new StringBuilder(id + ";" + dark); - - lastCompleted.ifPresent(last -> path.append("/").append(last.id().type().jobName()).append(";").append(colorOf(last))); - 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 3dc88d5d6d2..25bc21a0076 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 @@ -82,7 +82,6 @@ public class JobController { private final CuratorDb curator; private final BufferedLogStore logs; private final TesterCloud cloud; - private final Badges badges; private final JobMetrics metric; private final AtomicReference<Consumer<Run>> runner = new AtomicReference<>(__ -> { }); @@ -92,7 +91,6 @@ public class JobController { this.curator = controller.curator(); this.logs = new BufferedLogStore(curator, controller.serviceRegistry().runDataStore()); this.cloud = controller.serviceRegistry().testerCloud(); - this.badges = new Badges(controller.zoneRegistry().badgeUrl()); this.metric = new JobMetrics(controller.metric(), controller::system); } @@ -539,30 +537,6 @@ 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) { - List<Run> runs = new ArrayList<>(runs(id, type).values()); - Run lastCompleted = null; - if (runs.size() > 0) - lastCompleted = runs.get(runs.size() - 1); - if (runs.size() > 1 && ! lastCompleted.hasEnded()) - lastCompleted = runs.get(runs.size() - 2); - - return badges.historic(id, Optional.ofNullable(lastCompleted), runs.subList(Math.max(0, runs.size() - historyLength), runs.size())); - } - - /** 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().requireApplication(TenantAndApplicationId.from(id)) - .deploymentSpec().requireInstance(id.instance()), - controller::system); - return badges.overview(id, - steps.jobs().stream() - .map(type -> last(id, type)) - .flatMap(Optional::stream) - .collect(toList())); - } - private void prunePackages(TenantAndApplicationId id) { controller.applications().lockApplicationIfPresent(id, application -> { application.get().productionDeployments().values().stream() diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/BadgeApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/BadgeApiHandler.java index befec42e84e..6fef422b811 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/BadgeApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/BadgeApiHandler.java @@ -5,19 +5,23 @@ import com.yahoo.config.provision.ApplicationId; import com.yahoo.container.jdisc.HttpRequest; import com.yahoo.container.jdisc.HttpResponse; import com.yahoo.container.jdisc.LoggingRequestHandler; -import com.yahoo.jdisc.Response; import com.yahoo.jdisc.http.HttpRequest.Method; import com.yahoo.restapi.Path; import com.yahoo.vespa.hosted.controller.Controller; +import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId; import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; import com.yahoo.restapi.ErrorResponse; +import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId; +import com.yahoo.vespa.hosted.controller.deployment.DeploymentStatus; import com.yahoo.yolean.Exceptions; +import java.io.IOException; import java.io.OutputStream; -import java.net.URI; import java.util.logging.Level; import java.util.logging.Logger; +import static java.nio.charset.StandardCharsets.UTF_8; + /** * This API serves redirects to a badge server. * @@ -62,24 +66,30 @@ public class BadgeApiHandler 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); + ApplicationId id = ApplicationId.from(tenant, application, instance); + DeploymentStatus status = controller.jobController().deploymentStatus(controller.applications().requireApplication(TenantAndApplicationId.from(id))); + return svgResponse(Badges.overviewBadge(id, + status.jobs().instance(id.instance()), + controller.system())); } /** 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 : Math.min(32, Math.max(0, Integer.parseInt(historyLength)))); - return redirect(location); + ApplicationId id = ApplicationId.from(tenant, application, instance); + return svgResponse(Badges.historyBadge(id, + controller.jobController().jobStatus(new JobId(id, JobType.fromJobName(jobName))), + historyLength == null ? 5 : Math.min(32, Math.max(0, Integer.parseInt(historyLength))))); } - private static HttpResponse redirect(URI location) { - HttpResponse httpResponse = new HttpResponse(Response.Status.FOUND) { - @Override public void render(OutputStream outputStream) { } + private static HttpResponse svgResponse(String svg) { + return new HttpResponse(200) { + @Override public void render(OutputStream outputStream) throws IOException { + outputStream.write(svg.getBytes(UTF_8)); + } + @Override public String getContentType() { + return "image/svg+xml; charset=UTF-8"; + } }; - httpResponse.headers().add("Location", location.toString()); - return httpResponse; } } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/Badges.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/Badges.java new file mode 100644 index 00000000000..006da9f4439 --- /dev/null +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/Badges.java @@ -0,0 +1,293 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.hosted.controller.restapi.deployment; + +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.SystemName; +import com.yahoo.slime.ArrayTraverser; +import com.yahoo.slime.SlimeUtils; +import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; +import com.yahoo.vespa.hosted.controller.deployment.JobList; +import com.yahoo.vespa.hosted.controller.deployment.JobStatus; +import com.yahoo.vespa.hosted.controller.deployment.Run; +import com.yahoo.vespa.hosted.controller.deployment.RunStatus; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static java.util.stream.Collectors.toList; + +public class Badges { + + // https://chrishewett.com/blog/calculating-text-width-programmatically/ thank you! + private static final String characterWidths = "[[\" \",35.156],[\"!\",39.355],[\"\\\"\",45.898],[\"#\",81.836],[\"$\",63.574],[\"%\",107.617],[\"&\",72.656],[\"'\",26.855],[\"(\",45.41],[\")\",45.41],[\"*\",63.574],[\"+\",81.836],[\",\",36.377],[\"-\",45.41],[\".\",36.377],[\"/\",45.41],[\"0\",63.574],[\"1\",63.574],[\"2\",63.574],[\"3\",63.574],[\"4\",63.574],[\"5\",63.574],[\"6\",63.574],[\"7\",63.574],[\"8\",63.574],[\"9\",63.574],[\":\",45.41],[\";\",45.41],[\"<\",81.836],[\"=\",81.836],[\">\",81.836],[\"?\",54.541],[\"@\",100],[\"A\",68.359],[\"B\",68.555],[\"C\",69.824],[\"D\",77.051],[\"E\",63.232],[\"F\",57.471],[\"G\",77.539],[\"H\",75.146],[\"I\",42.09],[\"J\",45.459],[\"K\",69.287],[\"L\",55.664],[\"M\",84.277],[\"N\",74.805],[\"O\",78.711],[\"P\",60.303],[\"Q\",78.711],[\"R\",69.531],[\"S\",68.359],[\"T\",61.621],[\"U\",73.193],[\"V\",68.359],[\"W\",98.877],[\"X\",68.506],[\"Y\",61.523],[\"Z\",68.506],[\"[\",45.41],[\"\\\\\",45.41],[\"]\",45.41],[\"^\",81.836],[\"_\",63.574],[\"`\",63.574],[\"a\",60.059],[\"b\",62.305],[\"c\",52.1],[\"d\",62.305],[\"e\",59.57],[\"f\",35.156],[\"g\",62.305],[\"h\",63.281],[\"i\",27.441],[\"j\",34.424],[\"k\",59.18],[\"l\",27.441],[\"m\",97.266],[\"n\",63.281],[\"o\",60.693],[\"p\",62.305],[\"q\",62.305],[\"r\",42.676],[\"s\",52.1],[\"t\",39.404],[\"u\",63.281],[\"v\",59.18],[\"w\",81.836],[\"x\",59.18],[\"y\",59.18],[\"z\",52.539],[\"{\",63.477],[\"|\",45.41],[\"}\",63.477],[\"~\",81.836],[\"_median\",63.281]]"; + private static final double[] widths = new double[128]; // 0-94 hold widths for corresponding chars (+32); 95 holds the fallback width. + + static { + SlimeUtils.jsonToSlimeOrThrow(characterWidths).get() + .traverse((ArrayTraverser) (i, pair) -> { + if (i < 95) + assert Arrays.equals(new byte[]{(byte) (i + 32)}, pair.entry(0).asUtf8()) : i + ": " + pair.entry(0).asString(); + else + assert "_median".equals(pair.entry(0).asString()); + + widths[i] = pair.entry(1).asDouble(); + }); + } + + /** Character pixel width of a 100px size Verdana font rendering of the given code point, for code points in the range [32, 126]. */ + public static double widthOf(int codePoint) { + return 32 <= codePoint && codePoint <= 126 ? widths[codePoint - 32] : widths[95]; + } + + /** Computes an approximate pixel width of a 10px size Verdana font rendering of the given string, ignoring kerning. */ + public static double widthOf(String text) { + return text.codePoints().mapToDouble(Badges::widthOf).sum() / 10; + } + + static String colorOf(Run run, Boolean wasOk) { + switch (run.status()) { + case running: + return wasOk ? "url(#run-on-success)" : "url(#run-on-failure)"; + case success: + return success; + default: + return failure; + } + } + + static String nameOf(JobType type) { + return type.isTest() ? type.isProduction() ? "test" + : type.jobName() + : type.jobName().replace("production-", ""); + } + + static final double xPad = 8; + static final double logoSize = 16; + static final String dark = "#5a5a5a"; + static final String success = "#00ff48"; + static final String running = "#ab83ff"; + static final String failure = "#bf103c"; + + static void addText(List<String> texts, String text, double x, double width) { + texts.add(" <text x='" + (x + 0.5) + "' y='14' fill='#000' fill-opacity='.3' textLength='" + width + "'>" + text + "</text>\n"); + texts.add(" <text x='" + x + "' y='13' fill='#fff' textLength='" + width + "'>" + text + "</text>\n"); + } + + static void addShade(List<String> sections, double x, double width) { + sections.add(" <rect x='" + (x - 6) + "' rx='3' width='" + (width + 6) + "' height='20' fill='url(#shade)'/>\n"); + } + + static void addShadow(List<String> sections, double x) { + sections.add(" <rect x='" + (x - 6) + "' rx='3' width='" + 9 + "' height='20' fill='url(#shadow)'/>\n"); + } + + static String historyBadge(ApplicationId id, JobStatus status, int length) { + List<String> sections = new ArrayList<>(); + List<String> texts = new ArrayList<>(); + + double x = 0; + String text = id.toFullString(); + double textWidth = widthOf(text); + double dx = xPad + logoSize + xPad + textWidth + xPad; + + addShade(sections, x, dx); + sections.add(" <rect width='" + dx + "' height='20' fill='" + dark + "'/>\n"); + addText(texts, text, x + (xPad + logoSize + dx) / 2, textWidth); + x += dx; + + if (status.lastTriggered().isEmpty()) + return badge(sections, texts, x); + + Run lastTriggered = status.lastTriggered().get(); + List<Run> runs = status.runs().descendingMap().values().stream() + .filter(Run::hasEnded) + .limit(length + (lastTriggered.hasEnded() ? 0 : 1)) + .collect(toList()); + + boolean isOk = runs.isEmpty() || runs.get(0).status() == RunStatus.success; + if ( ! lastTriggered.hasEnded()) + runs.remove(0); + + text = lastTriggered.id().type().jobName(); + textWidth = widthOf(text); + dx = xPad + textWidth + xPad; + addShade(sections, x, dx); + sections.add(" <rect x='" + (x - 6) + "' rx='3' width='" + (dx + 6) + "' height='20' fill='" + colorOf(lastTriggered, isOk) + "'/>\n"); + addShadow(sections, x + dx); + addText(texts, text, x + dx / 2, textWidth); + x += dx; + + dx = xPad * (128.0 / (32 + runs.size())); // Broader sections with shorter history. + for (Run run : runs) { + addShade(sections, x, dx); + sections.add(" <rect x='" + (x - 6) + "' rx='3' width='" + (dx + 6) + "' height='20' fill='" + colorOf(run, null) + "'/>\n"); + addShadow(sections, x + dx); + dx *= Math.pow(0.3, 1.0 / (runs.size() + 8)); // Gradually narrowing sections with age. + x += dx; + } + Collections.reverse(sections); + + return badge(sections, texts, x); + } + + static String overviewBadge(ApplicationId id, JobList jobs, SystemName system) { + // Put production tests right after their deployments, for a more compact rendering. + List<Run> runs = new ArrayList<>(jobs.lastTriggered().asList()); + for (int i = 0; i < runs.size(); i++) { + Run run = runs.get(i); + if (run.id().type().isProduction() && run.id().type().isTest()) { + int j = i; + while (!runs.get(j - 1).id().type().zone(system).equals(run.id().type().zone(system))) + runs.set(j, runs.get(--j)); + runs.set(j, run); + } + } + + List<String> sections = new ArrayList<>(); + List<String> texts = new ArrayList<>(); + + double x = 0; + String text = id.toFullString(); + double textWidth = widthOf(text); + double dx = xPad + logoSize + xPad + textWidth + xPad; + double tdx = xPad + widthOf("test"); + + addShade(sections, 0, dx); + sections.add(" <rect width='" + dx + "' height='20' fill='" + dark + "'/>\n"); + addText(texts, text, x + (xPad + logoSize + dx) / 2, textWidth); + x += dx; + + for (int i = 0; i < runs.size(); i++) { + Run run = runs.get(i); + Run test = i + 1 < runs.size() ? runs.get(i + 1) : null; + if (test == null || ! test.id().type().isTest() || ! test.id().type().isProduction()) + test = null; + + boolean isTest = run.id().type().isTest() && run.id().type().isProduction(); + text = nameOf(run.id().type()); + textWidth = widthOf(text); + dx = xPad + textWidth + (isTest ? 0 : xPad); + boolean wasOk = jobs.get(run.id().job()).flatMap(JobStatus::lastStatus).map(RunStatus.success::equals).orElse(true); + + addText(texts, text, x + (dx - (isTest ? xPad : 0)) / 2, textWidth); + + // Add "deploy" when appropriate + if ( ! run.id().type().isTest()) { + String deploy = "deploy"; + textWidth = widthOf(deploy); + addText(texts, deploy, x + dx + textWidth / 2, textWidth); + dx += textWidth + xPad; + } + + // Add shade across zone section. + if ( ! (isTest)) + addShade(sections, x, dx + (test != null ? tdx : 0)); + + // Add colored section for job ... + if (test == null) + sections.add(" <rect x='" + (x - 16) + "' rx='3' width='" + (dx + 16) + "' height='20' fill='" + colorOf(run, wasOk) + "'/>\n"); + // ... with a slant if a test is next. + else + sections.add(" <polygon points='" + (x - 6) + " 0 " + (x - 6) + " 20 " + (x + dx - 8.5) + " 20 " + (x + dx - 0.5) + " 0' fill='" + colorOf(run, wasOk) + "'/>\n"); + + // Cast a shadow onto the next zone ... + if (test == null) + addShadow(sections, x + dx); + + x += dx; + } + Collections.reverse(sections); + + return badge(sections, texts, x); + } + + static String badge(List<String> sections, List<String> texts, double width) { + return "<svg xmlns='http://www.w3.org/2000/svg' width='" + width + "' height='20' role='img' aria-label='Deployment Status'>\n" + + " <title>Deployment Status</title>\n" + + // Lighting to give the badge a 3d look--dispersion at the top, shadow at the bottom. + " <linearGradient id='light' x2='0' y2='100%'>\n" + + " <stop offset='0' stop-color='#fff' stop-opacity='.5'/>\n" + + " <stop offset='.1' stop-color='#fff' stop-opacity='.15'/>\n" + + " <stop offset='.9' stop-color='#000' stop-opacity='.15'/>\n" + + " <stop offset='1' stop-color='#000' stop-opacity='.7'/>\n" + + " </linearGradient>\n" + + // Dispersed light at the left of the badge. + " <linearGradient id='left-light' x2='100%' y2='0'>\n" + + " <stop offset='0' stop-color='#fff' stop-opacity='.3'/>\n" + + " <stop offset='.5' stop-color='#fff' stop-opacity='.1'/>\n" + + " <stop offset='1' stop-color='#fff' stop-opacity='.0'/>\n" + + " </linearGradient>\n" + + // Shadow at the right of the badge. + " <linearGradient id='right-shadow' x2='100%' y2='0'>\n" + + " <stop offset='0' stop-color='#000' stop-opacity='.0'/>\n" + + " <stop offset='.5' stop-color='#000' stop-opacity='.1'/>\n" + + " <stop offset='1' stop-color='#000' stop-opacity='.3'/>\n" + + " </linearGradient>\n" + + // Shadow to highlight the border between sections, without using a heavy separator. + " <linearGradient id='shadow' x2='100%' y2='0'>\n" + + " <stop offset='0' stop-color='#222' stop-opacity='.4'/>\n" + + " <stop offset='.5' stop-color='#555' stop-opacity='.4'/>\n" + + " <stop offset='1' stop-color='#555' stop-opacity='.0'/>\n" + + " </linearGradient>\n" + + // Weak shade across each panel to highlight borders further. + " <linearGradient id='shade' x2='100%' y2='0'>\n" + + " <stop offset='0' stop-color='#000' stop-opacity='.20'/>\n" + + " <stop offset='0.05' stop-color='#000' stop-opacity='.10'/>\n" + + " <stop offset='1' stop-color='#000' stop-opacity='.0'/>\n" + + " </linearGradient>\n" + + // Running color sloshing back and forth on top of the failure color. + " <linearGradient id='run-on-failure' x1='40%' x2='80%' y2='0%'>\n" + + " <stop offset='0' stop-color='" + running + "' />\n" + + " <stop offset='1' stop-color='" + failure + "' />\n" + + " <animate attributeName='x1' values='-110%;150%;20%;-110%' dur='6s' repeatCount='indefinite' />\n" + + " <animate attributeName='x2' values='-10%;250%;120%;-10%' dur='6s' repeatCount='indefinite' />\n" + + " </linearGradient>\n" + + // Running color sloshing back and forth on top of the success color. + " <linearGradient id='run-on-success' x1='40%' x2='80%' y2='0%'>\n" + + " <stop offset='0' stop-color='" + running + "' />\n" + + " <stop offset='1' stop-color='" + success + "' />\n" + + " <animate attributeName='x1' values='-110%;150%;20%;-110%' dur='6s' repeatCount='indefinite' />\n" + + " <animate attributeName='x2' values='-10%;250%;120%;-10%' dur='6s' repeatCount='indefinite' />\n" + + " </linearGradient>\n" + + // Clipping to give the badge rounded corners. + " <clipPath id='rounded'>\n" + + " <rect width='" + width + "' height='20' rx='3' fill='#fff'/>\n" + + " </clipPath>\n" + + // Badge section backgrounds with status colors and shades for distinction. + " <g clip-path='url(#rounded)'>\n" + + String.join("", sections) + + " <rect width='" + 2 + "' height='20' fill='url(#left-light)'/>\n" + + " <rect x='" + (width - 2) + "' width='" + 2 + "' height='20' fill='url(#right-shadow)'/>\n" + + " <rect width='" + width + "' height='20' fill='url(#light)'/>\n" + + " </g>\n" + + " <g fill='#fff' text-anchor='middle' font-family='Verdana,Geneva,DejaVu Sans,sans-serif' text-rendering='geometricPrecision' font-size='10'>\n" + + // The vespa.ai logo (with a slightly coloured shadow)! + " <svg x='" + (xPad + 0.5) + "' y='" + ((20 - logoSize) / 2 + 1) + "' width='" + logoSize + "' height='" + logoSize + "' viewBox='0 0 150 150'>\n" + + " <polygon fill='#402a14' fill-opacity='0.5' points='84.84 10 34.1 44.46 34.1 103.78 84.84 68.02 135.57 103.78 135.57 44.46 84.84 10'/>\n" + + " <polygon fill='#402a14' fill-opacity='0.5' points='84.84 68.02 84.84 10 135.57 44.46 135.57 103.78 84.84 68.02'/>\n" + + " <polygon fill='#061a29' fill-opacity='0.5' points='65.07 81.99 14.34 46.22 14.34 105.54 65.07 140 115.81 105.54 115.81 46.22 65.07 81.99'/>\n" + + " <polygon fill='#061a29' fill-opacity='0.5' points='65.07 81.99 65.07 140 14.34 105.54 14.34 46.22 65.07 81.99'/>\n" + + " </svg>\n" + + " <svg x='" + xPad + "' y='" + ((20 - logoSize) / 2) + "' width='" + logoSize + "' height='" + logoSize + "' viewBox='0 0 150 150'>\n" + + " <linearGradient id='yellow-shaded' x1='91.17' y1='44.83' x2='136.24' y2='73.4' gradientUnits='userSpaceOnUse'>\n" + + " <stop offset='0.01' stop-color='#c6783e'/>\n" + + " <stop offset='0.54' stop-color='#ff9750'/>\n" + + " </linearGradient>\n" + + " <linearGradient id='blue-shaded' x1='60.71' y1='104.56' x2='-15.54' y2='63' gradientUnits='userSpaceOnUse'>\n" + + " <stop offset='0' stop-color='#005a8e'/>\n" + + " <stop offset='0.54' stop-color='#1a7db6'/>\n" + + " </linearGradient>\n" + + " <polygon fill='#ff9d4b' points='84.84 10 34.1 44.46 34.1 103.78 84.84 68.02 135.57 103.78 135.57 44.46 84.84 10'/>\n" + + " <polygon fill='url(#yellow-shaded)' points='84.84 68.02 84.84 10 135.57 44.46 135.57 103.78 84.84 68.02'/>\n" + + " <polygon fill='#1a7db6' points='65.07 81.99 14.34 46.22 14.34 105.54 65.07 140 115.81 105.54 115.81 46.22 65.07 81.99'/>\n" + + " <polygon fill='url(#blue-shaded)' points='65.07 81.99 65.07 140 14.34 105.54 14.34 46.22 65.07 81.99'/>\n" + + " </svg>\n" + + // Application ID and job names. + String.join("", texts) + + " </g>\n" + + "</svg>\n"; + } + +}
\ No newline at end of file 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 deleted file mode 100644 index 06d5a42f9c0..00000000000 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/BadgesTest.java +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -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.Test; - -import java.net.URI; -import java.util.Collections; -import java.util.List; -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.EPOCH; -import static java.time.Instant.now; -import static org.junit.Assert.assertEquals; - -/** - * @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, new StepInfo(report, Step.Status.succeeded, Optional.empty())), - null, null, Optional.of(now()), RunStatus.success, 0, EPOCH, Optional.empty(), Optional.empty(), Optional.empty()); - - private static final Run running = new Run(new RunId(id, systemTest, 4), ImmutableMap.of(report, new StepInfo(report, Step.Status.succeeded, Optional.empty())), - null, null, Optional.empty(), RunStatus.running, 0, EPOCH, Optional.empty(), Optional.empty(), Optional.empty()); - - private static final Run failure = new Run(new RunId(id, JobType.stagingTest, 2), ImmutableMap.of(report, new StepInfo(report, Step.Status.succeeded, Optional.empty())), - null, null, Optional.of(now()), RunStatus.testFailure, 0, EPOCH, Optional.empty(), Optional.empty(), Optional.empty()); - - @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, Optional.empty(), Collections.emptyList())); - - assertEquals(URI.create("https://badges.tld/api/tenant.application;" + Badges.dark + - "/" + systemTest.jobName() + ";" + Badges.blue + - "/%20;" + Badges.purple + ";s%7B" + Badges.white + "%7D"), - badges.historic(id, Optional.of(success), 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, Optional.of(success), List.of(success, running))); - - assertEquals(URI.create("https://badges.tld/api/tenant.application;" + Badges.dark + - "/" + systemTest.jobName() + ";" + Badges.purple + - "/" + stagingTest.jobName() + ";" + Badges.red), - badges.overview(id, List.of(running, failure))); - } - -} diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ZoneRegistryMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ZoneRegistryMock.java index 1d2c743ffba..f1e0b8125e1 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ZoneRegistryMock.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ZoneRegistryMock.java @@ -206,11 +206,6 @@ public class ZoneRegistryMock extends AbstractComponent implements ZoneRegistry } @Override - public URI badgeUrl() { - return URI.create("https://badges.tld"); - } - - @Override public URI apiUrl() { return URI.create("https://api.tld:4443/"); } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/BadgeApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/BadgeApiTest.java index ff02a1e16be..21d8030dbee 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/BadgeApiTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/BadgeApiTest.java @@ -2,11 +2,18 @@ package com.yahoo.vespa.hosted.controller.restapi.deployment; import com.yahoo.vespa.hosted.controller.ControllerTester; +import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; +import com.yahoo.vespa.hosted.controller.application.ApplicationPackage; +import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder; import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester; import com.yahoo.vespa.hosted.controller.restapi.ContainerTester; import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest; import org.junit.Test; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + /** * @author jonmv */ @@ -15,15 +22,40 @@ public class BadgeApiTest extends ControllerContainerTest { private static final String responseFiles = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/"; @Test - public void testBadgeApi() { + public void testBadgeApi() throws IOException { ContainerTester tester = new ContainerTester(container, responseFiles); var application = new DeploymentTester(new ControllerTester(tester)).newDeploymentContext("tenant", "application", "default"); - application.submit(); + ApplicationPackage applicationPackage = new ApplicationPackageBuilder().systemTest() + .parallel("us-west-1", "aws-us-east-1a") + .test("us-west-1") + .test("aws-us-east-1a") + .region("ap-southeast-1") + .test("ap-southeast-1") + .region("eu-west-1") + .test("eu-west-1") + .build(); + application.submit(applicationPackage).deploy(); + application.submit(applicationPackage) + .runJob(JobType.systemTest) + .runJob(JobType.stagingTest) + .runJob(JobType.productionUsWest1) + .runJob(JobType.productionAwsUsEast1a) + .runJob(JobType.testUsWest1) + .runJob(JobType.testAwsUsEast1a) + .runJob(JobType.productionApSoutheast1) + .failDeployment(JobType.testApSoutheast1); + application.submit(applicationPackage) + .runJob(JobType.systemTest) + .runJob(JobType.stagingTest); + for (int i = 0; i < 32; i++) + application.failDeployment(JobType.productionUsWest1); + application.triggerJobs(); + tester.controller().applications().deploymentTrigger().reTrigger(application.instanceId(), JobType.testEuWest1); tester.assertResponse(authenticatedRequest("http://localhost:8080/badge/v1/tenant/application/default"), - "", 302); - tester.assertResponse(authenticatedRequest("http://localhost:8080/badge/v1/tenant/application/default/system-test?historyLength=10"), - "", 302); + Files.readString(Paths.get(responseFiles + "overview.svg")), 200); + tester.assertResponse(authenticatedRequest("http://localhost:8080/badge/v1/tenant/application/default/production-us-west-1?historyLength=32"), + Files.readString(Paths.get(responseFiles + "history.svg")), 200); } } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/history.svg b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/history.svg new file mode 100644 index 00000000000..a081b21ec3f --- /dev/null +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/history.svg @@ -0,0 +1,176 @@ +<svg xmlns='http://www.w3.org/2000/svg' width='616.7390658896196' height='20' role='img' aria-label='Deployment Status'> + <title>Deployment Status</title> + <linearGradient id='light' x2='0' y2='100%'> + <stop offset='0' stop-color='#fff' stop-opacity='.5'/> + <stop offset='.1' stop-color='#fff' stop-opacity='.15'/> + <stop offset='.9' stop-color='#000' stop-opacity='.15'/> + <stop offset='1' stop-color='#000' stop-opacity='.7'/> + </linearGradient> + <linearGradient id='left-light' x2='100%' y2='0'> + <stop offset='0' stop-color='#fff' stop-opacity='.3'/> + <stop offset='.5' stop-color='#fff' stop-opacity='.1'/> + <stop offset='1' stop-color='#fff' stop-opacity='.0'/> + </linearGradient> + <linearGradient id='right-shadow' x2='100%' y2='0'> + <stop offset='0' stop-color='#000' stop-opacity='.0'/> + <stop offset='.5' stop-color='#000' stop-opacity='.1'/> + <stop offset='1' stop-color='#000' stop-opacity='.3'/> + </linearGradient> + <linearGradient id='shadow' x2='100%' y2='0'> + <stop offset='0' stop-color='#222' stop-opacity='.4'/> + <stop offset='.5' stop-color='#555' stop-opacity='.4'/> + <stop offset='1' stop-color='#555' stop-opacity='.0'/> + </linearGradient> + <linearGradient id='shade' x2='100%' y2='0'> + <stop offset='0' stop-color='#000' stop-opacity='.20'/> + <stop offset='0.05' stop-color='#000' stop-opacity='.10'/> + <stop offset='1' stop-color='#000' stop-opacity='.0'/> + </linearGradient> + <linearGradient id='run-on-failure' x1='40%' x2='80%' y2='0%'> + <stop offset='0' stop-color='#ab83ff' /> + <stop offset='1' stop-color='#bf103c' /> + <animate attributeName='x1' values='-110%;150%;20%;-110%' dur='6s' repeatCount='indefinite' /> + <animate attributeName='x2' values='-10%;250%;120%;-10%' dur='6s' repeatCount='indefinite' /> + </linearGradient> + <linearGradient id='run-on-success' x1='40%' x2='80%' y2='0%'> + <stop offset='0' stop-color='#ab83ff' /> + <stop offset='1' stop-color='#00ff48' /> + <animate attributeName='x1' values='-110%;150%;20%;-110%' dur='6s' repeatCount='indefinite' /> + <animate attributeName='x2' values='-10%;250%;120%;-10%' dur='6s' repeatCount='indefinite' /> + </linearGradient> + <clipPath id='rounded'> + <rect width='616.7390658896196' height='20' rx='3' fill='#fff'/> + </clipPath> + <g clip-path='url(#rounded)'> + <rect x='610.9256720815912' rx='3' width='9' height='20' fill='url(#shadow)'/> + <rect x='604.6322196342305' rx='3' width='12.293452447360632' height='20' fill='#00ff48'/> + <rect x='604.6322196342305' rx='3' width='12.293452447360632' height='20' fill='url(#shade)'/> + <rect x='604.8245279299437' rx='3' width='9' height='20' fill='url(#shadow)'/> + <rect x='598.3387671868699' rx='3' width='12.485760743073765' height='20' fill='#bf103c'/> + <rect x='598.3387671868699' rx='3' width='12.485760743073765' height='20' fill='url(#shade)'/> + <rect x='598.5369518248465' rx='3' width='9' height='20' fill='url(#shadow)'/> + <rect x='591.8530064437961' rx='3' width='12.68394538105045' height='20' fill='#bf103c'/> + <rect x='591.8530064437961' rx='3' width='12.68394538105045' height='20' fill='url(#shade)'/> + <rect x='592.0572469867445' rx='3' width='9' height='20' fill='url(#shadow)'/> + <rect x='585.1690610627456' rx='3' width='12.888185923998947' height='20' fill='#bf103c'/> + <rect x='585.1690610627456' rx='3' width='12.888185923998947' height='20' fill='url(#shade)'/> + <rect x='585.3795425602511' rx='3' width='9' height='20' fill='url(#shadow)'/> + <rect x='578.2808751387468' rx='3' width='13.098667421504338' height='20' fill='#bf103c'/> + <rect x='578.2808751387468' rx='3' width='13.098667421504338' height='20' fill='url(#shade)'/> + <rect x='578.4977882949328' rx='3' width='9' height='20' fill='url(#shadow)'/> + <rect x='571.1822077172424' rx='3' width='13.31558057769039' height='20' fill='#bf103c'/> + <rect x='571.1822077172424' rx='3' width='13.31558057769039' height='20' fill='url(#shade)'/> + <rect x='571.4057490635566' rx='3' width='9' height='20' fill='url(#shadow)'/> + <rect x='563.866627139552' rx='3' width='13.539121924004645' height='20' fill='#bf103c'/> + <rect x='563.866627139552' rx='3' width='13.539121924004645' height='20' fill='url(#shade)'/> + <rect x='564.0969992128305' rx='3' width='9' height='20' fill='url(#shadow)'/> + <rect x='556.3275052155473' rx='3' width='13.769493997283261' height='20' fill='#bf103c'/> + <rect x='556.3275052155473' rx='3' width='13.769493997283261' height='20' fill='url(#shade)'/> + <rect x='556.564916741521' rx='3' width='9' height='20' fill='url(#shadow)'/> + <rect x='548.558011218264' rx='3' width='14.006905523256986' height='20' fill='#bf103c'/> + <rect x='548.558011218264' rx='3' width='14.006905523256986' height='20' fill='url(#shade)'/> + <rect x='548.8026773006716' rx='3' width='9' height='20' fill='url(#shadow)'/> + <rect x='540.5511056950071' rx='3' width='14.251571605664486' height='20' fill='#bf103c'/> + <rect x='540.5511056950071' rx='3' width='14.251571605664486' height='20' fill='url(#shade)'/> + <rect x='540.803248010487' rx='3' width='9' height='20' fill='url(#shadow)'/> + <rect x='532.2995340893426' rx='3' width='14.503713921144396' height='20' fill='#bf103c'/> + <rect x='532.2995340893426' rx='3' width='14.503713921144396' height='20' fill='url(#shade)'/> + <rect x='532.5593810882809' rx='3' width='9' height='20' fill='url(#shadow)'/> + <rect x='523.7958201681982' rx='3' width='14.763560920082657' height='20' fill='#bf103c'/> + <rect x='523.7958201681982' rx='3' width='14.763560920082657' height='20' fill='url(#shade)'/> + <rect x='524.0636072817127' rx='3' width='9' height='20' fill='url(#shadow)'/> + <rect x='515.0322592481156' rx='3' width='15.031348033597132' height='20' fill='#bf103c'/> + <rect x='515.0322592481156' rx='3' width='15.031348033597132' height='20' fill='url(#shade)'/> + <rect x='515.3082291013654' rx='3' width='9' height='20' fill='url(#shadow)'/> + <rect x='506.0009112145184' rx='3' width='15.307317886847013' height='20' fill='#bf103c'/> + <rect x='506.0009112145184' rx='3' width='15.307317886847013' height='20' fill='url(#shade)'/> + <rect x='506.2853138465317' rx='3' width='9' height='20' fill='url(#shadow)'/> + <rect x='496.6935933276714' rx='3' width='15.591720518860313' height='20' fill='#bf103c'/> + <rect x='496.6935933276714' rx='3' width='15.591720518860313' height='20' fill='url(#shade)'/> + <rect x='496.9866864178897' rx='3' width='9' height='20' fill='url(#shadow)'/> + <rect x='487.1018728088111' rx='3' width='15.884813609078591' height='20' fill='#bf103c'/> + <rect x='487.1018728088111' rx='3' width='15.884813609078591' height='20' fill='url(#shade)'/> + <rect x='487.4039219105567' rx='3' width='9' height='20' fill='url(#shadow)'/> + <rect x='477.2170591997325' rx='3' width='16.186862710824187' height='20' fill='#bf103c'/> + <rect x='477.2170591997325' rx='3' width='16.186862710824187' height='20' fill='url(#shade)'/> + <rect x='477.5283379808098' rx='3' width='9' height='20' fill='url(#shadow)'/> + <rect x='467.0301964889083' rx='3' width='16.498141491901468' height='20' fill='#bf103c'/> + <rect x='467.0301964889083' rx='3' width='16.498141491901468' height='20' fill='url(#shade)'/> + <rect x='467.3509869795569' rx='3' width='9' height='20' fill='url(#shadow)'/> + <rect x='456.5320549970068' rx='3' width='16.81893198255014' height='20' fill='#bf103c'/> + <rect x='456.5320549970068' rx='3' width='16.81893198255014' height='20' fill='url(#shade)'/> + <rect x='456.86264784543187' rx='3' width='9' height='20' fill='url(#shadow)'/> + <rect x='445.7131230144567' rx='3' width='17.14952483097518' height='20' fill='#bf103c'/> + <rect x='445.7131230144567' rx='3' width='17.14952483097518' height='20' fill='url(#shade)'/> + <rect x='446.05381775016656' rx='3' width='9' height='20' fill='url(#shadow)'/> + <rect x='434.56359818348153' rx='3' width='17.49021956668504' height='20' fill='#bf103c'/> + <rect x='434.56359818348153' rx='3' width='17.49021956668504' height='20' fill='url(#shade)'/> + <rect x='434.91470348867307' rx='3' width='9' height='20' fill='url(#shadow)'/> + <rect x='423.0733786167965' rx='3' width='17.841324871876566' height='20' fill='#bf103c'/> + <rect x='423.0733786167965' rx='3' width='17.841324871876566' height='20' fill='url(#shade)'/> + <rect x='423.43521260603256' rx='3' width='9' height='20' fill='url(#shadow)'/> + <rect x='411.23205374491994' rx='3' width='18.20315886111265' height='20' fill='#bf103c'/> + <rect x='411.23205374491994' rx='3' width='18.20315886111265' height='20' fill='url(#shade)'/> + <rect x='411.60494425335327' rx='3' width='9' height='20' fill='url(#shadow)'/> + <rect x='399.0288948838073' rx='3' width='18.576049369545963' height='20' fill='#bf103c'/> + <rect x='399.0288948838073' rx='3' width='18.576049369545963' height='20' fill='url(#shade)'/> + <rect x='399.41317976421124' rx='3' width='9' height='20' fill='url(#shadow)'/> + <rect x='386.45284551426136' rx='3' width='18.960334249949863' height='20' fill='#bf103c'/> + <rect x='386.45284551426136' rx='3' width='18.960334249949863' height='20' fill='url(#shade)'/> + <rect x='386.84887294313717' rx='3' width='9' height='20' fill='url(#shadow)'/> + <rect x='373.4925112643115' rx='3' width='19.35636167882567' height='20' fill='#bf103c'/> + <rect x='373.4925112643115' rx='3' width='19.35636167882567' height='20' fill='url(#shade)'/> + <rect x='373.90064005734945' rx='3' width='9' height='20' fill='url(#shadow)'/> + <rect x='360.1361495854858' rx='3' width='19.764490471863645' height='20' fill='#bf103c'/> + <rect x='360.1361495854858' rx='3' width='19.764490471863645' height='20' fill='url(#shade)'/> + <rect x='360.55674952266554' rx='3' width='9' height='20' fill='url(#shadow)'/> + <rect x='346.3716591136221' rx='3' width='20.18509040904341' height='20' fill='#bf103c'/> + <rect x='346.3716591136221' rx='3' width='20.18509040904341' height='20' fill='url(#shade)'/> + <rect x='346.8051112742472' rx='3' width='9' height='20' fill='url(#shadow)'/> + <rect x='332.1865687045787' rx='3' width='20.61854256966852' height='20' fill='#bf103c'/> + <rect x='332.1865687045787' rx='3' width='20.61854256966852' height='20' fill='url(#shade)'/> + <rect x='332.6332658125487' rx='3' width='9' height='20' fill='url(#shadow)'/> + <rect x='317.56802613491016' rx='3' width='21.06523967763854' height='20' fill='#bf103c'/> + <rect x='317.56802613491016' rx='3' width='21.06523967763854' height='20' fill='url(#shade)'/> + <rect x='318.02837291454324' rx='3' width='9' height='20' fill='url(#shadow)'/> + <rect x='302.5027864572716' rx='3' width='21.525586457271643' height='20' fill='#bf103c'/> + <rect x='302.5027864572716' rx='3' width='21.525586457271643' height='20' fill='url(#shade)'/> + <rect x='302.9772' rx='3' width='9' height='20' fill='url(#shadow)'/> + <rect x='286.9772' rx='3' width='22.0' height='20' fill='#bf103c'/> + <rect x='286.9772' rx='3' width='22.0' height='20' fill='url(#shade)'/> + <rect x='286.9772' rx='3' width='9' height='20' fill='url(#shadow)'/> + <rect x='162.7498' rx='3' width='130.2274' height='20' fill='url(#run-on-failure)'/> + <rect x='162.7498' rx='3' width='130.2274' height='20' fill='url(#shade)'/> + <rect width='168.7498' height='20' fill='#5a5a5a'/> + <rect x='-6.0' rx='3' width='174.7498' height='20' fill='url(#shade)'/> + <rect width='2' height='20' fill='url(#left-light)'/> + <rect x='614.7390658896196' width='2' height='20' fill='url(#right-shadow)'/> + <rect width='616.7390658896196' height='20' fill='url(#light)'/> + </g> + <g fill='#fff' text-anchor='middle' font-family='Verdana,Geneva,DejaVu Sans,sans-serif' text-rendering='geometricPrecision' font-size='10'> + <svg x='8.5' y='3.0' width='16.0' height='16.0' viewBox='0 0 150 150'> + <polygon fill='#402a14' fill-opacity='0.5' points='84.84 10 34.1 44.46 34.1 103.78 84.84 68.02 135.57 103.78 135.57 44.46 84.84 10'/> + <polygon fill='#402a14' fill-opacity='0.5' points='84.84 68.02 84.84 10 135.57 44.46 135.57 103.78 84.84 68.02'/> + <polygon fill='#061a29' fill-opacity='0.5' points='65.07 81.99 14.34 46.22 14.34 105.54 65.07 140 115.81 105.54 115.81 46.22 65.07 81.99'/> + <polygon fill='#061a29' fill-opacity='0.5' points='65.07 81.99 65.07 140 14.34 105.54 14.34 46.22 65.07 81.99'/> + </svg> + <svg x='8.0' y='2.0' width='16.0' height='16.0' viewBox='0 0 150 150'> + <linearGradient id='yellow-shaded' x1='91.17' y1='44.83' x2='136.24' y2='73.4' gradientUnits='userSpaceOnUse'> + <stop offset='0.01' stop-color='#c6783e'/> + <stop offset='0.54' stop-color='#ff9750'/> + </linearGradient> + <linearGradient id='blue-shaded' x1='60.71' y1='104.56' x2='-15.54' y2='63' gradientUnits='userSpaceOnUse'> + <stop offset='0' stop-color='#005a8e'/> + <stop offset='0.54' stop-color='#1a7db6'/> + </linearGradient> + <polygon fill='#ff9d4b' points='84.84 10 34.1 44.46 34.1 103.78 84.84 68.02 135.57 103.78 135.57 44.46 84.84 10'/> + <polygon fill='url(#yellow-shaded)' points='84.84 68.02 84.84 10 135.57 44.46 135.57 103.78 84.84 68.02'/> + <polygon fill='#1a7db6' points='65.07 81.99 14.34 46.22 14.34 105.54 65.07 140 115.81 105.54 115.81 46.22 65.07 81.99'/> + <polygon fill='url(#blue-shaded)' points='65.07 81.99 65.07 140 14.34 105.54 14.34 46.22 65.07 81.99'/> + </svg> + <text x='96.8749' y='14' fill='#000' fill-opacity='.3' textLength='128.7498'>tenant.application.default</text> + <text x='96.3749' y='13' fill='#fff' textLength='128.7498'>tenant.application.default</text> + <text x='231.3635' y='14' fill='#000' fill-opacity='.3' textLength='108.22739999999999'>production-us-west-1</text> + <text x='230.8635' y='13' fill='#fff' textLength='108.22739999999999'>production-us-west-1</text> + </g> +</svg> diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/overview.svg b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/overview.svg new file mode 100644 index 00000000000..0ddd91d4008 --- /dev/null +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/overview.svg @@ -0,0 +1,125 @@ +<svg xmlns='http://www.w3.org/2000/svg' width='913.3539000000001' height='20' role='img' aria-label='Deployment Status'> + <title>Deployment Status</title> + <linearGradient id='light' x2='0' y2='100%'> + <stop offset='0' stop-color='#fff' stop-opacity='.5'/> + <stop offset='.1' stop-color='#fff' stop-opacity='.15'/> + <stop offset='.9' stop-color='#000' stop-opacity='.15'/> + <stop offset='1' stop-color='#000' stop-opacity='.7'/> + </linearGradient> + <linearGradient id='left-light' x2='100%' y2='0'> + <stop offset='0' stop-color='#fff' stop-opacity='.3'/> + <stop offset='.5' stop-color='#fff' stop-opacity='.1'/> + <stop offset='1' stop-color='#fff' stop-opacity='.0'/> + </linearGradient> + <linearGradient id='right-shadow' x2='100%' y2='0'> + <stop offset='0' stop-color='#000' stop-opacity='.0'/> + <stop offset='.5' stop-color='#000' stop-opacity='.1'/> + <stop offset='1' stop-color='#000' stop-opacity='.3'/> + </linearGradient> + <linearGradient id='shadow' x2='100%' y2='0'> + <stop offset='0' stop-color='#222' stop-opacity='.4'/> + <stop offset='.5' stop-color='#555' stop-opacity='.4'/> + <stop offset='1' stop-color='#555' stop-opacity='.0'/> + </linearGradient> + <linearGradient id='shade' x2='100%' y2='0'> + <stop offset='0' stop-color='#000' stop-opacity='.20'/> + <stop offset='0.05' stop-color='#000' stop-opacity='.10'/> + <stop offset='1' stop-color='#000' stop-opacity='.0'/> + </linearGradient> + <linearGradient id='run-on-failure' x1='40%' x2='80%' y2='0%'> + <stop offset='0' stop-color='#ab83ff' /> + <stop offset='1' stop-color='#bf103c' /> + <animate attributeName='x1' values='-110%;150%;20%;-110%' dur='6s' repeatCount='indefinite' /> + <animate attributeName='x2' values='-10%;250%;120%;-10%' dur='6s' repeatCount='indefinite' /> + </linearGradient> + <linearGradient id='run-on-success' x1='40%' x2='80%' y2='0%'> + <stop offset='0' stop-color='#ab83ff' /> + <stop offset='1' stop-color='#00ff48' /> + <animate attributeName='x1' values='-110%;150%;20%;-110%' dur='6s' repeatCount='indefinite' /> + <animate attributeName='x2' values='-10%;250%;120%;-10%' dur='6s' repeatCount='indefinite' /> + </linearGradient> + <clipPath id='rounded'> + <rect width='913.3539000000001' height='20' rx='3' fill='#fff'/> + </clipPath> + <g clip-path='url(#rounded)'> + <rect x='907.3539000000001' rx='3' width='9' height='20' fill='url(#shadow)'/> + <rect x='870.3061' rx='3' width='43.0478' height='20' fill='url(#run-on-success)'/> + <polygon points='772.1412 0 772.1412 20 877.8061 20 885.8061 0' fill='#00ff48'/> + <rect x='772.1412' rx='3' width='141.2127' height='20' fill='url(#shade)'/> + <rect x='772.1412' rx='3' width='9' height='20' fill='url(#shadow)'/> + <rect x='735.0934' rx='3' width='43.0478' height='20' fill='#bf103c'/> + <polygon points='611.279 0 611.279 20 742.5934 20 750.5934 0' fill='#00ff48'/> + <rect x='611.279' rx='3' width='166.86219999999997' height='20' fill='url(#shade)'/> + <rect x='611.279' rx='3' width='9' height='20' fill='url(#shadow)'/> + <rect x='574.2312' rx='3' width='43.0478' height='20' fill='#00ff48'/> + <polygon points='449.0446 0 449.0446 20 581.7312 20 589.7312 0' fill='url(#run-on-success)'/> + <rect x='449.0446' rx='3' width='168.2344' height='20' fill='url(#shade)'/> + <rect x='449.0446' rx='3' width='9' height='20' fill='url(#shadow)'/> + <rect x='411.9968' rx='3' width='43.0478' height='20' fill='#00ff48'/> + <polygon points='314.5789 0 314.5789 20 419.4968 20 427.4968 0' fill='url(#run-on-failure)'/> + <rect x='314.5789' rx='3' width='140.4657' height='20' fill='url(#shade)'/> + <rect x='314.5789' rx='3' width='9' height='20' fill='url(#shadow)'/> + <rect x='228.3006' rx='3' width='92.2783' height='20' fill='url(#run-on-success)'/> + <rect x='238.3006' rx='3' width='82.2783' height='20' fill='url(#shade)'/> + <rect x='238.3006' rx='3' width='9' height='20' fill='url(#shadow)'/> + <rect x='152.7498' rx='3' width='91.55080000000001' height='20' fill='#00ff48'/> + <rect x='162.7498' rx='3' width='81.55080000000001' height='20' fill='url(#shade)'/> + <rect width='168.7498' height='20' fill='#5a5a5a'/> + <rect x='-6.0' rx='3' width='174.7498' height='20' fill='url(#shade)'/> + <rect width='2' height='20' fill='url(#left-light)'/> + <rect x='911.3539000000001' width='2' height='20' fill='url(#right-shadow)'/> + <rect width='913.3539000000001' height='20' fill='url(#light)'/> + </g> + <g fill='#fff' text-anchor='middle' font-family='Verdana,Geneva,DejaVu Sans,sans-serif' text-rendering='geometricPrecision' font-size='10'> + <svg x='8.5' y='3.0' width='16.0' height='16.0' viewBox='0 0 150 150'> + <polygon fill='#402a14' fill-opacity='0.5' points='84.84 10 34.1 44.46 34.1 103.78 84.84 68.02 135.57 103.78 135.57 44.46 84.84 10'/> + <polygon fill='#402a14' fill-opacity='0.5' points='84.84 68.02 84.84 10 135.57 44.46 135.57 103.78 84.84 68.02'/> + <polygon fill='#061a29' fill-opacity='0.5' points='65.07 81.99 14.34 46.22 14.34 105.54 65.07 140 115.81 105.54 115.81 46.22 65.07 81.99'/> + <polygon fill='#061a29' fill-opacity='0.5' points='65.07 81.99 65.07 140 14.34 105.54 14.34 46.22 65.07 81.99'/> + </svg> + <svg x='8.0' y='2.0' width='16.0' height='16.0' viewBox='0 0 150 150'> + <linearGradient id='yellow-shaded' x1='91.17' y1='44.83' x2='136.24' y2='73.4' gradientUnits='userSpaceOnUse'> + <stop offset='0.01' stop-color='#c6783e'/> + <stop offset='0.54' stop-color='#ff9750'/> + </linearGradient> + <linearGradient id='blue-shaded' x1='60.71' y1='104.56' x2='-15.54' y2='63' gradientUnits='userSpaceOnUse'> + <stop offset='0' stop-color='#005a8e'/> + <stop offset='0.54' stop-color='#1a7db6'/> + </linearGradient> + <polygon fill='#ff9d4b' points='84.84 10 34.1 44.46 34.1 103.78 84.84 68.02 135.57 103.78 135.57 44.46 84.84 10'/> + <polygon fill='url(#yellow-shaded)' points='84.84 68.02 84.84 10 135.57 44.46 135.57 103.78 84.84 68.02'/> + <polygon fill='#1a7db6' points='65.07 81.99 14.34 46.22 14.34 105.54 65.07 140 115.81 105.54 115.81 46.22 65.07 81.99'/> + <polygon fill='url(#blue-shaded)' points='65.07 81.99 65.07 140 14.34 105.54 14.34 46.22 65.07 81.99'/> + </svg> + <text x='96.8749' y='14' fill='#000' fill-opacity='.3' textLength='128.7498'>tenant.application.default</text> + <text x='96.3749' y='13' fill='#fff' textLength='128.7498'>tenant.application.default</text> + <text x='207.02519999999998' y='14' fill='#000' fill-opacity='.3' textLength='59.5508'>system-test</text> + <text x='206.52519999999998' y='13' fill='#fff' textLength='59.5508'>system-test</text> + <text x='282.93975' y='14' fill='#000' fill-opacity='.3' textLength='60.2783'>staging-test</text> + <text x='282.43975' y='13' fill='#fff' textLength='60.2783'>staging-test</text> + <text x='354.21315' y='14' fill='#000' fill-opacity='.3' textLength='50.2685'>us-west-1</text> + <text x='353.71315' y='13' fill='#fff' textLength='50.2685'>us-west-1</text> + <text x='403.9221' y='14' fill='#000' fill-opacity='.3' textLength='33.1494'>deploy</text> + <text x='403.4221' y='13' fill='#fff' textLength='33.1494'>deploy</text> + <text x='438.02070000000003' y='14' fill='#000' fill-opacity='.3' textLength='19.047800000000002'>test</text> + <text x='437.52070000000003' y='13' fill='#fff' textLength='19.047800000000002'>test</text> + <text x='502.5632' y='14' fill='#000' fill-opacity='.3' textLength='78.0372'>aws-us-east-1a</text> + <text x='502.0632' y='13' fill='#fff' textLength='78.0372'>aws-us-east-1a</text> + <text x='566.1565' y='14' fill='#000' fill-opacity='.3' textLength='33.1494'>deploy</text> + <text x='565.6565' y='13' fill='#fff' textLength='33.1494'>deploy</text> + <text x='600.2551' y='14' fill='#000' fill-opacity='.3' textLength='19.047800000000002'>test</text> + <text x='599.7551' y='13' fill='#fff' textLength='19.047800000000002'>test</text> + <text x='664.1115' y='14' fill='#000' fill-opacity='.3' textLength='76.66499999999999'>ap-southeast-1</text> + <text x='663.6115' y='13' fill='#fff' textLength='76.66499999999999'>ap-southeast-1</text> + <text x='727.0187' y='14' fill='#000' fill-opacity='.3' textLength='33.1494'>deploy</text> + <text x='726.5187' y='13' fill='#fff' textLength='33.1494'>deploy</text> + <text x='761.1173' y='14' fill='#000' fill-opacity='.3' textLength='19.047800000000002'>test</text> + <text x='760.6173' y='13' fill='#fff' textLength='19.047800000000002'>test</text> + <text x='812.14895' y='14' fill='#000' fill-opacity='.3' textLength='51.015499999999996'>eu-west-1</text> + <text x='811.64895' y='13' fill='#fff' textLength='51.015499999999996'>eu-west-1</text> + <text x='862.2314' y='14' fill='#000' fill-opacity='.3' textLength='33.1494'>deploy</text> + <text x='861.7314' y='13' fill='#fff' textLength='33.1494'>deploy</text> + <text x='896.33' y='14' fill='#000' fill-opacity='.3' textLength='19.047800000000002'>test</text> + <text x='895.83' y='13' fill='#fff' textLength='19.047800000000002'>test</text> + </g> +</svg> |