aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorHarald Musum <musum@verizonmedia.com>2021-09-28 18:30:46 +0200
committerGitHub <noreply@github.com>2021-09-28 18:30:46 +0200
commit35223653327b86a059d23c543bbac3611d43775f (patch)
tree90959d45885018409331e934a6f76dfa690b879f
parent6ffe9403bf9ce0745bc10b1043dd11be1894aa4e (diff)
parentb9a11e9c3d78562fc2e6b433fd9196c5255eac8f (diff)
Merge pull request #19327 from vespa-engine/hmusum/wire-in-deployment-dry-run-option
Wire in dryRun deployment option
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentData.java22
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java13
-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/deployment/Run.java29
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializer.java7
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java8
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentContext.java4
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializerTest.java5
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelperTest.java13
9 files changed, 86 insertions, 30 deletions
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentData.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentData.java
index 1608e72b0b6..1f1f8577a32 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentData.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentData.java
@@ -1,4 +1,4 @@
-// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.api.application.v4.model;
import com.yahoo.component.Version;
@@ -6,7 +6,6 @@ import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.DockerImage;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.vespa.athenz.api.AthenzDomain;
-import com.yahoo.vespa.hosted.controller.api.integration.aws.TenantRoles;
import com.yahoo.vespa.hosted.controller.api.integration.billing.Quota;
import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateMetadata;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.ContainerEndpoint;
@@ -37,7 +36,9 @@ public class DeploymentData {
private final Quota quota;
private final List<TenantSecretStore> tenantSecretStores;
private final List<X509Certificate> operatorCertificates;
+ private final boolean dryRun;
+ // TODO: Remove when users have been updated to use constructor below
public DeploymentData(ApplicationId instance, ZoneId zone, byte[] applicationPackage, Version platform,
Set<ContainerEndpoint> containerEndpoints,
Optional<EndpointCertificateMetadata> endpointCertificateMetadata,
@@ -46,6 +47,19 @@ public class DeploymentData {
Quota quota,
List<TenantSecretStore> tenantSecretStores,
List<X509Certificate> operatorCertificates) {
+ this(instance, zone, applicationPackage, platform, containerEndpoints, endpointCertificateMetadata,
+ dockerImageRepo, athenzDomain, quota, tenantSecretStores, operatorCertificates, false);
+ }
+
+ public DeploymentData(ApplicationId instance, ZoneId zone, byte[] applicationPackage, Version platform,
+ Set<ContainerEndpoint> containerEndpoints,
+ Optional<EndpointCertificateMetadata> endpointCertificateMetadata,
+ Optional<DockerImage> dockerImageRepo,
+ Optional<AthenzDomain> athenzDomain,
+ Quota quota,
+ List<TenantSecretStore> tenantSecretStores,
+ List<X509Certificate> operatorCertificates,
+ boolean dryRun) {
this.instance = requireNonNull(instance);
this.zone = requireNonNull(zone);
this.applicationPackage = requireNonNull(applicationPackage);
@@ -57,6 +71,7 @@ public class DeploymentData {
this.quota = quota;
this.tenantSecretStores = tenantSecretStores;
this.operatorCertificates = operatorCertificates;
+ this.dryRun = dryRun;
}
public ApplicationId instance() {
@@ -102,4 +117,7 @@ public class DeploymentData {
public List<X509Certificate> operatorCertificates() {
return operatorCertificates;
}
+
+ public boolean isDryRun() { return dryRun; }
+
}
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 fe5fc90df60..861d101ae3f 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
@@ -384,7 +384,8 @@ public class ApplicationController {
} // Release application lock while doing the deployment, which is a lengthy task.
// Carry out deployment without holding the application lock.
- ActivateResult result = deploy(job.application(), applicationPackage, zone, platform, containerEndpoints, endpointCertificateMetadata);
+ ActivateResult result = deploy(job.application(), applicationPackage, zone, platform, containerEndpoints,
+ endpointCertificateMetadata, run.isDryRun());
// Record the quota usage for this application
var quotaUsage = deploymentQuotaUsage(zone, job.application());
@@ -466,7 +467,7 @@ public class ApplicationController {
ApplicationPackage applicationPackage = new ApplicationPackage(
artifactRepository.getSystemApplicationPackage(application.id(), zone, version)
);
- return deploy(application.id(), applicationPackage, zone, version, Set.of(), /* No application cert */ Optional.empty());
+ return deploy(application.id(), applicationPackage, zone, version, Set.of(), /* No application cert */ Optional.empty(), false);
} else {
throw new RuntimeException("This system application does not have an application package: " + application.id().toShortString());
}
@@ -474,12 +475,13 @@ public class ApplicationController {
/** Deploys the given tester application to the given zone. */
public ActivateResult deployTester(TesterId tester, ApplicationPackage applicationPackage, ZoneId zone, Version platform) {
- return deploy(tester.id(), applicationPackage, zone, platform, Set.of(), /* No application cert for tester*/ Optional.empty());
+ return deploy(tester.id(), applicationPackage, zone, platform, Set.of(), /* No application cert for tester*/ Optional.empty(), false);
}
private ActivateResult deploy(ApplicationId application, ApplicationPackage applicationPackage,
ZoneId zone, Version platform, Set<ContainerEndpoint> endpoints,
- Optional<EndpointCertificateMetadata> endpointCertificateMetadata) {
+ Optional<EndpointCertificateMetadata> endpointCertificateMetadata,
+ boolean dryRun) {
try {
Optional<DockerImage> dockerImageRepo = Optional.ofNullable(
dockerImageRepoFlag
@@ -513,7 +515,8 @@ public class ApplicationController {
ConfigServer.PreparedApplication preparedApplication =
configServer.deploy(new DeploymentData(application, zone, applicationPackage.zippedContent(), platform,
endpoints, endpointCertificateMetadata, dockerImageRepo, domain,
- deploymentQuota, tenantSecretStores, operatorCertificates));
+ deploymentQuota, tenantSecretStores, operatorCertificates,
+ dryRun));
return new ActivateResult(new RevisionId(applicationPackage.hash()), preparedApplication.prepareResponse(),
applicationPackage.zippedContent().length);
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 88d440b52b9..4076013b2c5 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
@@ -475,24 +475,30 @@ public class JobController {
/** Orders a run of the given type, or throws an IllegalStateException if that job type is already running. */
public void start(ApplicationId id, JobType type, Versions versions, boolean isRedeployment) {
- start(id, type, versions, isRedeployment, JobProfile.of(type));
+ start(id, type, versions, isRedeployment, JobProfile.of(type), false);
}
/** Orders a run of the given type, or throws an IllegalStateException if that job type is already running. */
- public void start(ApplicationId id, JobType type, Versions versions, boolean isRedeployment, JobProfile profile) {
+ public void start(ApplicationId id, JobType type, Versions versions, boolean isRedeployment, JobProfile profile, boolean dryRun) {
locked(id, type, __ -> {
Optional<Run> last = last(id, type);
if (last.flatMap(run -> active(run.id())).isPresent())
throw new IllegalStateException("Can not start " + type + " for " + id + "; it is already running!");
RunId newId = new RunId(id, type, last.map(run -> run.id().number()).orElse(0L) + 1);
- curator.writeLastRun(Run.initial(newId, versions, isRedeployment, controller.clock().instant(), profile));
+ curator.writeLastRun(Run.initial(newId, versions, isRedeployment, controller.clock().instant(), profile, dryRun));
metric.jobStarted(newId.job());
});
}
+
/** Stores the given package and starts a deployment of it, after aborting any such ongoing deployment. */
public void deploy(ApplicationId id, JobType type, Optional<Version> platform, ApplicationPackage applicationPackage) {
+ deploy(id, type, platform, applicationPackage, false);
+ }
+
+ /** Stores the given package and starts a deployment of it, after aborting any such ongoing deployment.*/
+ public void deploy(ApplicationId id, JobType type, Optional<Version> platform, ApplicationPackage applicationPackage, boolean dryRun) {
controller.applications().lockApplicationOrThrow(TenantAndApplicationId.from(id), application -> {
if ( ! application.get().instances().containsKey(id.instance()))
application = controller.applications().withNewInstance(application, id);
@@ -525,7 +531,8 @@ public class JobController {
lastRun.map(run -> run.versions().targetPlatform()),
lastRun.map(run -> run.versions().targetApplication())),
false,
- JobProfile.development);
+ JobProfile.development,
+ dryRun);
});
locked(id, type, __ -> {
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Run.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Run.java
index d93a0133c97..b3768178b1e 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Run.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Run.java
@@ -39,11 +39,12 @@ public class Run {
private final Optional<Instant> noNodesDownSince;
private final Optional<ConvergenceSummary> convergenceSummary;
private final Optional<X509Certificate> testerCertificate;
+ private final boolean dryRun;
// For deserialisation only -- do not use!
public Run(RunId id, Map<Step, StepInfo> steps, Versions versions, boolean isRedeployment, Instant start, Optional<Instant> end,
RunStatus status, long lastTestRecord, Instant lastVespaLogTimestamp, Optional<Instant> noNodesDownSince,
- Optional<ConvergenceSummary> convergenceSummary, Optional<X509Certificate> testerCertificate) {
+ Optional<ConvergenceSummary> convergenceSummary, Optional<X509Certificate> testerCertificate, boolean dryRun) {
this.id = id;
this.steps = Collections.unmodifiableMap(new EnumMap<>(steps));
this.versions = versions;
@@ -56,13 +57,14 @@ public class Run {
this.noNodesDownSince = noNodesDownSince;
this.convergenceSummary = convergenceSummary;
this.testerCertificate = testerCertificate;
+ this.dryRun = dryRun;
}
- public static Run initial(RunId id, Versions versions, boolean isRedeployment, Instant now, JobProfile profile) {
+ public static Run initial(RunId id, Versions versions, boolean isRedeployment, Instant now, JobProfile profile, boolean dryRun) {
EnumMap<Step, StepInfo> steps = new EnumMap<>(Step.class);
profile.steps().forEach(step -> steps.put(step, StepInfo.initial(step)));
return new Run(id, steps, requireNonNull(versions), isRedeployment, requireNonNull(now), Optional.empty(), running,
- -1, Instant.EPOCH, Optional.empty(), Optional.empty(), Optional.empty());
+ -1, Instant.EPOCH, Optional.empty(), Optional.empty(), Optional.empty(), dryRun);
}
/** Returns a new Run with the status of the given completed step set accordingly. */
@@ -76,7 +78,7 @@ public class Run {
EnumMap<Step, StepInfo> steps = new EnumMap<>(this.steps);
steps.put(step.get(), stepInfo.with(Step.Status.of(status)));
return new Run(id, steps, versions, isRedeployment, start, end, this.status == running ? status : this.status,
- lastTestRecord, lastVespaLogTimestamp, noNodesDownSince, convergenceSummary, testerCertificate);
+ lastTestRecord, lastVespaLogTimestamp, noNodesDownSince, convergenceSummary, testerCertificate, dryRun);
}
/** Returns a new Run with a new start time*/
@@ -91,49 +93,49 @@ public class Run {
steps.put(step.get(), stepInfo.with(startTime));
return new Run(id, steps, versions, isRedeployment, start, end, status, lastTestRecord, lastVespaLogTimestamp,
- noNodesDownSince, convergenceSummary, testerCertificate);
+ noNodesDownSince, convergenceSummary, testerCertificate, dryRun);
}
public Run finished(Instant now) {
requireActive();
return new Run(id, steps, versions, isRedeployment, start, Optional.of(now), status == running ? success : status,
- lastTestRecord, lastVespaLogTimestamp, noNodesDownSince, convergenceSummary, Optional.empty());
+ lastTestRecord, lastVespaLogTimestamp, noNodesDownSince, convergenceSummary, Optional.empty(), dryRun);
}
public Run aborted() {
requireActive();
return new Run(id, steps, versions, isRedeployment, start, end, aborted, lastTestRecord, lastVespaLogTimestamp,
- noNodesDownSince, convergenceSummary, testerCertificate);
+ noNodesDownSince, convergenceSummary, testerCertificate, dryRun);
}
public Run with(long lastTestRecord) {
requireActive();
return new Run(id, steps, versions, isRedeployment, start, end, status, lastTestRecord, lastVespaLogTimestamp,
- noNodesDownSince, convergenceSummary, testerCertificate);
+ noNodesDownSince, convergenceSummary, testerCertificate, dryRun);
}
public Run with(Instant lastVespaLogTimestamp) {
requireActive();
return new Run(id, steps, versions, isRedeployment, start, end, status, lastTestRecord, lastVespaLogTimestamp,
- noNodesDownSince, convergenceSummary, testerCertificate);
+ noNodesDownSince, convergenceSummary, testerCertificate, dryRun);
}
public Run noNodesDownSince(Instant noNodesDownSince) {
requireActive();
return new Run(id, steps, versions, isRedeployment, start, end, status, lastTestRecord, lastVespaLogTimestamp,
- Optional.ofNullable(noNodesDownSince), convergenceSummary, testerCertificate);
+ Optional.ofNullable(noNodesDownSince), convergenceSummary, testerCertificate, dryRun);
}
public Run withSummary(ConvergenceSummary convergenceSummary) {
requireActive();
return new Run(id, steps, versions, isRedeployment, start, end, status, lastTestRecord, lastVespaLogTimestamp,
- noNodesDownSince, Optional.ofNullable(convergenceSummary), testerCertificate);
+ noNodesDownSince, Optional.ofNullable(convergenceSummary), testerCertificate, dryRun);
}
public Run with(X509Certificate testerCertificate) {
requireActive();
return new Run(id, steps, versions, isRedeployment, start, end, status, lastTestRecord, lastVespaLogTimestamp,
- noNodesDownSince, convergenceSummary, Optional.of(testerCertificate));
+ noNodesDownSince, convergenceSummary, Optional.of(testerCertificate), dryRun);
}
/** Returns the id of this run. */
@@ -229,6 +231,9 @@ public class Run {
return isRedeployment;
}
+ /** Whether this is a dry run deployment. */
+ public boolean isDryRun() { return dryRun; }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializer.java
index b4a580a1562..6af3978d0cd 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializer.java
@@ -1,4 +1,4 @@
-// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.persistence;
import com.yahoo.component.Version;
@@ -100,6 +100,7 @@ class RunSerializer {
private static final String noNodesDownSinceField = "noNodesDownSince";
private static final String convergenceSummaryField = "convergenceSummaryV2";
private static final String testerCertificateField = "testerCertificate";
+ private static final String isDryRunField = "isDryRun";
Run runFromSlime(Slime slime) {
return runFromSlime(slime.get());
@@ -143,7 +144,8 @@ class RunSerializer {
convergenceSummaryFrom(runObject.field(convergenceSummaryField)),
Optional.of(runObject.field(testerCertificateField))
.filter(Inspector::valid)
- .map(certificate -> X509CertificateUtils.fromPem(certificate.asString())));
+ .map(certificate -> X509CertificateUtils.fromPem(certificate.asString())),
+ runObject.field(isDryRunField).valid() && runObject.field(isDryRunField).asBool());
}
private Versions versionsFromSlime(Inspector versionsObject) {
@@ -250,6 +252,7 @@ class RunSerializer {
.orElseThrow(() -> new IllegalArgumentException("Source versions must be both present or absent.")),
versionsObject.setObject(sourceField));
});
+ runObject.setBool(isDryRunField, run.isDryRun());
}
private void toSlime(Version platformVersion, ApplicationVersion applicationVersion, Cursor versionsObject) {
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 98ac789de04..1774694853b 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
@@ -1969,7 +1969,13 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
ensureApplicationExists(TenantAndApplicationId.from(id), request);
- controller.jobController().deploy(id, type, version, applicationPackage);
+ boolean dryRun = Optional.ofNullable(dataParts.get("deployOptions"))
+ .map(json -> SlimeUtils.jsonToSlime(json).get())
+ .flatMap(options -> optional("dryRun", options))
+ .map(Boolean::valueOf)
+ .orElse(false);
+
+ controller.jobController().deploy(id, type, version, applicationPackage, dryRun);
RunId runId = controller.jobController().last(id, type).get().id();
Slime slime = new Slime();
Cursor rootObject = slime.setObject();
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentContext.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentContext.java
index c225dcbe49d..83e1a019109 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentContext.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentContext.java
@@ -1,4 +1,4 @@
-// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+// Copyright Yahoo. 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.base.Supplier;
@@ -323,7 +323,7 @@ public class DeploymentContext {
/** Runs a deployment of the given package to the given dev/perf job, on the given version. */
public DeploymentContext runJob(JobType type, ApplicationPackage applicationPackage, Version vespaVersion) {
- jobs.deploy(instanceId, type, Optional.ofNullable(vespaVersion), applicationPackage);
+ jobs.deploy(instanceId, type, Optional.ofNullable(vespaVersion), applicationPackage, false);
return runJob(type);
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializerTest.java
index 7623a02f6af..8bd22b5217f 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializerTest.java
@@ -1,4 +1,4 @@
-// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.persistence;
import com.google.common.collect.ImmutableMap;
@@ -151,8 +151,9 @@ public class RunSerializerTest {
assertEquals(run.testerCertificate(), phoenix.testerCertificate());
assertEquals(run.versions(), phoenix.versions());
assertEquals(run.steps(), phoenix.steps());
+ assertEquals(run.isDryRun(), phoenix.isDryRun());
- Run initial = Run.initial(id, run.versions(), run.isRedeployment(), run.start(), JobProfile.production);
+ Run initial = Run.initial(id, run.versions(), run.isRedeployment(), run.start(), JobProfile.production, true);
assertEquals(initial, serializer.runFromSlime(serializer.toSlime(initial)));
}
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 3a9e2cf84d4..01dd1a58f44 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
@@ -176,6 +176,19 @@ public class JobControllerApiHandlerHelperTest {
"jobs-direct-deployment.json");
}
+ @Test
+ public void testResponsesWithDryRunDeployment() {
+ var tester = new DeploymentTester();
+ var app = tester.newDeploymentContext();
+ tester.clock().setInstant(Instant.EPOCH);
+ var region = "us-west-1";
+ var applicationPackage = new ApplicationPackageBuilder().region(region).build();
+ // Deploy directly to production zone, like integration tests, with dryRun.
+ tester.controller().jobController().deploy(tester.instance().id(), productionUsWest1, Optional.empty(), applicationPackage, true);
+ assertResponse(JobControllerApiHandlerHelper.jobTypeResponse(tester.controller(), app.instanceId(), URI.create("https://some.url:43/root/")),
+ "jobs-direct-deployment.json");
+ }
+
private void compare(HttpResponse response, String expected) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
response.render(baos);