summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJon Marius Venstad <jonmv@users.noreply.github.com>2022-04-26 11:25:40 +0200
committerGitHub <noreply@github.com>2022-04-26 11:25:40 +0200
commit7f94a8bf1c618c71470485607bdc38d7aa1d970d (patch)
tree7429531dd29f8dfa41c741b04deb8775006ffc83
parent137e28c94c9a6b22309abf803b69dae746cf17de (diff)
parent87e5304bd2e41279f37ce670132ac8cb36222029 (diff)
Merge pull request #22260 from vespa-engine/jonmv/add-test-categories-header
Move test package logic to separate class, and add validation
-rw-r--r--config-application-package/src/main/java/com/yahoo/config/model/application/provider/Bundle.java8
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java41
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackage.java6
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/TestPackage.java318
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ZipEntries.java23
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java149
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java15
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notification.java7
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java1
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.java1
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java26
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageTest.java36
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/TestPackageTest.java157
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunnerTest.java66
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelperTest.java3
-rw-r--r--vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/JunitRunner.java2
17 files changed, 609 insertions, 252 deletions
diff --git a/config-application-package/src/main/java/com/yahoo/config/model/application/provider/Bundle.java b/config-application-package/src/main/java/com/yahoo/config/model/application/provider/Bundle.java
index 721f3fd3460..c4fb7c29e7f 100644
--- a/config-application-package/src/main/java/com/yahoo/config/model/application/provider/Bundle.java
+++ b/config-application-package/src/main/java/com/yahoo/config/model/application/provider/Bundle.java
@@ -56,13 +56,7 @@ public class Bundle {
if (!bundleDir.isDirectory()) {
return new ArrayList<>();
}
- return Arrays.asList(bundleDir.listFiles(
- new FilenameFilter() {
- @Override
- public boolean accept(File dir, String name) {
- return name.endsWith(".jar");
- }
- }));
+ return Arrays.asList(bundleDir.listFiles((dir, name) -> name.endsWith(".jar")));
}
public List<DefEntry> getDefEntries() {
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 613e3413ae8..6907747646e 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
@@ -93,6 +93,8 @@ import java.util.Optional;
import java.util.OptionalInt;
import java.util.Set;
import java.util.TreeMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.logging.Level;
@@ -471,8 +473,10 @@ public class ApplicationController {
RevisionId revision = run.versions().sourceRevision().filter(__ -> deploySourceVersions).orElse(run.versions().targetRevision());
ApplicationPackage applicationPackage = new ApplicationPackage(applicationStore.get(deployment, revision));
+ AtomicReference<RevisionId> lastRevision = new AtomicReference<>();
try (Mutex lock = lock(applicationId)) {
LockedApplication application = new LockedApplication(requireApplication(applicationId), lock);
+ application.get().revisions().last().map(ApplicationVersion::id).ifPresent(lastRevision::set);
Instance instance = application.get().require(job.application().instance());
if ( ! applicationPackage.trustedCertificates().isEmpty()
@@ -492,22 +496,25 @@ public class ApplicationController {
var quotaUsage = deploymentQuotaUsage(zone, job.application());
// For direct deployments use the full deployment ID, but otherwise use just the tenant and application as
- // the source since it's the same application, so it should have the same warnings
- NotificationSource source = zone.environment().isManuallyDeployed() ?
- NotificationSource.from(deployment) : NotificationSource.from(applicationId);
-
- @SuppressWarnings("deprecation")
- List<String> warnings = Optional.ofNullable(result.prepareResponse().log)
- .map(logs -> logs.stream()
- .filter(log -> log.applicationPackage)
- .filter(log -> LogLevel.parse(log.level).intValue() >= Level.WARNING.intValue())
- .map(log -> log.message)
- .sorted()
- .distinct()
- .collect(Collectors.toList()))
- .orElseGet(List::of);
- if (warnings.isEmpty()) controller.notificationsDb().removeNotification(source, Notification.Type.applicationPackage);
- else controller.notificationsDb().setNotification(source, Notification.Type.applicationPackage, Notification.Level.warning, warnings);
+ // the source since it's the same application, so it should have the same warnings.
+ // These notifications are only updated when the last submitted revision is deployed here.
+ NotificationSource source = zone.environment().isManuallyDeployed()
+ ? NotificationSource.from(deployment)
+ : revision.equals(lastRevision.get()) ? NotificationSource.from(applicationId) : null;
+ if (source != null) {
+ @SuppressWarnings("deprecation")
+ List<String> warnings = Optional.ofNullable(result.prepareResponse().log)
+ .map(logs -> logs.stream()
+ .filter(log -> log.applicationPackage)
+ .filter(log -> LogLevel.parse(log.level).intValue() >= Level.WARNING.intValue())
+ .map(log -> log.message)
+ .sorted()
+ .distinct()
+ .collect(Collectors.toList()))
+ .orElseGet(List::of);
+ if (warnings.isEmpty()) controller.notificationsDb().removeNotification(source, Notification.Type.applicationPackage);
+ else controller.notificationsDb().setNotification(source, Notification.Type.applicationPackage, Notification.Level.warning, warnings);
+ }
lockApplicationOrThrow(applicationId, application ->
store(application.with(job.application().instance(),
@@ -542,7 +549,7 @@ public class ApplicationController {
controller.jobController().deploymentStatus(application.get());
for (Notification notification : controller.notificationsDb().listNotifications(NotificationSource.from(application.get().id()), true)) {
- if ( ! notification.source().instance().map(declaredInstances::contains).orElse(false))
+ if ( ! notification.source().instance().map(declaredInstances::contains).orElse(true))
controller.notificationsDb().removeNotifications(notification.source());
if (notification.source().instance().isPresent() &&
! notification.source().zoneId().map(application.get().require(notification.source().instance().get()).deployments()::containsKey).orElse(false))
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackage.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackage.java
index ed794a9d929..0d39703b70d 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackage.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackage.java
@@ -66,9 +66,9 @@ public class ApplicationPackage {
private static final String trustedCertificatesFile = "security/clients.pem";
private static final String buildMetaFile = "build-meta.json";
- private static final String deploymentFile = "deployment.xml";
+ static final String deploymentFile = "deployment.xml";
private static final String validationOverridesFile = "validation-overrides.xml";
- private static final String servicesFile = "services.xml";
+ static final String servicesFile = "services.xml";
private final String contentHash;
private final String bundleHash;
@@ -212,7 +212,7 @@ public class ApplicationPackage {
entry -> entry.getValue().get())));
}
- static byte[] filesZip(Map<String, byte[]> files) {
+ public static byte[] filesZip(Map<String, byte[]> files) {
try (ZipBuilder zipBuilder = new ZipBuilder(files.values().stream().mapToInt(bytes -> bytes.length).sum() + 512)) {
files.forEach(zipBuilder::add);
zipBuilder.close();
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/TestPackage.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/TestPackage.java
new file mode 100644
index 00000000000..fb352848911
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/TestPackage.java
@@ -0,0 +1,318 @@
+package com.yahoo.vespa.hosted.controller.application.pkg;
+
+import com.yahoo.config.application.api.DeploymentInstanceSpec;
+import com.yahoo.config.application.api.DeploymentSpec;
+import com.yahoo.config.application.api.DeploymentSpec.Step;
+import com.yahoo.config.provision.AthenzDomain;
+import com.yahoo.config.provision.AthenzService;
+import com.yahoo.config.provision.Environment;
+import com.yahoo.config.provision.NodeResources;
+import com.yahoo.config.provision.zone.ZoneId;
+import com.yahoo.path.Path;
+import com.yahoo.security.KeyAlgorithm;
+import com.yahoo.security.KeyUtils;
+import com.yahoo.security.SignatureAlgorithm;
+import com.yahoo.security.X509CertificateBuilder;
+import com.yahoo.security.X509CertificateUtils;
+import com.yahoo.text.Text;
+import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
+import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud.Suite;
+import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterId;
+import com.yahoo.vespa.hosted.controller.config.ControllerConfig;
+import com.yahoo.vespa.hosted.controller.config.ControllerConfig.Steprunner.Testerapp;
+import com.yahoo.yolean.Exceptions;
+
+import javax.security.auth.x500.X500Principal;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.math.BigInteger;
+import java.security.KeyPair;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.jar.JarInputStream;
+import java.util.jar.Manifest;
+import java.util.regex.Pattern;
+
+import static com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud.Suite.production;
+import static com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud.Suite.staging;
+import static com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud.Suite.staging_setup;
+import static com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud.Suite.system;
+import static com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage.deploymentFile;
+import static com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage.servicesFile;
+import static com.yahoo.vespa.hosted.controller.application.pkg.ZipEntries.transferAndWrite;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.stream.Collectors.groupingBy;
+import static java.util.stream.Collectors.mapping;
+import static java.util.stream.Collectors.toList;
+
+/**
+ * Validation and manipulation of test package.
+ *
+ * @author jonmv
+ */
+public class TestPackage {
+
+ // Must match exactly the advertised resources of an AWS instance type. Also consider that the container
+ // will have ~1.8 GB less memory than equivalent resources in AWS (VESPA-16259).
+ static final NodeResources DEFAULT_TESTER_RESOURCES_AWS = new NodeResources(2, 8, 50, 0.3, NodeResources.DiskSpeed.any);
+ static final NodeResources DEFAULT_TESTER_RESOURCES = new NodeResources(1, 4, 50, 0.3, NodeResources.DiskSpeed.any);
+
+ private final ApplicationPackage applicationPackage;
+ private final X509Certificate certificate;
+
+ public TestPackage(byte[] testPackage, boolean isPublicSystem, RunId id, Testerapp testerApp,
+ DeploymentSpec spec, Instant certificateValidFrom, Duration certificateValidDuration) {
+
+ // Copy contents of submitted application-test.zip, and ensure required directories exist within the zip.
+ Map<String, byte[]> entries = new HashMap<>();
+ entries.put("artifacts/.ignore-" + UUID.randomUUID(), new byte[0]);
+ entries.put("tests/.ignore-" + UUID.randomUUID(), new byte[0]);
+
+ entries.put(servicesFile,
+ servicesXml( ! isPublicSystem,
+ certificateValidFrom != null,
+ testerResourcesFor(id.type().zone(), spec.requireInstance(id.application().instance())),
+ testerApp));
+
+ entries.put(deploymentFile,
+ deploymentXml(id.tester(),
+ spec.athenzDomain(),
+ spec.requireInstance(id.application().instance())
+ .athenzService(id.type().zone().environment(), id.type().zone().region())));
+
+ if (certificateValidFrom != null) {
+ KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.RSA, 2048);
+ X500Principal subject = new X500Principal("CN=" + id.tester().id().toFullString() + "." + id.type() + "." + id.number());
+ this.certificate = X509CertificateBuilder.fromKeypair(keyPair,
+ subject,
+ certificateValidFrom,
+ certificateValidFrom.plus(certificateValidDuration),
+ SignatureAlgorithm.SHA512_WITH_RSA,
+ BigInteger.valueOf(1))
+ .build();
+ entries.put("artifacts/key", KeyUtils.toPem(keyPair.getPrivate()).getBytes(UTF_8));
+ entries.put("artifacts/cert", X509CertificateUtils.toPem(certificate).getBytes(UTF_8));
+ }
+ else {
+ this.certificate = null;
+ }
+
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream(testPackage.length + 10_000);
+ transferAndWrite(buffer, new ByteArrayInputStream(testPackage), entries);
+ this.applicationPackage = new ApplicationPackage(buffer.toByteArray());
+ }
+
+ public ApplicationPackage asApplicationPackage() {
+ return applicationPackage;
+ }
+
+ public X509Certificate certificate() {
+ return Objects.requireNonNull(certificate);
+ }
+
+ public static TestSummary validateTests(DeploymentSpec spec, byte[] testPackage) {
+ return validateTests(expectedSuites(spec.steps()), testPackage);
+ }
+
+ static TestSummary validateTests(Collection<Suite> expectedSuites, byte[] testPackage) {
+ List<String> problems = new ArrayList<>();
+ Set<Suite> suites = new LinkedHashSet<>();
+ ZipEntries.from(testPackage, __ -> true, 0, false).asList().stream()
+ .map(entry -> Path.fromString(entry.name()))
+ .collect(groupingBy(path -> path.elements().size() > 1 ? path.elements().get(0) : "",
+ mapping(path -> (path.elements().size() > 1 ? path.getChildPath() : path).getRelative(), toList())))
+ .forEach((directory, paths) -> {
+ switch (directory) {
+ case "components": {
+ for (String path : paths) {
+ if (path.endsWith("-tests.jar")) {
+ try {
+ byte[] testsJar = ZipEntries.readFile(testPackage, "components/" + path, 1 << 30);
+ Manifest manifest = new JarInputStream(new ByteArrayInputStream(testsJar)).getManifest();
+ for (String suite : manifest.getMainAttributes().getValue("X-JDisc-Test-Bundle-Categories").split(","))
+ switch (suite.trim()) {
+ case "SystemTest": suites.add(system); break;
+ case "StagingSetup": suites.add(staging_setup); break;
+ case "StagingTest": suites.add(staging); break;
+ case "ProductionTest": suites.add(production); break;
+ default: problems.add("unexpected test suite name '" + suite + "' in bundle manifest");
+ }
+ }
+ catch (Exception e) {
+ problems.add("failed reading test bundle manifest: " + Exceptions.toMessageString(e));
+ }
+ }
+ }
+ }
+ break;
+ case "tests": {
+ if (paths.stream().anyMatch(Pattern.compile("system-test/.+\\.json").asMatchPredicate())) suites.add(system);
+ if (paths.stream().anyMatch(Pattern.compile("staging-setup/.+\\.json").asMatchPredicate())) suites.add(staging_setup);
+ if (paths.stream().anyMatch(Pattern.compile("staging-test/.+\\.json").asMatchPredicate())) suites.add(staging);
+ if (paths.stream().anyMatch(Pattern.compile("production-test/.+\\.json").asMatchPredicate())) suites.add(production);
+ }
+ break;
+ case "artifacts": {
+ if (paths.stream().anyMatch(Pattern.compile(".+-tests.jar").asMatchPredicate()))
+ suites.addAll(expectedSuites); // ಠ_ಠ
+
+ for (String forbidden : List.of("key", "cert"))
+ if (paths.contains(forbidden))
+ problems.add("test package contains 'artifacts/" + forbidden +
+ "'; this conflicts with credentials used to run tests in Vespa Cloud");
+ }
+ break;
+ }
+ });
+
+ if (expectedSuites.contains(system) && ! suites.contains(system))
+ problems.add("test package has no system tests, but <test /> is declared in deployment.xml");
+
+ if (suites.contains(staging) != suites.contains(staging_setup))
+ problems.add("test package has " + (suites.contains(staging) ? "staging tests" : "staging setup") +
+ ", so it should also include " + (suites.contains(staging) ? "staging setup" : "staging tests"));
+ else if (expectedSuites.contains(staging) && ! suites.contains(staging))
+ problems.add("test package has no staging setup and tests, but <staging /> is declared in deployment.xml");
+
+ if (suites.contains(production) != expectedSuites.contains(production))
+ problems.add("test package has " + (suites.contains(production) ? "" : "no ") + "production tests, " +
+ "but " + (suites.contains(production) ? "no " : "") + "production tests are declared in deployment.xml");
+
+ if ( ! problems.isEmpty())
+ problems.add("see https://docs.vespa.ai/en/testing.html for details on how to write system tests for Vespa");
+
+ return new TestSummary(problems, suites);
+ }
+
+ public static NodeResources testerResourcesFor(ZoneId zone, DeploymentInstanceSpec spec) {
+ NodeResources nodeResources = spec.steps().stream()
+ .filter(step -> step.concerns(zone.environment()))
+ .findFirst()
+ .flatMap(step -> step.zones().get(0).testerFlavor())
+ .map(NodeResources::fromLegacyName)
+ .orElse(zone.region().value().contains("aws-") ? DEFAULT_TESTER_RESOURCES_AWS
+ : DEFAULT_TESTER_RESOURCES);
+ return nodeResources.with(NodeResources.DiskSpeed.any);
+ }
+
+ /** Returns the generated services.xml content for the tester application. */
+ public static byte[] servicesXml(boolean systemUsesAthenz, boolean useTesterCertificate,
+ NodeResources resources, ControllerConfig.Steprunner.Testerapp config) {
+ int jdiscMemoryGb = 2; // 2Gb memory for tester application (excessive?).
+ int jdiscMemoryPct = (int) Math.ceil(100 * jdiscMemoryGb / resources.memoryGb());
+
+ // Of the remaining memory, split 50/50 between Surefire running the tests and the rest
+ int testMemoryMb = (int) (1024 * (resources.memoryGb() - jdiscMemoryGb) / 2);
+
+ String resourceString = Text.format("<resources vcpu=\"%.2f\" memory=\"%.2fGb\" disk=\"%.2fGb\" disk-speed=\"%s\" storage-type=\"%s\"/>",
+ resources.vcpu(), resources.memoryGb(), resources.diskGb(), resources.diskSpeed().name(), resources.storageType().name());
+
+ String runtimeProviderClass = config.runtimeProviderClass();
+ String tenantCdBundle = config.tenantCdBundle();
+
+ String servicesXml =
+ "<?xml version='1.0' encoding='UTF-8'?>\n" +
+ "<services xmlns:deploy='vespa' version='1.0'>\n" +
+ " <container version='1.0' id='tester'>\n" +
+ "\n" +
+ " <component id=\"com.yahoo.vespa.hosted.testrunner.TestRunner\" bundle=\"vespa-testrunner-components\">\n" +
+ " <config name=\"com.yahoo.vespa.hosted.testrunner.test-runner\">\n" +
+ " <artifactsPath>artifacts</artifactsPath>\n" +
+ " <surefireMemoryMb>" + testMemoryMb + "</surefireMemoryMb>\n" +
+ " <useAthenzCredentials>" + systemUsesAthenz + "</useAthenzCredentials>\n" +
+ " <useTesterCertificate>" + useTesterCertificate + "</useTesterCertificate>\n" +
+ " </config>\n" +
+ " </component>\n" +
+ "\n" +
+ " <handler id=\"com.yahoo.vespa.testrunner.TestRunnerHandler\" bundle=\"vespa-osgi-testrunner\">\n" +
+ " <binding>http://*/tester/v1/*</binding>\n" +
+ " </handler>\n" +
+ "\n" +
+ " <component id=\"" + runtimeProviderClass + "\" bundle=\"" + tenantCdBundle + "\" />\n" +
+ "\n" +
+ " <component id=\"com.yahoo.vespa.testrunner.JunitRunner\" bundle=\"vespa-osgi-testrunner\">\n" +
+ " <config name=\"com.yahoo.vespa.testrunner.junit-test-runner\">\n" +
+ " <artifactsPath>artifacts</artifactsPath>\n" +
+ " <useAthenzCredentials>" + systemUsesAthenz + "</useAthenzCredentials>\n" +
+ " </config>\n" +
+ " </component>\n" +
+ "\n" +
+ " <component id=\"com.yahoo.vespa.testrunner.VespaCliTestRunner\" bundle=\"vespa-osgi-testrunner\">\n" +
+ " <config name=\"com.yahoo.vespa.testrunner.vespa-cli-test-runner\">\n" +
+ " <artifactsPath>artifacts</artifactsPath>\n" +
+ " <testsPath>tests</testsPath>\n" +
+ " <useAthenzCredentials>" + systemUsesAthenz + "</useAthenzCredentials>\n" +
+ " </config>\n" +
+ " </component>\n" +
+ "\n" +
+ " <nodes count=\"1\">\n" +
+ " <jvm allocated-memory=\"" + jdiscMemoryPct + "%\"/>\n" +
+ " " + resourceString + "\n" +
+ " </nodes>\n" +
+ " </container>\n" +
+ "</services>\n";
+
+ return servicesXml.getBytes(UTF_8);
+ }
+
+ /** Returns a dummy deployment xml which sets up the service identity for the tester, if present. */
+ public static byte[] deploymentXml(TesterId id, Optional<AthenzDomain> athenzDomain, Optional<AthenzService> athenzService) {
+ String deploymentSpec =
+ "<?xml version='1.0' encoding='UTF-8'?>\n" +
+ "<deployment version=\"1.0\" " +
+ athenzDomain.map(domain -> "athenz-domain=\"" + domain.value() + "\" ").orElse("") +
+ athenzService.map(service -> "athenz-service=\"" + service.value() + "\" ").orElse("") + ">" +
+ " <instance id=\"" + id.id().instance().value() + "\" />" +
+ "</deployment>";
+ return deploymentSpec.getBytes(UTF_8);
+ }
+
+ static Set<Suite> expectedSuites(List<Step> steps) {
+ Set<Suite> suites = new HashSet<>();
+ if (steps.isEmpty()) return suites;
+ for (Step step : steps) {
+ if (step.isTest()) {
+ if (step.concerns(Environment.prod)) suites.add(production);
+ if (step.concerns(Environment.test)) suites.add(system);
+ if (step.concerns(Environment.staging)) { suites.add(staging); suites.add(staging_setup); }
+ }
+ else
+ suites.addAll(expectedSuites(step.steps()));
+ }
+ return suites;
+ }
+
+
+ public static class TestSummary {
+
+ private final List<String> problems;
+ private final List<Suite> suites;
+
+ public TestSummary(List<String> problems, Set<Suite> suites) {
+ this.problems = List.copyOf(problems);
+ this.suites = List.copyOf(suites);
+ }
+
+ public List<String> problems() {
+ return problems;
+ }
+
+ public List<Suite> suites() {
+ return suites;
+ }
+
+ }
+
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ZipEntries.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ZipEntries.java
index a6cb7f23fc3..8392a77bad5 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ZipEntries.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ZipEntries.java
@@ -13,6 +13,8 @@ import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;
@@ -35,19 +37,28 @@ public class ZipEntries {
/** Copies the zipped content from in to out, adding/overwriting an entry with the given name and content. */
public static void transferAndWrite(OutputStream out, InputStream in, String name, byte[] content) {
+ transferAndWrite(out, in, Map.of(name, content));
+ }
+
+ /** Copies the zipped content from in to out, adding/overwriting/removing (on {@code null}) entries as specified. */
+ public static void transferAndWrite(OutputStream out, InputStream in, Map<String, byte[]> entries) {
try (ZipOutputStream zipOut = new ZipOutputStream(out);
ZipInputStream zipIn = new ZipInputStream(in)) {
for (ZipEntry entry = zipIn.getNextEntry(); entry != null; entry = zipIn.getNextEntry()) {
- if (entry.getName().equals(name))
+ if (entries.containsKey(entry.getName()))
continue;
zipOut.putNextEntry(new ZipEntry(entry.getName()));
zipIn.transferTo(zipOut);
zipOut.closeEntry();
}
- zipOut.putNextEntry(new ZipEntry(name));
- zipOut.write(content);
- zipOut.closeEntry();
+ for (Entry<String, byte[]> entry : entries.entrySet()) {
+ if (entry.getValue() != null) {
+ zipOut.putNextEntry(new ZipEntry(entry.getKey()));
+ zipOut.write(entry.getValue());
+ zipOut.closeEntry();
+ }
+ }
}
catch (IOException e) {
throw new UncheckedIOException(e);
@@ -76,6 +87,10 @@ public class ZipEntries {
return new ZipEntries(entries);
}
+ public static byte[] readFile(byte[] zip, String name, int maxEntrySizeInBytes) {
+ return from(zip, name::equals, maxEntrySizeInBytes, true).asList().get(0).contentOrThrow();
+ }
+
public List<ZipEntryWithContent> asList() { return entries; }
public static class ZipEntryWithContent {
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java
index 9e551c7ce78..52e5431b552 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java
@@ -3,16 +3,12 @@ package com.yahoo.vespa.hosted.controller.deployment;
import ai.vespa.http.DomainName;
import com.yahoo.component.Version;
-import com.yahoo.config.application.api.DeploymentInstanceSpec;
import com.yahoo.config.application.api.DeploymentSpec;
import com.yahoo.config.application.api.Notifications;
import com.yahoo.config.application.api.Notifications.When;
import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.AthenzDomain;
-import com.yahoo.config.provision.AthenzService;
import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.NodeResources;
import com.yahoo.config.provision.SystemName;
import com.yahoo.config.provision.zone.RoutingMethod;
import com.yahoo.config.provision.zone.ZoneId;
@@ -22,7 +18,6 @@ import com.yahoo.security.KeyUtils;
import com.yahoo.security.SignatureAlgorithm;
import com.yahoo.security.X509CertificateBuilder;
import com.yahoo.security.X509CertificateUtils;
-import com.yahoo.text.Text;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.Instance;
@@ -38,7 +33,6 @@ import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterId;
import com.yahoo.vespa.hosted.controller.api.integration.organization.DeploymentFailureMails;
import com.yahoo.vespa.hosted.controller.api.integration.organization.Mail;
import com.yahoo.vespa.hosted.controller.application.ActivateResult;
@@ -46,7 +40,7 @@ import com.yahoo.vespa.hosted.controller.application.Deployment;
import com.yahoo.vespa.hosted.controller.application.Endpoint;
import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
-import com.yahoo.vespa.hosted.controller.config.ControllerConfig;
+import com.yahoo.vespa.hosted.controller.application.pkg.TestPackage;
import com.yahoo.vespa.hosted.controller.maintenance.JobRunner;
import com.yahoo.vespa.hosted.controller.notification.Notification;
import com.yahoo.vespa.hosted.controller.notification.NotificationSource;
@@ -124,13 +118,6 @@ public class InternalStepRunner implements StepRunner {
private static final Logger logger = Logger.getLogger(InternalStepRunner.class.getName());
- static final NodeResources DEFAULT_TESTER_RESOURCES =
- new NodeResources(1, 4, 50, 0.3, NodeResources.DiskSpeed.any);
- // Must match exactly the advertised resources of an AWS instance type. Also consider that the container
- // will have ~1.8 GB less memory than equivalent resources in AWS (VESPA-16259).
- static final NodeResources DEFAULT_TESTER_RESOURCES_AWS =
- new NodeResources(2, 8, 50, 0.3, NodeResources.DiskSpeed.any);
-
private final Controller controller;
private final TestConfigSerializer testConfigSerializer;
private final DeploymentFailureMails mails;
@@ -907,48 +894,20 @@ public class InternalStepRunner implements StepRunner {
private ApplicationPackage testerPackage(RunId id) {
RevisionId revision = controller.jobController().run(id).get().versions().targetRevision();
DeploymentSpec spec = controller.applications().requireApplication(TenantAndApplicationId.from(id.application())).deploymentSpec();
-
- ZoneId zone = id.type().zone();
+ byte[] testZip = controller.applications().applicationStore().getTester(id.application().tenant(),
+ id.application().application(), revision);
boolean useTesterCertificate = useTesterCertificate(id);
- byte[] servicesXml = servicesXml( ! controller.system().isPublic(),
- useTesterCertificate,
- testerResourcesFor(zone, spec.requireInstance(id.application().instance())),
- controller.controllerConfig().steprunner().testerapp());
- byte[] testPackage = controller.applications().applicationStore().getTester(id.application().tenant(), id.application().application(), revision);
- byte[] deploymentXml = deploymentXml(id.tester(),
- spec.athenzDomain(),
- spec.requireInstance(id.application().instance()).athenzService(zone.environment(), zone.region()));
-
- try (ZipBuilder zipBuilder = new ZipBuilder(testPackage.length + servicesXml.length + deploymentXml.length + 1000)) {
- // Copy contents of submitted application-test.zip, and ensure required directories exist within the zip.
- zipBuilder.add(testPackage);
- zipBuilder.add("artifacts/.ignore-" + UUID.randomUUID(), new byte[0]);
- zipBuilder.add("tests/.ignore-" + UUID.randomUUID(), new byte[0]);
-
- zipBuilder.add("services.xml", servicesXml);
- zipBuilder.add("deployment.xml", deploymentXml);
- if (useTesterCertificate)
- appendAndStoreCertificate(zipBuilder, id);
-
- zipBuilder.close();
- return new ApplicationPackage(zipBuilder.toByteArray());
- }
- }
+ TestPackage testPackage = new TestPackage(testZip,
+ controller.system().isPublic(),
+ id,
+ controller.controllerConfig().steprunner().testerapp(),
+ spec,
+ useTesterCertificate ? controller.clock().instant() : null,
+ timeouts.testerCertificate());
+ if (useTesterCertificate) controller.jobController().storeTesterCertificate(id, testPackage.certificate());
- private void appendAndStoreCertificate(ZipBuilder zipBuilder, RunId id) {
- KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.RSA, 2048);
- X500Principal subject = new X500Principal("CN=" + id.tester().id().toFullString() + "." + id.type() + "." + id.number());
- X509Certificate certificate = X509CertificateBuilder.fromKeypair(keyPair,
- subject,
- controller.clock().instant(),
- controller.clock().instant().plus(timeouts.testerCertificate()),
- SignatureAlgorithm.SHA512_WITH_RSA,
- BigInteger.valueOf(1))
- .build();
- controller.jobController().storeTesterCertificate(id, certificate);
- zipBuilder.add("artifacts/key", KeyUtils.toPem(keyPair.getPrivate()).getBytes(UTF_8));
- zipBuilder.add("artifacts/cert", X509CertificateUtils.toPem(certificate).getBytes(UTF_8));
+ return testPackage.asApplicationPackage();
}
private DeploymentId getTesterDeploymentId(RunId runId) {
@@ -956,90 +915,6 @@ public class InternalStepRunner implements StepRunner {
return new DeploymentId(runId.tester().id(), zoneId);
}
- static NodeResources testerResourcesFor(ZoneId zone, DeploymentInstanceSpec spec) {
- NodeResources nodeResources = spec.steps().stream()
- .filter(step -> step.concerns(zone.environment()))
- .findFirst()
- .flatMap(step -> step.zones().get(0).testerFlavor())
- .map(NodeResources::fromLegacyName)
- .orElse(zone.region().value().contains("aws-") ?
- DEFAULT_TESTER_RESOURCES_AWS : DEFAULT_TESTER_RESOURCES);
- return nodeResources.with(NodeResources.DiskSpeed.any);
- }
-
- /** Returns the generated services.xml content for the tester application. */
- static byte[] servicesXml(boolean systemUsesAthenz, boolean useTesterCertificate,
- NodeResources resources, ControllerConfig.Steprunner.Testerapp config) {
- int jdiscMemoryGb = 2; // 2Gb memory for tester application (excessive?).
- int jdiscMemoryPct = (int) Math.ceil(100 * jdiscMemoryGb / resources.memoryGb());
-
- // Of the remaining memory, split 50/50 between Surefire running the tests and the rest
- int testMemoryMb = (int) (1024 * (resources.memoryGb() - jdiscMemoryGb) / 2);
-
- String resourceString = Text.format(
- "<resources vcpu=\"%.2f\" memory=\"%.2fGb\" disk=\"%.2fGb\" disk-speed=\"%s\" storage-type=\"%s\"/>",
- resources.vcpu(), resources.memoryGb(), resources.diskGb(), resources.diskSpeed().name(), resources.storageType().name());
-
- String runtimeProviderClass = config.runtimeProviderClass();
- String tenantCdBundle = config.tenantCdBundle();
-
- String servicesXml =
- "<?xml version='1.0' encoding='UTF-8'?>\n" +
- "<services xmlns:deploy='vespa' version='1.0'>\n" +
- " <container version='1.0' id='tester'>\n" +
- "\n" +
- " <component id=\"com.yahoo.vespa.hosted.testrunner.TestRunner\" bundle=\"vespa-testrunner-components\">\n" +
- " <config name=\"com.yahoo.vespa.hosted.testrunner.test-runner\">\n" +
- " <artifactsPath>artifacts</artifactsPath>\n" +
- " <surefireMemoryMb>" + testMemoryMb + "</surefireMemoryMb>\n" +
- " <useAthenzCredentials>" + systemUsesAthenz + "</useAthenzCredentials>\n" +
- " <useTesterCertificate>" + useTesterCertificate + "</useTesterCertificate>\n" +
- " </config>\n" +
- " </component>\n" +
- "\n" +
- " <handler id=\"com.yahoo.vespa.testrunner.TestRunnerHandler\" bundle=\"vespa-osgi-testrunner\">\n" +
- " <binding>http://*/tester/v1/*</binding>\n" +
- " </handler>\n" +
- "\n" +
- " <component id=\"" + runtimeProviderClass + "\" bundle=\"" + tenantCdBundle + "\" />\n" +
- "\n" +
- " <component id=\"com.yahoo.vespa.testrunner.JunitRunner\" bundle=\"vespa-osgi-testrunner\">\n" +
- " <config name=\"com.yahoo.vespa.testrunner.junit-test-runner\">\n" +
- " <artifactsPath>artifacts</artifactsPath>\n" +
- " <useAthenzCredentials>" + systemUsesAthenz + "</useAthenzCredentials>\n" +
- " </config>\n" +
- " </component>\n" +
- "\n" +
- " <component id=\"com.yahoo.vespa.testrunner.VespaCliTestRunner\" bundle=\"vespa-osgi-testrunner\">\n" +
- " <config name=\"com.yahoo.vespa.testrunner.vespa-cli-test-runner\">\n" +
- " <artifactsPath>artifacts</artifactsPath>\n" +
- " <testsPath>tests</testsPath>\n" +
- " <useAthenzCredentials>" + systemUsesAthenz + "</useAthenzCredentials>\n" +
- " </config>\n" +
- " </component>\n" +
- "\n" +
- " <nodes count=\"1\">\n" +
- " <jvm allocated-memory=\"" + jdiscMemoryPct + "%\"/>\n" +
- " " + resourceString + "\n" +
- " </nodes>\n" +
- " </container>\n" +
- "</services>\n";
-
- return servicesXml.getBytes(UTF_8);
- }
-
- /** Returns a dummy deployment xml which sets up the service identity for the tester, if present. */
- private static byte[] deploymentXml(TesterId id, Optional<AthenzDomain> athenzDomain, Optional<AthenzService> athenzService) {
- String deploymentSpec =
- "<?xml version='1.0' encoding='UTF-8'?>\n" +
- "<deployment version=\"1.0\" " +
- athenzDomain.map(domain -> "athenz-domain=\"" + domain.value() + "\" ").orElse("") +
- athenzService.map(service -> "athenz-service=\"" + service.value() + "\" ").orElse("") + ">" +
- " <instance id=\"" + id.id().instance().value() + "\" />" +
- "</deployment>";
- return deploymentSpec.getBytes(UTF_8);
- }
-
/** Logger which logs to a {@link JobController}, as well as to the parent class' {@link Logger}. */
private class DualLogger {
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 69d9ba504a5..91d127976ce 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
@@ -28,6 +28,11 @@ import com.yahoo.vespa.hosted.controller.application.Deployment;
import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackageDiff;
+import com.yahoo.vespa.hosted.controller.application.pkg.TestPackage;
+import com.yahoo.vespa.hosted.controller.application.pkg.TestPackage.TestSummary;
+import com.yahoo.vespa.hosted.controller.notification.Notification;
+import com.yahoo.vespa.hosted.controller.notification.Notification.Type;
+import com.yahoo.vespa.hosted.controller.notification.NotificationSource;
import com.yahoo.vespa.hosted.controller.persistence.BufferedLogStore;
import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
@@ -489,6 +494,16 @@ public class JobController {
application = application.withRevisions(revisions -> revisions.with(version.get()));
application = withPrunedPackages(application);
+ TestSummary testSummary = TestPackage.validateTests(applicationPackage.deploymentSpec(), testPackageBytes);
+ if (testSummary.problems().isEmpty())
+ controller.notificationsDb().removeNotification(NotificationSource.from(id),
+ Type.testPackage);
+ else
+ controller.notificationsDb().setNotification(NotificationSource.from(id),
+ Type.testPackage,
+ Notification.Level.warning,
+ testSummary.problems());
+
applications.storeWithUpdatedConfig(application, applicationPackage);
applications.deploymentTrigger().triggerNewRevision(id);
});
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notification.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notification.java
index c39ee031e27..38cb09355ca 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notification.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notification.java
@@ -66,10 +66,13 @@ public class Notification {
}
public enum Type {
- /** Related to contents of application package, e.g. usage of deprecated features/syntax */
+ /** Related to contents of application package, e.g., usage of deprecated features/syntax */
applicationPackage,
- /** Related to deployment of application, e.g. system test failure, node allocation failure, internal errors, etc. */
+ /** Related to contents of application test package, e.g., mismatch between deployment spec and provided tests */
+ testPackage,
+
+ /** Related to deployment of application, e.g., system test failure, node allocation failure, internal errors, etc. */
deployment,
/** Application cluster is (near) external feed blocked */
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java
index c882681632e..570dbdd870e 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java
@@ -92,6 +92,7 @@ public class NotificationsSerializer {
private static String asString(Notification.Type type) {
switch (type) {
case applicationPackage: return "applicationPackage";
+ case testPackage: return "testPackage";
case deployment: return "deployment";
case feedBlock: return "feedBlock";
case reindex: return "reindex";
@@ -102,6 +103,7 @@ public class NotificationsSerializer {
private static Notification.Type typeFrom(Inspector field) {
switch (field.asString()) {
case "applicationPackage": return Notification.Type.applicationPackage;
+ case "testPackage": return Notification.Type.testPackage;
case "deployment": return Notification.Type.deployment;
case "feedBlock": return Notification.Type.feedBlock;
case "reindex": return Notification.Type.reindex;
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 c07794ea39c..4df53be57f2 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
@@ -745,6 +745,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
private static String notificationTypeAsString(Notification.Type type) {
switch (type) {
case applicationPackage: return "applicationPackage";
+ case testPackage: return "testPackage";
case deployment: return "deployment";
case feedBlock: return "feedBlock";
case reindex: return "reindex";
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.java
index dff379a2249..de03bbfb767 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.java
@@ -225,6 +225,7 @@ class JobControllerApiHandlerHelper {
case aborted: return "aborted";
case error: return "error";
case testFailure: return "testFailure";
+ case noTests: return "noTests";
case endpointCertificateTimeout: return "endpointCertificateTimeout";
case nodeAllocationFailure: return "nodeAllocationFailure";
case installationFailed: return "installationFailed";
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java
index e9619297a2f..20b64419f28 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java
@@ -4,6 +4,7 @@ package com.yahoo.vespa.hosted.controller;
import com.google.common.collect.Sets;
import com.yahoo.component.Version;
import com.yahoo.config.application.api.DeploymentSpec;
+import com.yahoo.config.application.api.Notifications;
import com.yahoo.config.application.api.ValidationId;
import com.yahoo.config.application.api.ValidationOverrides;
import com.yahoo.config.provision.ApplicationId;
@@ -38,6 +39,10 @@ import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock;
+import com.yahoo.vespa.hosted.controller.notification.Notification;
+import com.yahoo.vespa.hosted.controller.notification.Notification.Level;
+import com.yahoo.vespa.hosted.controller.notification.Notification.Type;
+import com.yahoo.vespa.hosted.controller.notification.NotificationSource;
import com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb;
import com.yahoo.vespa.hosted.controller.routing.RoutingStatus;
import com.yahoo.vespa.hosted.controller.routing.context.DeploymentRoutingContext;
@@ -1104,6 +1109,27 @@ public class ControllerTest {
}
@Test
+ public void testTestPackageWarnings() {
+ String deploymentXml = "<deployment version='1.0'>\n" +
+ " <prod>\n" +
+ " <region>us-west-1</region>\n" +
+ " </prod>\n" +
+ "</deployment>\n";
+ ApplicationPackage applicationPackage = ApplicationPackageBuilder.fromDeploymentXml(deploymentXml);
+ byte[] testPackage = ApplicationPackage.filesZip(Map.of("tests/staging-test/foo.json", new byte[0]));
+ var app = tester.newDeploymentContext();
+ tester.jobs().submit(app.application().id(), Optional.empty(), Optional.empty(), Optional.empty(), 1,
+ applicationPackage, testPackage, Optional.empty(), 0);
+ assertEquals(List.of(new Notification(tester.clock().instant(),
+ Type.testPackage,
+ Level.warning,
+ NotificationSource.from(app.application().id()),
+ List.of("test package has staging tests, so it should also include staging setup",
+ "see https://docs.vespa.ai/en/testing.html for details on how to write system tests for Vespa"))),
+ tester.controller().notificationsDb().listNotifications(NotificationSource.from(app.application().id()), true));
+ }
+
+ @Test
public void testCompileVersion() {
DeploymentContext context = tester.newDeploymentContext();
ApplicationPackage applicationPackage = new ApplicationPackageBuilder().region("us-west-1").build();
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageTest.java
index 4e155e937b9..99e22302c73 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageTest.java
@@ -24,24 +24,24 @@ import static org.junit.Assert.fail;
*/
public class ApplicationPackageTest {
- private static final String deploymentXml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
- "<deployment version=\"1.0\">\n" +
- " <test />\n" +
- " <prod>\n" +
- " <parallel>\n" +
- " <region active=\"true\">us-central-1</region>\n" +
- " </parallel>\n" +
- " </prod>\n" +
- "</deployment>\n";
-
- private static final String servicesXml = "<services version='1.0' xmlns:deploy=\"vespa\" xmlns:preprocess=\"properties\">\n" +
- " <preprocess:include file='jdisc.xml' />\n" +
- " <content version='1.0' if='foo' />\n" +
- " <content version='1.0' id='foo' deploy:environment='staging prod' deploy:region='us-east-3 us-central-1'>\n" +
- " <preprocess:include file='content/content.xml' />\n" +
- " </content>\n" +
- " <preprocess:include file='not_found.xml' required='false' />\n" +
- "</services>\n";
+ static final String deploymentXml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+ "<deployment version=\"1.0\">\n" +
+ " <test />\n" +
+ " <prod>\n" +
+ " <parallel>\n" +
+ " <region active=\"true\">us-central-1</region>\n" +
+ " </parallel>\n" +
+ " </prod>\n" +
+ "</deployment>\n";
+
+ static final String servicesXml = "<services version='1.0' xmlns:deploy=\"vespa\" xmlns:preprocess=\"properties\">\n" +
+ " <preprocess:include file='jdisc.xml' />\n" +
+ " <content version='1.0' if='foo' />\n" +
+ " <content version='1.0' id='foo' deploy:environment='staging prod' deploy:region='us-east-3 us-central-1'>\n" +
+ " <preprocess:include file='content/content.xml' />\n" +
+ " </content>\n" +
+ " <preprocess:include file='not_found.xml' required='false' />\n" +
+ "</services>\n";
private static final String jdiscXml = "<container id='stateless' version='1.0' />\n";
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/TestPackageTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/TestPackageTest.java
new file mode 100644
index 00000000000..8588bb9ea16
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/TestPackageTest.java
@@ -0,0 +1,157 @@
+package com.yahoo.vespa.hosted.controller.application.pkg;
+
+import com.yahoo.config.application.api.DeploymentSpec;
+import com.yahoo.config.provision.NodeResources;
+import com.yahoo.config.provision.zone.ZoneId;
+import com.yahoo.vespa.hosted.controller.application.pkg.TestPackage.TestSummary;
+import com.yahoo.vespa.hosted.controller.config.ControllerConfig;
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Map;
+import java.util.jar.JarOutputStream;
+import java.util.zip.ZipEntry;
+
+import static com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud.Suite.production;
+import static com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud.Suite.staging;
+import static com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud.Suite.staging_setup;
+import static com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud.Suite.system;
+import static com.yahoo.vespa.hosted.controller.application.pkg.TestPackage.validateTests;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author jonmv
+ */
+public class TestPackageTest {
+
+ static byte[] testsJar(String... suites) throws IOException {
+ String manifest = "Manifest-Version: 1.0\n" +
+ "Created-By: vespa container maven plugin\n" +
+ "Build-Jdk-Spec: 17\n" +
+ "Bundle-ManifestVersion: 2\n" +
+ "Bundle-SymbolicName: canary-application-test\n" +
+ "Bundle-Version: 1.0.1\n" +
+ "Bundle-Name: Test & verification application for Vespa\n" +
+ "X-JDisc-Test-Bundle-Version: 1.0\n" +
+ "Bundle-Vendor: Yahoo!\n" +
+ "Bundle-ClassPath: .,dependencies/fest-assert-1.4.jar,dependencies/fest-u\n" +
+ " til-1.1.6.jar\n" +
+ "Import-Package: ai.vespa.feed.client;version=\"[1.0.0,2)\",ai.vespa.hosted\n" +
+ " .cd;version=\"[1.0.0,2)\",com.yahoo.config;version=\"[1.0.0,2)\",com.yahoo.\n" +
+ " container.jdisc;version=\"[1.0.0,2)\",com.yahoo.jdisc.http;version=\"[1.0.\n" +
+ " 0,2)\",com.yahoo.slime;version=\"[1.0.0,2)\",java.awt.image;version=\"[0.0.\n" +
+ " 0,1)\",java.awt;version=\"[0.0.0,1)\",java.beans;version=\"[0.0.0,1)\",java.\n" +
+ " io;version=\"[0.0.0,1)\",java.lang.annotation;version=\"[0.0.0,1)\",java.la\n" +
+ " ng.reflect;version=\"[0.0.0,1)\",java.lang;version=\"[0.0.0,1)\",java.math;\n" +
+ " version=\"[0.0.0,1)\",java.net.http;version=\"[0.0.0,1)\",java.net;version=\n" +
+ " \"[0.0.0,1)\",java.nio.file;version=\"[0.0.0,1)\",java.security;version=\"[0\n" +
+ " .0.0,1)\",java.text;version=\"[0.0.0,1)\",java.time.temporal;version=\"[0.0\n" +
+ " .0,1)\",java.time;version=\"[0.0.0,1)\",java.util.concurrent;version=\"[0.0\n" +
+ " .0,1)\",java.util.function;version=\"[0.0.0,1)\",java.util.stream;version=\n" +
+ " \"[0.0.0,1)\",java.util;version=\"[0.0.0,1)\",javax.imageio;version=\"[0.0.0\n" +
+ " ,1)\",org.junit.jupiter.api;version=\"[5.8.1,6)\"\n" +
+ "X-JDisc-Test-Bundle-Categories: " + String.join(",", suites) + "\n" +
+ "\n";
+
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ try (JarOutputStream out = new JarOutputStream(buffer)) {
+ write("META-INF/MANIFEST.MF", manifest, out);
+ write("dependencies/foo.jar", "bar", out);
+ write("META-INF/maven/ai.vespa.test/app/pom.xml", "<project />", out);
+ write("ai/vespa/test/Test.class", "baz", out);
+ }
+ return buffer.toByteArray();
+ }
+
+ static void write(String name, String content, JarOutputStream out) throws IOException {
+ out.putNextEntry(new ZipEntry(name));
+ out.write(content.getBytes(UTF_8));
+ out.closeEntry();
+ }
+
+ @Test
+ public void testBundleValidation() throws IOException {
+ byte[] testZip = ApplicationPackage.filesZip(Map.of("components/foo-tests.jar", testsJar("SystemTest", "StagingSetup", "ProductionTest"),
+ "artifacts/key", new byte[0]));
+ TestSummary summary = validateTests(List.of(system), testZip);
+
+ assertEquals(List.of(system, staging_setup, production), summary.suites());
+ assertEquals(List.of("test package contains 'artifacts/key'; this conflicts with credentials used to run tests in Vespa Cloud",
+ "test package has staging setup, so it should also include staging tests",
+ "test package has production tests, but no production tests are declared in deployment.xml",
+ "see https://docs.vespa.ai/en/testing.html for details on how to write system tests for Vespa"),
+ summary.problems());
+ }
+
+ @Test
+ public void testFatTestsValidation() {
+ byte[] testZip = ApplicationPackage.filesZip(Map.of("artifacts/foo-tests.jar", new byte[0]));
+ TestSummary summary = validateTests(List.of(staging, production), testZip);
+
+ assertEquals(List.of(staging, production), summary.suites());
+ assertEquals(List.of("test package has staging tests, so it should also include staging setup",
+ "see https://docs.vespa.ai/en/testing.html for details on how to write system tests for Vespa"),
+ summary.problems());
+ }
+
+ @Test
+ public void testBasicTestsValidation() {
+ byte[] testZip = ApplicationPackage.filesZip(Map.of("tests/staging-test/foo.json", new byte[0],
+ "tests/staging-setup/foo.json", new byte[0]));
+ TestSummary summary = validateTests(List.of(system, production), testZip);
+ assertEquals(List.of(staging_setup, staging), summary.suites());
+ assertEquals(List.of("test package has no system tests, but <test /> is declared in deployment.xml",
+ "test package has no production tests, but production tests are declared in deployment.xml",
+ "see https://docs.vespa.ai/en/testing.html for details on how to write system tests for Vespa"),
+ summary.problems());
+ }
+
+ @Test
+ public void generates_correct_tester_flavor() {
+ DeploymentSpec spec = DeploymentSpec.fromXml("<deployment version='1.0' athenz-domain='domain' athenz-service='service'>\n" +
+ " <instance id='first'>\n" +
+ " <test tester-flavor=\"d-6-16-100\" />\n" +
+ " <prod>\n" +
+ " <region active=\"true\">us-west-1</region>\n" +
+ " <test>us-west-1</test>\n" +
+ " </prod>\n" +
+ " </instance>\n" +
+ " <instance id='second'>\n" +
+ " <test />\n" +
+ " <staging />\n" +
+ " <prod tester-flavor=\"d-6-16-100\">\n" +
+ " <parallel>\n" +
+ " <region active=\"true\">us-east-3</region>\n" +
+ " <region active=\"true\">us-central-1</region>\n" +
+ " </parallel>\n" +
+ " <region active=\"true\">us-west-1</region>\n" +
+ " <test>us-west-1</test>\n" +
+ " </prod>\n" +
+ " </instance>\n" +
+ "</deployment>\n");
+
+ NodeResources firstResources = TestPackage.testerResourcesFor(ZoneId.from("prod", "us-west-1"), spec.requireInstance("first"));
+ assertEquals(TestPackage.DEFAULT_TESTER_RESOURCES, firstResources);
+
+ NodeResources secondResources = TestPackage.testerResourcesFor(ZoneId.from("prod", "us-west-1"), spec.requireInstance("second"));
+ assertEquals(6, secondResources.vcpu(), 1e-9);
+ assertEquals(16, secondResources.memoryGb(), 1e-9);
+ assertEquals(100, secondResources.diskGb(), 1e-9);
+ }
+
+ @Test
+ public void generates_correct_services_xml() throws IOException {
+ assertEquals(Files.readString(Paths.get("src/test/resources/test_runner_services.xml-cd")),
+ new String(TestPackage.servicesXml(true,
+ false,
+ new NodeResources(2, 12, 75, 1, NodeResources.DiskSpeed.fast, NodeResources.StorageType.local),
+ new ControllerConfig.Steprunner.Testerapp.Builder().build()),
+ UTF_8));
+ }
+
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunnerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunnerTest.java
index 0dde6bd882f..031cdaa84ae 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunnerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunnerTest.java
@@ -13,9 +13,6 @@ import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.slime.Inspector;
import com.yahoo.slime.SlimeUtils;
import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.configserverbindings.ConfigChangeActions;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.configserverbindings.RestartAction;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.configserverbindings.ServiceInfo;
import com.yahoo.vespa.hosted.controller.api.integration.LogEntry;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServerException;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
@@ -27,6 +24,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud.
import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockMailer;
import com.yahoo.vespa.hosted.controller.application.SystemApplication;
import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
+import com.yahoo.vespa.hosted.controller.application.pkg.TestPackage;
import com.yahoo.vespa.hosted.controller.config.ControllerConfig;
import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock;
import com.yahoo.vespa.hosted.controller.maintenance.JobRunner;
@@ -35,6 +33,7 @@ import org.junit.Test;
import java.io.IOException;
import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -47,8 +46,6 @@ import java.util.List;
import java.util.Optional;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.function.Supplier;
import static com.yahoo.vespa.hosted.controller.api.integration.LogEntry.Type.error;
import static com.yahoo.vespa.hosted.controller.api.integration.LogEntry.Type.info;
@@ -63,6 +60,7 @@ import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.success;
import static com.yahoo.vespa.hosted.controller.deployment.Step.Status.failed;
import static com.yahoo.vespa.hosted.controller.deployment.Step.Status.succeeded;
import static com.yahoo.vespa.hosted.controller.deployment.Step.Status.unfinished;
+import static java.nio.charset.StandardCharsets.UTF_8;
import static java.time.temporal.ChronoUnit.SECONDS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -556,62 +554,4 @@ public class InternalStepRunnerTest {
"3554970337.947820\t17491290-v6-1.ostk.bm2.prod.ne1.yahoo.com\t5480\tcontainer\tstdout\tinfo\tERROR: Bundle canary-application [71] Unable to get module class path. (java.lang.NullPointerException)\n" +
"3554970337.947845\t17491290-v6-1.ostk.bm2.prod.ne1.yahoo.com\t5480\tcontainer\tstderr\twarning\tjava.lang.NullPointerException\\n\\tat org.apache.felix.framework.BundleRevisionImpl.calculateContentPath(BundleRevisionImpl.java:438)\\n\\tat org.apache.felix.framework.BundleRevisionImpl.initializeContentPath(BundleRevisionImpl.java:371)";
- @Test
- public void generates_correct_tester_flavor() {
- DeploymentSpec spec = DeploymentSpec.fromXml("<deployment version='1.0' athenz-domain='domain' athenz-service='service'>\n" +
- " <instance id='first'>\n" +
- " <test tester-flavor=\"d-6-16-100\" />\n" +
- " <prod>\n" +
- " <region active=\"true\">us-west-1</region>\n" +
- " <test>us-west-1</test>\n" +
- " </prod>\n" +
- " </instance>\n" +
- " <instance id='second'>\n" +
- " <test />\n" +
- " <staging />\n" +
- " <prod tester-flavor=\"d-6-16-100\">\n" +
- " <parallel>\n" +
- " <region active=\"true\">us-east-3</region>\n" +
- " <region active=\"true\">us-central-1</region>\n" +
- " </parallel>\n" +
- " <region active=\"true\">us-west-1</region>\n" +
- " <test>us-west-1</test>\n" +
- " </prod>\n" +
- " </instance>\n" +
- "</deployment>\n");
-
- NodeResources firstResources = InternalStepRunner.testerResourcesFor(ZoneId.from("prod", "us-west-1"), spec.requireInstance("first"));
- assertEquals(InternalStepRunner.DEFAULT_TESTER_RESOURCES, firstResources);
-
- NodeResources secondResources = InternalStepRunner.testerResourcesFor(ZoneId.from("prod", "us-west-1"), spec.requireInstance("second"));
- assertEquals(6, secondResources.vcpu(), 1e-9);
- assertEquals(16, secondResources.memoryGb(), 1e-9);
- assertEquals(100, secondResources.diskGb(), 1e-9);
- }
-
- @Test
- public void generates_correct_services_xml() {
- generates_correct_services_xml("test_runner_services.xml-cd");
- }
-
- private void generates_correct_services_xml(String filenameExpectedOutput) {
- ControllerConfig.Steprunner.Testerapp config = new ControllerConfig.Steprunner.Testerapp.Builder().build();
- assertFile(filenameExpectedOutput,
- new String(InternalStepRunner.servicesXml(
- true,
- false,
- new NodeResources(2, 12, 75, 1, NodeResources.DiskSpeed.fast, NodeResources.StorageType.local),
- config)));
- }
-
- private void assertFile(String resourceName, String actualContent) {
- try {
- Path path = Paths.get("src/test/resources/").resolve(resourceName);
- String expectedContent = new String(Files.readAllBytes(path));
- assertEquals(expectedContent, actualContent);
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- }
-
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelperTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelperTest.java
index ac8bdafa2bd..d2698afdc48 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelperTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelperTest.java
@@ -8,10 +8,13 @@ import com.yahoo.slime.SlimeUtils;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServerException;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.TestReport;
+import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
+import com.yahoo.vespa.hosted.controller.notification.Notification.Type;
+import com.yahoo.vespa.hosted.controller.notification.NotificationSource;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
diff --git a/vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/JunitRunner.java b/vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/JunitRunner.java
index c59c693a789..8a0446ac2e6 100644
--- a/vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/JunitRunner.java
+++ b/vespa-osgi-testrunner/src/main/java/com/yahoo/vespa/testrunner/JunitRunner.java
@@ -153,7 +153,7 @@ public class JunitRunner extends AbstractComponent implements TestRunner {
try {
var jsonDescriptor = IOUtils.readAll(resource.openStream(), Charset.defaultCharset()).trim();
testDescriptor = TestDescriptor.fromJsonString(jsonDescriptor);
- logger.info( "Test classes in bundle :" + testDescriptor.toString());
+ logger.info("Test classes in bundle: " + testDescriptor);
return Optional.of(testDescriptor);
} catch (IOException e) {
return Optional.empty();