summaryrefslogtreecommitdiffstats
path: root/controller-server/src
diff options
context:
space:
mode:
Diffstat (limited to 'controller-server/src')
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java94
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java171
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java6
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java216
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationList.java85
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Change.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentJobs.java158
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentMetrics.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/JobList.java189
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/JobStatus.java44
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/AthenzPrincipalFilter.java21
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/SecurityFilterUtils.java32
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/UserAuthWithAthenzPrincipalFilter.java106
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentOrder.java85
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java317
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/PolledBuildSystem.java4
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmer.java92
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ClusterInfoMaintainer.java28
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ClusterUtilizationMaintainer.java20
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java18
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DelayedDeployer.java24
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporter.java13
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentMetricsMaintainer.java52
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/FailureRedeployer.java35
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/JobControl.java3
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/Maintainer.java1
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporter.java8
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ReadyJobsTrigger.java (renamed from controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/BlockedChangeDeployer.java)6
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/Upgrader.java39
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java29
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java60
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/MemoryControllerDb.java21
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializer.java8
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutor.java14
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutorImpl.java243
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ErrorResponse.java66
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ProxyException.java16
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ProxyRequest.java119
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ProxyResponse.java64
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/package-info.java8
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/Path.java7
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java268
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiHandler.java93
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AccessControlHeaders.java5
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/securitycontext/CreateSecurityContextFilter.java1
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/ScrewdriverApiHandler.java26
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiHandler.java131
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/package-info.java5
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiHandler.java116
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/package-info.java5
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/DeploymentStatistics.java44
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VersionStatus.java53
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VespaVersion.java7
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/rotation/ControllerRotationRepository.java26
-rw-r--r--controller-server/src/main/resources/configdefinitions/athenz.def4
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ConfigServerProxyMock.java43
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java107
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java9
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ZoneRegistryMock.java46
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTester.java75
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java49
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/MockBuildService.java5
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmerTest.java121
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporterTest.java6
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentMetricsMaintainerTest.java24
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/FailureRedeployerTest.java25
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporterTest.java3
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OutstandingChangeDeployerTest.java2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/UpgraderTest.java141
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/testdata/pr-instance-with-dead-locked-job.json2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java29
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializerTest.java2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/proxy/ProxyRequestTest.java83
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/proxy/ProxyResponseTest.java69
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerControllerTester.java6
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerTester.java17
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java10
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java599
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-without-change-multiple-deployments.json26
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application.json14
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application1-recursive.json161
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/dev-us-west-1.json63
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/opsdb-tenant-with-id-without-applications.json1
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/opsdb-tenant-with-new-id-without-applications.json1
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/prod-corp-us-east-1.json68
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/recursive-root.json5
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/recursive-until-tenant-root.json6
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-with-application.json1
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-without-applications-with-id.json1
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-without-applications.json1
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant1-recursive.json9
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant2.json19
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant3.json12
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/maintenance.json23
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/root.json96
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/ScrewdriverApiTest.java37
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/responses/unexpected-completion.json2
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiTest.java65
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/responses/default-for-region.json4
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/responses/no-default-region.json4
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/responses/prod.json6
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/responses/root.json18
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiTest.java117
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/responses/root.json26
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/responses/unknown-zone.json4
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/versions/VersionStatusTest.java50
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/rotation/ControllerRotationRepositoryTest.java15
107 files changed, 3831 insertions, 1807 deletions
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java
index 364e91f1828..e3400d76bce 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java
@@ -8,7 +8,11 @@ import com.yahoo.config.application.api.ValidationOverrides;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.Zone;
+import com.yahoo.vespa.hosted.controller.api.integration.MetricsService.ApplicationMetrics;
+import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
+import com.yahoo.vespa.hosted.controller.application.ApplicationRevision;
import com.yahoo.vespa.hosted.controller.application.Change;
+import com.yahoo.vespa.hosted.controller.application.Change.VersionChange;
import com.yahoo.vespa.hosted.controller.application.Deployment;
import com.yahoo.vespa.hosted.controller.application.DeploymentJobs;
@@ -38,26 +42,28 @@ public class Application {
private final DeploymentJobs deploymentJobs;
private final Optional<Change> deploying;
private final boolean outstandingChange;
+ private final Optional<IssueId> ownershipIssueId;
+ private final ApplicationMetrics metrics;
/** Creates an empty application */
public Application(ApplicationId id) {
this(id, DeploymentSpec.empty, ValidationOverrides.empty, ImmutableMap.of(),
new DeploymentJobs(Optional.empty(), Collections.emptyList(), Optional.empty()),
- Optional.empty(), false);
+ Optional.empty(), false, Optional.empty(), new ApplicationMetrics(0, 0));
}
/** Used from persistence layer: Do not use */
public Application(ApplicationId id, DeploymentSpec deploymentSpec, ValidationOverrides validationOverrides,
- List<Deployment> deployments,
- DeploymentJobs deploymentJobs, Optional<Change> deploying, boolean outstandingChange) {
+ List<Deployment> deployments, DeploymentJobs deploymentJobs, Optional<Change> deploying,
+ boolean outstandingChange, Optional<IssueId> ownershipIssueId, ApplicationMetrics metrics) {
this(id, deploymentSpec, validationOverrides,
deployments.stream().collect(Collectors.toMap(Deployment::zone, d -> d)),
- deploymentJobs, deploying, outstandingChange);
+ deploymentJobs, deploying, outstandingChange, ownershipIssueId, metrics);
}
Application(ApplicationId id, DeploymentSpec deploymentSpec, ValidationOverrides validationOverrides,
Map<Zone, Deployment> deployments, DeploymentJobs deploymentJobs, Optional<Change> deploying,
- boolean outstandingChange) {
+ boolean outstandingChange, Optional<IssueId> ownershipIssueId, ApplicationMetrics metrics) {
Objects.requireNonNull(id, "id cannot be null");
Objects.requireNonNull(deploymentSpec, "deploymentSpec cannot be null");
Objects.requireNonNull(validationOverrides, "validationOverrides cannot be null");
@@ -71,6 +77,8 @@ public class Application {
this.deploymentJobs = deploymentJobs;
this.deploying = deploying;
this.outstandingChange = outstandingChange;
+ this.ownershipIssueId = ownershipIssueId;
+ this.metrics = metrics;
}
public ApplicationId id() { return id; }
@@ -96,9 +104,9 @@ public class Application {
* (deployments also includes manually deployed environments)
*/
public Map<Zone, Deployment> productionDeployments() {
- return deployments.values().stream()
- .filter(deployment -> deployment.zone().environment() == Environment.prod)
- .collect(Collectors.toMap(Deployment::zone, Function.identity()));
+ return ImmutableMap.copyOf(deployments.values().stream()
+ .filter(deployment -> deployment.zone().environment() == Environment.prod)
+ .collect(Collectors.toMap(Deployment::zone, Function.identity())));
}
public DeploymentJobs deploymentJobs() { return deploymentJobs; }
@@ -115,39 +123,49 @@ public class Application {
*/
public boolean hasOutstandingChange() { return outstandingChange; }
- /**
+ public Optional<IssueId> ownershipIssueId() {
+ return ownershipIssueId;
+ }
+
+ public ApplicationMetrics metrics() {
+ return metrics;
+ }
+
+ /**
* Returns the oldest version this has deployed in a permanent zone (not test or staging),
* or empty version if it is not deployed anywhere
*/
- public Optional<Version> deployedVersion() {
+ public Optional<Version> oldestDeployedVersion() {
return productionDeployments().values().stream()
- .sorted(Comparator.comparing(Deployment::version))
- .findFirst()
- .map(Deployment::version);
- }
-
- /** The version that should be used to compile this application */
- public Version compileVersion(Controller controller) {
- return deployedVersion().orElse(controller.systemVersion());
+ .map(Deployment::version)
+ .min(Comparator.naturalOrder());
}
- /** Returns the version a deployment to this zone should use for this application */
- public Version currentDeployVersion(Controller controller, Zone zone) {
- if ( ! deploying().isPresent())
- return currentVersion(controller, zone);
- else if ( deploying().get() instanceof Change.ApplicationChange)
- return currentVersion(controller, zone);
- else
+ /** Returns the version a new deployment to this zone should use for this application */
+ public Version deployVersionIn(Zone zone, Controller controller) {
+ if (deploying().isPresent() && deploying().get() instanceof VersionChange)
return ((Change.VersionChange) deploying().get()).version();
+
+ return versionIn(zone, controller);
}
/** Returns the current version this application has, or if none; should use, in the given zone */
- public Version currentVersion(Controller controller, Zone zone) {
- Deployment currentDeployment = deployments().get(zone);
- if (currentDeployment != null) // Already deployed in this zone: Use that version
- return currentDeployment.version();
+ public Version versionIn(Zone zone, Controller controller) {
+ return Optional.ofNullable(deployments().get(zone)).map(Deployment::version) // Already deployed in this zone: Use that version
+ .orElse(oldestDeployedVersion().orElse(controller.systemVersion()));
+ }
- return deployedVersion().orElse(controller.systemVersion());
+ /** Returns the revision a new deployment to this zone should use for this application, or empty if we don't know */
+ public Optional<ApplicationRevision> deployRevisionIn(Zone zone) {
+ if (deploying().isPresent() && deploying().get() instanceof Change.ApplicationChange)
+ return ((Change.ApplicationChange) deploying().get()).revision();
+
+ return revisionIn(zone);
+ }
+
+ /** Returns the revision this application is or should be deployed with in the given zone, or empty if unknown. */
+ public Optional<ApplicationRevision> revisionIn(Zone zone) {
+ return Optional.ofNullable(deployments().get(zone)).map(Deployment::revision);
}
@Override
@@ -170,20 +188,8 @@ public class Application {
return "application '" + id + "'";
}
- /** Returns true if there is no current change to deploy - i.e deploying is empty or completely deployed */
- public boolean deployingCompleted() {
- if ( ! deploying.isPresent()) return true;
- return deploymentJobs().isDeployed(deploying.get());
- }
-
- /** Returns true if there is a current change which is blocked from being deployed to production at this instant */
- public boolean deployingBlocked(Instant instant) {
- if ( ! deploying.isPresent()) return false;
- return deploying.get().blockedBy(deploymentSpec, instant);
- }
-
public boolean isBlocked(Instant instant) {
- return ! deploymentSpec.canUpgradeAt(instant) || ! deploymentSpec.canChangeRevisionAt(instant);
+ return ! deploymentSpec().canUpgradeAt(instant) || ! deploymentSpec().canChangeRevisionAt(instant);
}
-
+
}
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 26debf3083f..841b9b4dd9f 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
@@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.controller;
import com.google.common.collect.ImmutableSet;
import com.yahoo.component.Version;
+import com.yahoo.config.application.api.DeploymentSpec;
import com.yahoo.config.application.api.ValidationId;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.Environment;
@@ -62,6 +63,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
+import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
@@ -109,14 +111,8 @@ public class ApplicationController {
this.deploymentTrigger = new DeploymentTrigger(controller, curator, clock);
- for (Application application : db.listApplications()) {
- try (Lock lock = lock(application.id())) {
- Optional<LockedApplication> lockedApplication = db.getApplication(application.id())
- .map(app -> new LockedApplication(app, lock));
- if ( ! lockedApplication.isPresent()) continue; // was removed since listing; ok
- store(lockedApplication.get()); // re-write all applications to update storage format
- }
- }
+ for (Application application : db.listApplications())
+ lockedIfPresent(application.id(), this::store);
}
/** Returns the application with the given id, or null if it is not present */
@@ -124,12 +120,6 @@ public class ApplicationController {
return db.getApplication(id);
}
-
- /** Returns an locked application with the given id that be updated and stored */
- public Optional<LockedApplication> get(ApplicationId id, Lock lock) {
- return db.getApplication(id).map(application -> new LockedApplication(application, lock));
- }
-
/**
* Returns the application with the given id
*
@@ -139,16 +129,6 @@ public class ApplicationController {
return get(id).orElseThrow(() -> new IllegalArgumentException(id + " not found"));
}
- /**
- * Returns a locked application that be updated and stored
- *
- * @throws IllegalArgumentException if it does not exist
- *
- */
- public LockedApplication require(ApplicationId id, Lock lock) {
- return get(id, lock).orElseThrow(() -> new IllegalArgumentException(id + " not found"));
- }
-
/** Returns a snapshot of all applications */
public List<Application> asList() {
return db.listApplications();
@@ -251,6 +231,7 @@ public class ApplicationController {
if ( ! (id.instance().value().equals("default") || id.instance().value().startsWith("default-pr"))) // TODO: Support instances properly
throw new UnsupportedOperationException("Only the instance names 'default' and names starting with 'default-pr' are supported at the moment");
try (Lock lock = lock(id)) {
+ // TODO: Throwing is duplicated below.
if (get(id).isPresent())
throw new IllegalArgumentException("An application with id '" + id + "' already exists");
@@ -288,22 +269,23 @@ public class ApplicationController {
public ActivateResult deployApplication(ApplicationId applicationId, Zone zone,
ApplicationPackage applicationPackage, DeployOptions options) {
try (Lock lock = lock(applicationId)) {
- // Determine what we are doing
- LockedApplication application = get(applicationId, lock).orElse(new LockedApplication(
+ // TODO: Shouldn't this go through the above method? Seems you can cheat the checks here ... ?
+ LockedApplication application = get(applicationId).map(application1 -> new LockedApplication(application1, lock)).orElse(new LockedApplication(
new Application(applicationId), lock)
- );
+ );
+ // Determine what we are doing
Version version;
if (options.deployCurrentVersion)
- version = application.currentVersion(controller, zone);
+ version = application.versionIn(zone, controller);
else if (canDeployDirectlyTo(zone, options))
version = options.vespaVersion.map(Version::new).orElse(controller.systemVersion());
else if ( ! application.deploying().isPresent() && ! zone.environment().isManuallyDeployed())
return unexpectedDeployment(applicationId, zone, applicationPackage);
else
- version = application.currentDeployVersion(controller, zone);
+ version = application.deployVersionIn(zone, controller);
- DeploymentJobs.JobType jobType = DeploymentJobs.JobType.from(controller.zoneRegistry().system(), zone);
+ Optional<DeploymentJobs.JobType> jobType = DeploymentJobs.JobType.from(controller.system(), zone);
ApplicationRevision revision = toApplicationPackageRevision(applicationPackage, options.screwdriverBuildJob);
if ( ! options.deployCurrentVersion) {
@@ -314,17 +296,17 @@ public class ApplicationController {
application = application.withProjectId(options.screwdriverBuildJob.get().screwdriverId.value());
if (application.deploying().isPresent() && application.deploying().get() instanceof Change.ApplicationChange)
application = application.withDeploying(Optional.of(Change.ApplicationChange.of(revision)));
- if ( ! canDeployDirectlyTo(zone, options) && jobType != null) {
- // Update with (potentially) missing information about what we triggered
- JobStatus.JobRun triggering = getOrCreateTriggering(application, version, jobType);
- application = application.with(application.deploymentJobs()
- .withTriggering(jobType,
- application.deploying(),
- triggering.id(),
- version,
- Optional.of(revision),
- triggering.reason(),
- triggering.at()));
+ if ( ! canDeployDirectlyTo(zone, options) && jobType.isPresent()) {
+ // Update with (potentially) missing information about what we triggered:
+ // * When someone else triggered the job, we need to store a stand-in triggering event.
+ // * When this is the system test job, we need to record the new revision, for future use.
+ JobStatus.JobRun triggering = getOrCreateTriggering(application, version, jobType.get());
+ application = application.withJobTriggering(jobType.get(),
+ application.deploying(),
+ triggering.at(),
+ version,
+ Optional.of(revision),
+ triggering.reason());
}
// Delete zones not listed in DeploymentSpec, if allowed
@@ -357,14 +339,8 @@ public class ApplicationController {
configserverClient.prepare(deploymentId, options, rotationInDns.cnames(), rotationInDns.rotations(),
applicationPackage.zippedContent());
preparedApplication.activate();
+ application = application.withNewDeployment(zone, revision, version, clock.instant());
- // Use info from previous deployments is available
- Deployment previousDeployment = application.deployments().getOrDefault(zone, new Deployment(zone, revision, version, clock.instant()));
- Deployment newDeployment = new Deployment(zone, revision, version, clock.instant(),
- previousDeployment.clusterUtils(),
- previousDeployment.clusterInfo(), previousDeployment.metrics());
-
- application = application.with(newDeployment);
store(application);
return new ActivateResult(new RevisionId(applicationPackage.hash()), preparedApplication.prepareResponse());
@@ -423,10 +399,9 @@ public class ApplicationController {
* This is needed (only) in the case where some external entity triggers a job.
*/
private JobStatus.JobRun getOrCreateTriggering(Application application, Version version, DeploymentJobs.JobType jobType) {
- if (jobType == null) return incompleteTriggeringEvent(version);
JobStatus status = application.deploymentJobs().jobStatus().get(jobType);
- if (status == null) return incompleteTriggeringEvent(version);
- if ( ! status.lastTriggered().isPresent()) return incompleteTriggeringEvent(version);
+ if (status == null) return incompleteTriggeringEvent(version);
+ if ( ! status.lastTriggered().isPresent()) return incompleteTriggeringEvent(version);
return status.lastTriggered().get();
}
@@ -501,7 +476,7 @@ public class ApplicationController {
return Optional.of(new InstanceEndpoints(endPointUrls));
}
catch (RuntimeException e) {
- log.log(Level.WARNING, "Failed to get endpoint information for " + deploymentId, e);
+ log.log(Level.FINE, "Failed to get endpoint information for " + deploymentId, e);
return Optional.empty();
}
}
@@ -509,14 +484,15 @@ public class ApplicationController {
/**
* Deletes the application with this id
*
- * @return the deleted application, or null if it did not exist
* @throws IllegalArgumentException if the application has deployments or the caller is not authorized
+ * @throws NotExistsException if the application does not exist
*/
- public Application deleteApplication(ApplicationId id, Optional<NToken> token) {
- try (Lock lock = lock(id)) {
- Optional<Application> application = get(id);
- if ( ! application.isPresent()) return null;
- if ( ! application.get().deployments().isEmpty())
+ public void deleteApplication(ApplicationId id, Optional<NToken> token) {
+ if ( ! controller.applications().get(id).isPresent())
+ throw new NotExistsException("Could not delete application '" + id + "': Application not found");
+
+ lockedOrThrow(id, application -> {
+ if ( ! application.deployments().isEmpty())
throw new IllegalArgumentException("Could not delete '" + application + "': It has active deployments");
Tenant tenant = controller.tenants().tenant(new TenantId(id.tenant().value())).get();
@@ -529,9 +505,8 @@ public class ApplicationController {
.deleteApplication(tenant.getAthensDomain().get(), new com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId(id.application().value()));
db.deleteApplication(id);
- log.info("Deleted " + application.get());
- return application.get();
- }
+ log.info("Deleted " + application);
+ });
}
/**
@@ -543,6 +518,30 @@ public class ApplicationController {
db.store(application);
}
+ /**
+ * Acquire a locked application to modify and store, if there is an application with the given id.
+ *
+ * @param applicationId Id of the application to lock and get.
+ * @param actions Things to do with the locked application.
+ */
+ public void lockedIfPresent(ApplicationId applicationId, Consumer<LockedApplication> actions) {
+ try (Lock lock = lock(applicationId)) {
+ get(applicationId).map(application -> new LockedApplication(application, lock)).ifPresent(actions);
+ }
+ }
+
+ /**
+ * Acquire a locked application to modify and store, or throw an exception if no application has the given id.
+ *
+ * @param applicationId Id of the application to lock and require.
+ * @param actions Things to do with the locked application.
+ */
+ public void lockedOrThrow(ApplicationId applicationId, Consumer<LockedApplication> actions) {
+ try (Lock lock = lock(applicationId)) {
+ actions.accept(new LockedApplication(require(applicationId), lock));
+ }
+ }
+
public void notifyJobCompletion(JobReport report) {
if ( ! get(report.applicationId()).isPresent()) {
log.log(Level.WARNING, "Ignoring completion of job of project '" + report.projectId() +
@@ -582,20 +581,17 @@ public class ApplicationController {
private void deactivate(Application application, Zone zone, Optional<Deployment> deployment,
boolean requireThatDeploymentHasExpired) {
- try (Lock lock = lock(application.id())) {
- LockedApplication lockedApplication = controller.applications().require(application.id(), lock);
- if (deployment.isPresent() && requireThatDeploymentHasExpired &&
- ! DeploymentExpirer.hasExpired(controller.zoneRegistry(), deployment.get(), clock.instant())) {
- return;
- }
- lockedApplication = deactivate(lockedApplication, zone);
- store(lockedApplication);
- }
+ if (requireThatDeploymentHasExpired && deployment.isPresent()
+ && ! DeploymentExpirer.hasExpired(controller.zoneRegistry(), deployment.get(), clock.instant()))
+ return;
+
+ lockedOrThrow(application.id(), lockedApplication ->
+ store(deactivate(lockedApplication, zone)));
}
- /**
+ /**
* Deactivates a locked application without storing it
- *
+ *
* @return the application with the deployment in the given zone removed
*/
private LockedApplication deactivate(LockedApplication application, Zone zone) {
@@ -611,19 +607,19 @@ public class ApplicationController {
public DeploymentTrigger deploymentTrigger() { return deploymentTrigger; }
private ApplicationId dashToUnderscore(ApplicationId id) {
- return ApplicationId.from(id.tenant().value(),
+ return ApplicationId.from(id.tenant().value(),
id.application().value().replaceAll("-", "_"),
id.instance().value());
}
-
+
public ConfigServerClient configserverClient() { return configserverClient; }
-
- /**
+
+ /**
* Returns a lock which provides exclusive rights to changing this application.
* Any operation which stores an application need to first acquire this lock, then read, modify
* and store the application, and finally release (close) the lock.
*/
- public Lock lock(ApplicationId application) {
+ Lock lock(ApplicationId application) {
return curator.lock(application, Duration.ofMinutes(10));
}
@@ -634,19 +630,30 @@ public class ApplicationController {
zone.environment().isManuallyDeployed();
}
+ /** Verify that each of the production zones listed in the deployment spec exist in this system. */
+ public void validate(DeploymentSpec deploymentSpec) {
+ deploymentSpec.zones().stream()
+ .filter(zone -> zone.environment() == Environment.prod)
+ .forEach(zone -> {
+ if ( ! controller.zoneRegistry().getZone(zone.environment(), zone.region().orElse(null)).isPresent())
+ throw new IllegalArgumentException("Zone " + zone + " in deployment spec was not found in this system!");
+ });
+ }
+
+
private static final class ApplicationRotation {
-
+
private final ImmutableSet<String> cnames;
private final ImmutableSet<Rotation> rotations;
-
+
public ApplicationRotation(Set<String> cnames, Set<Rotation> rotations) {
this.cnames = ImmutableSet.copyOf(cnames);
this.rotations = ImmutableSet.copyOf(rotations);
}
-
+
public Set<String> cnames() { return cnames; }
public Set<Rotation> rotations() { return rotations; }
-
+
}
-
+
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java
index 660013daa62..b854ad3f771 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java
@@ -156,8 +156,8 @@ public class Controller extends AbstractComponent {
public Clock clock() { return clock; }
- public URI getElkUri(Environment environment, RegionName region, DeploymentId deploymentId) {
- return elkUrl(zoneRegistry.getLogServerUri(environment, region), deploymentId);
+ public URI getElkUri(DeploymentId deploymentId) {
+ return elkUrl(zoneRegistry.getLogServerUri(deploymentId.zone().environment(), deploymentId.zone().region()), deploymentId);
}
public List<URI> getConfigServerUris(Environment environment, RegionName region) {
@@ -204,7 +204,7 @@ public class Controller extends AbstractComponent {
}
// TODO: Model the response properly
- // TODO: What is this
+ // TODO: What is this -- I believe it fetches, and purges, errors from some log server
public JsonNode grabLog(DeploymentId deploymentId) {
return configServerClient.grabLog(deploymentId);
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java
index b0424282ace..e8c8f8a389c 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java
@@ -3,18 +3,26 @@ package com.yahoo.vespa.hosted.controller;
import com.yahoo.component.Version;
import com.yahoo.config.application.api.DeploymentSpec;
import com.yahoo.config.application.api.ValidationOverrides;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.Zone;
import com.yahoo.vespa.curator.Lock;
+import com.yahoo.vespa.hosted.controller.api.integration.MetricsService;
+import com.yahoo.vespa.hosted.controller.api.integration.MetricsService.ApplicationMetrics;
import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
import com.yahoo.vespa.hosted.controller.application.ApplicationRevision;
import com.yahoo.vespa.hosted.controller.application.Change;
+import com.yahoo.vespa.hosted.controller.application.ClusterInfo;
+import com.yahoo.vespa.hosted.controller.application.ClusterUtilization;
import com.yahoo.vespa.hosted.controller.application.Deployment;
import com.yahoo.vespa.hosted.controller.application.DeploymentJobs;
+import com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobType;
+import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics;
+
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.Map;
-import java.util.Objects;
import java.util.Optional;
/**
@@ -22,147 +30,159 @@ import java.util.Optional;
* fields.
*
* @author mpolden
+ * @author jvenstad
*/
public class LockedApplication extends Application {
- private final Lock lock;
+ private LockedApplication(Builder builder) {
+ super(builder.applicationId, builder.deploymentSpec, builder.validationOverrides,
+ builder.deployments, builder.deploymentJobs, builder.deploying,
+ builder.hasOutstandingChange, builder.ownershipIssueId, builder.metrics);
+ }
/**
- * LockedApplication should be acquired through ApplicationController and never constructed directly
+ * Used to create a locked application
*
- * @param application Application instance for which lock has been acquired
- * @param lock Unused, but must be held when constructing this
+ * @param application The application to lock.
+ * @param lock The lock for the application.
*/
LockedApplication(Application application, Lock lock) {
- super(application.id(), application.deploymentSpec(), application.validationOverrides(),
- application.deployments(), application.deploymentJobs(), application.deploying(),
- application.hasOutstandingChange());
- this.lock = Objects.requireNonNull(lock, "lock cannot be null");
+ this(new Builder(application));
}
public LockedApplication withProjectId(long projectId) {
- return new LockedApplication(new Application(id(), deploymentSpec(), validationOverrides(), deployments(),
- deploymentJobs().withProjectId(projectId), deploying(),
- hasOutstandingChange()), lock);
+ return new LockedApplication(new Builder(this).with(deploymentJobs().withProjectId(projectId)));
}
- public LockedApplication with(IssueId issueId) {
- return new LockedApplication(new Application(id(), deploymentSpec(), validationOverrides(), deployments(),
- deploymentJobs().with(issueId), deploying(),
- hasOutstandingChange()), lock);
+ public LockedApplication withDeploymentIssueId(IssueId issueId) {
+ return new LockedApplication(new Builder(this).with(deploymentJobs().with(issueId)));
}
- public LockedApplication withJobCompletion(DeploymentJobs.JobReport report, Instant notificationTime,
- Controller controller) {
- return new LockedApplication(new Application(id(), deploymentSpec(), validationOverrides(),
- deployments(),
- deploymentJobs().withCompletion(report, notificationTime,
- controller),
- deploying(), hasOutstandingChange()), lock);
+ public LockedApplication withJobCompletion(DeploymentJobs.JobReport report, Instant notificationTime, Controller controller) {
+ return new LockedApplication(new Builder(this).with(deploymentJobs().withCompletion(report, notificationTime, controller)));
}
- public LockedApplication withJobTriggering(long runId, DeploymentJobs.JobType type, Optional<Change> change,
- String reason, Instant triggerTime, Controller controller) {
- return new LockedApplication(new Application(id(), deploymentSpec(), validationOverrides(), deployments(),
- deploymentJobs().withTriggering(type,
- change,
- runId,
- determineTriggerVersion(type, controller),
- determineTriggerRevision(type, controller),
- reason,
- triggerTime),
- deploying(), hasOutstandingChange()), lock);
+ public LockedApplication withJobTriggering(JobType type, Optional<Change> change, Instant triggerTime,
+ Version version, Optional<ApplicationRevision> revision, String reason) {
+ return new LockedApplication(new Builder(this).with(deploymentJobs().withTriggering(type, change, version, revision, reason, triggerTime)));
}
- public LockedApplication with(Deployment deployment) {
- Map<Zone, Deployment> deployments = new LinkedHashMap<>(deployments());
- deployments.put(deployment.zone(), deployment);
- return new LockedApplication(new Application(id(), deploymentSpec(), validationOverrides(),
- deployments, deploymentJobs(), deploying(),
- hasOutstandingChange()), lock);
+ public LockedApplication withNewDeployment(Zone zone, ApplicationRevision revision, Version version, Instant instant) {
+ // Use info from previous deployment if available, otherwise create a new one.
+ Deployment previousDeployment = deployments().getOrDefault(zone, new Deployment(zone, revision, version, instant));
+ Deployment newDeployment = new Deployment(zone, revision, version, instant,
+ previousDeployment.clusterUtils(),
+ previousDeployment.clusterInfo(),
+ previousDeployment.metrics());
+ return with(newDeployment);
+ }
+
+ public LockedApplication withClusterUtilization(Zone zone, Map<ClusterSpec.Id, ClusterUtilization> clusterUtilization) {
+ Deployment deployment = deployments().get(zone);
+ if (deployment == null) return this; // No longer deployed in this zone.
+ return with(deployment.withClusterUtils(clusterUtilization));
}
- public LockedApplication with(DeploymentJobs deploymentJobs) {
- return new LockedApplication(new Application(id(), deploymentSpec(), validationOverrides(),
- deployments(), deploymentJobs, deploying(),
- hasOutstandingChange()), lock);
+ public LockedApplication withClusterInfo(Zone zone, Map<ClusterSpec.Id, ClusterInfo> clusterInfo) {
+ Deployment deployment = deployments().get(zone);
+ if (deployment == null) return this; // No longer deployed in this zone.
+ return with(deployment.withClusterInfo(clusterInfo));
+
+ }
+
+ public LockedApplication with(Zone zone, DeploymentMetrics deploymentMetrics) {
+ Deployment deployment = deployments().get(zone);
+ if (deployment == null) return this; // No longer deployed in this zone.
+ return with(deployment.withMetrics(deploymentMetrics));
}
public LockedApplication withoutDeploymentIn(Zone zone) {
Map<Zone, Deployment> deployments = new LinkedHashMap<>(deployments());
deployments.remove(zone);
- return new LockedApplication(new Application(id(), deploymentSpec(), validationOverrides(),
- deployments, deploymentJobs(), deploying(),
- hasOutstandingChange()), lock);
+ return new LockedApplication(new Builder(this).with(deployments));
}
public LockedApplication withoutDeploymentJob(DeploymentJobs.JobType jobType) {
- DeploymentJobs deploymentJobs = deploymentJobs().without(jobType);
- return new LockedApplication(new Application(id(), deploymentSpec(), validationOverrides(),
- deployments(), deploymentJobs, deploying(),
- hasOutstandingChange()), lock);
+ return new LockedApplication(new Builder(this).with(deploymentJobs().without(jobType)));
}
public LockedApplication with(DeploymentSpec deploymentSpec) {
- return new LockedApplication(new Application(id(), deploymentSpec, validationOverrides(),
- deployments(), deploymentJobs(), deploying(),
- hasOutstandingChange()), lock);
+ return new LockedApplication(new Builder(this).with(deploymentSpec));
}
public LockedApplication with(ValidationOverrides validationOverrides) {
- return new LockedApplication(new Application(id(), deploymentSpec(), validationOverrides,
- deployments(), deploymentJobs(), deploying(),
- hasOutstandingChange()), lock);
+ return new LockedApplication(new Builder(this).with(validationOverrides));
}
public LockedApplication withDeploying(Optional<Change> deploying) {
- return new LockedApplication(new Application(id(), deploymentSpec(), validationOverrides(),
- deployments(), deploymentJobs(), deploying,
- hasOutstandingChange()), lock);
+ return new LockedApplication(new Builder(this).withDeploying(deploying));
}
public LockedApplication withOutstandingChange(boolean outstandingChange) {
- return new LockedApplication(new Application(id(), deploymentSpec(),
- validationOverrides(), deployments(),
- deploymentJobs(), deploying(), outstandingChange), lock);
- }
-
- private Version determineTriggerVersion(DeploymentJobs.JobType jobType, Controller controller) {
- Optional<Zone> zone = jobType.zone(controller.system());
- if ( ! zone.isPresent()) // a sloppy test TODO: Fix
- return controller.systemVersion();
- return currentDeployVersion(controller, zone.get());
- }
-
- private Optional<ApplicationRevision> determineTriggerRevision(DeploymentJobs.JobType jobType,
- Controller controller) {
- Optional<Zone> zone = jobType.zone(controller.system());
- if ( ! zone.isPresent()) // a sloppy test TODO: Fix
- return Optional.empty();
- return currentDeployRevision(jobType.zone(controller.system()).get());
- }
-
- /** Returns the version a deployment to this zone should use for this application, or empty if we don't know */
- private Optional<ApplicationRevision> currentDeployRevision(Zone zone) {
- if (!deploying().isPresent()) {
- return currentRevision(zone);
- } else if (deploying().get() instanceof Change.VersionChange) {
- return currentRevision(zone);
- } else {
- return ((Change.ApplicationChange) deploying().get()).revision();
- }
+ return new LockedApplication(new Builder(this).with(outstandingChange));
}
- /**
- * Returns the current revision this application has, or if none; should use assuming no change,
- * in the given zone. Empty if not known
- */
- private Optional<ApplicationRevision> currentRevision(Zone zone) {
- Deployment currentDeployment = deployments().get(zone);
- if (currentDeployment != null) { // Already deployed in this zone: Use that revision
- return Optional.of(currentDeployment.revision());
+ public LockedApplication withOwnershipIssueId(IssueId issueId) {
+ return new LockedApplication(new Builder(this).withOwnershipIssueId(Optional.ofNullable(issueId)));
+ }
+
+ public LockedApplication with(MetricsService.ApplicationMetrics metrics) {
+ return new LockedApplication(new Builder(this).with(metrics));
+ }
+
+ public Version deployVersionFor(DeploymentJobs.JobType jobType, Controller controller) {
+ return jobType == JobType.component
+ ? controller.systemVersion()
+ : deployVersionIn(jobType.zone(controller.system()).get(), controller);
+ }
+
+ public Optional<ApplicationRevision> deployRevisionFor(DeploymentJobs.JobType jobType, Controller controller) {
+ return jobType == JobType.component
+ ? Optional.empty()
+ : deployRevisionIn(jobType.zone(controller.system()).get());
+ }
+
+ /** Don't expose non-leaf sub-objects. */
+ private LockedApplication with(Deployment deployment) {
+ Map<Zone, Deployment> deployments = new LinkedHashMap<>(deployments());
+ deployments.put(deployment.zone(), deployment);
+ return new LockedApplication(new Builder(this).with(deployments));
+ }
+
+
+ private static class Builder {
+
+ private final ApplicationId applicationId;
+ private DeploymentSpec deploymentSpec;
+ private ValidationOverrides validationOverrides;
+ private Map<Zone, Deployment> deployments;
+ private DeploymentJobs deploymentJobs;
+ private Optional<Change> deploying;
+ private boolean hasOutstandingChange;
+ private Optional<IssueId> ownershipIssueId;
+ private ApplicationMetrics metrics;
+
+ private Builder(Application application) {
+ this.applicationId = application.id();
+ this.deploymentSpec = application.deploymentSpec();
+ this.validationOverrides = application.validationOverrides();
+ this.deployments = application.deployments();
+ this.deploymentJobs = application.deploymentJobs();
+ this.deploying = application.deploying();
+ this.hasOutstandingChange = application.hasOutstandingChange();
+ this.ownershipIssueId = application.ownershipIssueId();
+ this.metrics = application.metrics();
}
- return Optional.empty();
+
+ private Builder with(DeploymentSpec deploymentSpec) { this.deploymentSpec = deploymentSpec; return this; }
+ private Builder with(ValidationOverrides validationOverrides) { this.validationOverrides = validationOverrides; return this; }
+ private Builder with(Map<Zone, Deployment> deployments) { this.deployments = deployments; return this; }
+ private Builder with(DeploymentJobs deploymentJobs) { this.deploymentJobs = deploymentJobs; return this; }
+ private Builder withDeploying(Optional<Change> deploying) { this.deploying = deploying; return this; }
+ private Builder with(boolean hasOutstandingChange) { this.hasOutstandingChange = hasOutstandingChange; return this; }
+ private Builder withOwnershipIssueId(Optional<IssueId> ownershipIssueId) { this.ownershipIssueId = ownershipIssueId; return this; }
+ private Builder with(ApplicationMetrics metrics) { this.metrics = metrics; return this; }
+
}
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationList.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationList.java
index 710d2ad6492..4d1a009806f 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationList.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationList.java
@@ -9,6 +9,7 @@ import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.ApplicationController;
import java.time.Instant;
+import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
@@ -23,17 +24,17 @@ public class ApplicationList {
private final ImmutableList<Application> list;
- private ApplicationList(List<Application> applications) {
+ private ApplicationList(Iterable<Application> applications) {
this.list = ImmutableList.copyOf(applications);
}
// ----------------------------------- Factories
- public static ApplicationList from(List<Application> applications) {
+ public static ApplicationList from(Iterable<Application> applications) {
return new ApplicationList(applications);
}
- public static ApplicationList from(List<ApplicationId> ids, ApplicationController applications) {
+ public static ApplicationList from(Collection<ApplicationId> ids, ApplicationController applications) {
return listOf(ids.stream().map(applications::require));
}
@@ -42,6 +43,9 @@ public class ApplicationList {
/** Returns the applications in this as an immutable list */
public List<Application> asList() { return list; }
+ /** Returns the ids of the applications in this as an immutable list */
+ public List<ApplicationId> idList() { return ImmutableList.copyOf(list.stream().map(Application::id)::iterator); }
+
public boolean isEmpty() { return list.isEmpty(); }
public int size() { return list.size(); }
@@ -82,11 +86,21 @@ public class ApplicationList {
return listOf(list.stream().filter(application -> ! isDeployingApplicationChange(application)));
}
+ /** Returns the subset of applications which is currently not deploying a change */
+ public ApplicationList notDeploying() {
+ return listOf(list.stream().filter(application -> ! application.deploying().isPresent()));
+ }
+
/** Returns the subset of applications which currently does not have any failing jobs */
public ApplicationList notFailing() {
return listOf(list.stream().filter(application -> ! application.deploymentJobs().hasFailures()));
}
+ /** Returns the subset of applications which currently have failing jobs */
+ public ApplicationList failing() {
+ return listOf(list.stream().filter(application -> application.deploymentJobs().hasFailures()));
+ }
+
/** Returns the subset of applications which have been failing an upgrade to the given version since the given instant */
public ApplicationList failingUpgradeToVersionSince(Version version, Instant threshold) {
return listOf(list.stream().filter(application -> failingUpgradeToVersionSince(application, version, threshold)));
@@ -102,14 +116,14 @@ public class ApplicationList {
return listOf(list.stream().filter(application -> ! failingOn(version, application)));
}
- /** Returns the subset of applications which have at least one deployment */
+ /** Returns the subset of applications which have at least one production deployment */
public ApplicationList hasDeployment() {
return listOf(list.stream().filter(a -> !a.productionDeployments().isEmpty()));
}
/** Returns the subset of applications which started failing after the given instant */
- public ApplicationList startedFailingAfter(Instant instant) {
- return listOf(list.stream().filter(application -> application.deploymentJobs().failingSince().isAfter(instant)));
+ public ApplicationList startedFailingOnVersionAfter(Version version, Instant instant) {
+ return listOf(list.stream().filter(application -> JobList.from(application).firstFailing().on(version).firstFailing().after(instant).anyMatch()));
}
/** Returns the subset of applications which has the given upgrade policy */
@@ -161,12 +175,12 @@ public class ApplicationList {
* Applications without any deployments are ordered first.
*/
public ApplicationList byIncreasingDeployedVersion() {
- return listOf(list.stream().sorted(Comparator.comparing(application -> application.deployedVersion().orElse(Version.emptyVersion))));
+ return listOf(list.stream().sorted(Comparator.comparing(application -> application.oldestDeployedVersion().orElse(Version.emptyVersion))));
}
/** Returns the subset of applications that are not currently upgrading */
public ApplicationList notCurrentlyUpgrading(Change.VersionChange change, Instant jobTimeoutLimit) {
- return listOf(list.stream().filter(a -> !currentlyUpgrading(change, a, jobTimeoutLimit)));
+ return listOf(list.stream().filter(a -> ! currentlyUpgrading(change, a, jobTimeoutLimit)));
}
// ----------------------------------- Internal helpers
@@ -193,56 +207,39 @@ public class ApplicationList {
if ( ! application.deploying().isPresent()) return false;
return application.deploying().get() instanceof Change.ApplicationChange;
}
-
+
private static boolean failingOn(Version version, Application application) {
- for (JobStatus jobStatus : application.deploymentJobs().jobStatus().values())
- if ( ! jobStatus.isSuccess() && jobStatus.lastCompleted().get().version().equals(version)) return true;
- return false;
+ return JobList.from(application)
+ .failing()
+ .lastCompleted().on(version)
+ .anyMatch();
}
private static boolean currentlyUpgrading(Change.VersionChange change, Application application, Instant jobTimeoutLimit) {
- return application.deploymentJobs().jobStatus().values().stream()
- .filter(status -> status.isRunning(jobTimeoutLimit))
- .filter(status -> status.lastTriggered().isPresent())
- .map(status -> status.lastTriggered().get())
- .anyMatch(jobRun -> jobRun.version().equals(change.version()));
+ return JobList.from(application)
+ .running(jobTimeoutLimit)
+ .lastTriggered().on(change.version())
+ .anyMatch();
}
private static boolean failingUpgradeToVersionSince(Application application, Version version, Instant threshold) {
- return application.deploymentJobs().jobStatus().values().stream()
- .filter(job -> isUpgradeFailure(job))
- .filter(job -> job.firstFailing().get().at().isBefore(threshold))
- .anyMatch(job -> job.lastCompleted().get().version().equals(version));
+ return JobList.from(application)
+ .not().failingApplicationChange()
+ .firstFailing().before(threshold)
+ .lastCompleted().on(version)
+ .anyMatch();
}
private static boolean failingApplicationChangeSince(Application application, Instant threshold) {
- return application.deploymentJobs().jobStatus().values().stream()
- .filter(job -> isApplicationChangeFailure(job))
- .anyMatch(job -> job.firstFailing().get().at().isBefore(threshold));
- }
-
- private static boolean isUpgradeFailure(JobStatus job) {
- if ( job.isSuccess()) return false;
- if ( ! job.lastSuccess().isPresent()) return false; // An application which never succeeded is surely bad.
- if ( ! job.lastSuccess().get().revision().isPresent()) return false; // Indicates the component job, which is not an upgrade.
- if ( ! job.firstFailing().get().revision().equals(job.lastSuccess().get().revision())) return false; // Application change may be to blame.
- return ! job.firstFailing().get().version().equals(job.lastSuccess().get().version()); // Return whether there is a version change.
+ return JobList.from(application)
+ .failingApplicationChange()
+ .firstFailing().before(threshold)
+ .anyMatch();
}
- private static boolean isApplicationChangeFailure(JobStatus job) {
- if ( job.isSuccess()) return false;
- if ( ! job.lastSuccess().isPresent()) return true; // An application which never succeeded is surely bad.
- if ( ! job.lastSuccess().get().revision().isPresent()) return true; // Indicates the component job, which is always an application change.
- if ( ! job.firstFailing().get().version().equals(job.lastSuccess().get().version())) return false; // Version change may be to blame.
- return ! job.firstFailing().get().revision().equals(job.lastSuccess().get().revision()); // Return whether there is an application change.
- }
-
-
/** Convenience converter from a stream to an ApplicationList */
private static ApplicationList listOf(Stream<Application> applications) {
- ImmutableList.Builder<Application> b = new ImmutableList.Builder<>();
- applications.forEach(b::add);
- return new ApplicationList(b.build());
+ return from(applications::iterator);
}
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Change.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Change.java
index bf067a39bbb..d9c22018d26 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Change.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Change.java
@@ -33,7 +33,7 @@ public abstract class Change {
@Override
public boolean blockedBy(DeploymentSpec deploymentSpec, Instant instant) {
- return ! deploymentSpec.canChangeRevisionAt(instant);
+ return ! deploymentSpec.canChangeRevisionAt(instant);
}
@Override
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentJobs.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentJobs.java
index 8f654c66871..98f8c2a3d99 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentJobs.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentJobs.java
@@ -13,12 +13,14 @@ import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
import java.time.Instant;
import java.util.Collection;
-import java.util.Collections;
+import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
/**
* Information about which deployment jobs an application should run and their current status.
@@ -66,7 +68,6 @@ public class DeploymentJobs {
public DeploymentJobs withTriggering(JobType jobType,
Optional<Change> change,
- long runId,
Version version,
Optional<ApplicationRevision> revision,
String reason,
@@ -74,8 +75,7 @@ public class DeploymentJobs {
Map<JobType, JobStatus> status = new LinkedHashMap<>(this.status);
status.compute(jobType, (type, job) -> {
if (job == null) job = JobStatus.initial(jobType);
- return job.withTriggering(runId,
- version,
+ return job.withTriggering( version,
revision,
change.isPresent() && change.get() instanceof Change.VersionChange,
reason,
@@ -103,12 +103,12 @@ public class DeploymentJobs {
/** Returns whether this has some job status which is not a success */
public boolean hasFailures() {
- return status.values().stream().anyMatch(jobStatus -> ! jobStatus.isSuccess());
+ return JobList.from(status.values()).failing().anyMatch();
}
/** Returns whether any job is currently in progress */
public boolean isRunning(Instant timeoutLimit) {
- return status.values().stream().anyMatch(job -> job.isRunning(timeoutLimit));
+ return JobList.from(status.values()).running(timeoutLimit).anyMatch();
}
/** Returns whether the given job type is currently running and was started after timeoutLimit */
@@ -120,7 +120,7 @@ public class DeploymentJobs {
/** Returns whether change can be deployed to the given environment */
public boolean isDeployableTo(Environment environment, Optional<Change> change) {
- if (environment == null || !change.isPresent()) {
+ if (environment == null || ! change.isPresent()) {
return true;
}
if (environment == Environment.staging) {
@@ -131,31 +131,13 @@ public class DeploymentJobs {
return true; // other environments do not have any preconditions
}
- /** Returns whether the given change has been deployed completely */
- public boolean isDeployed(Change change) {
- return status.values().stream()
- .filter(status -> status.type().isProduction())
- .allMatch(status -> isSuccessful(change, status.type()));
- }
-
/** Returns whether job has completed successfully */
public boolean isSuccessful(Change change, JobType jobType) {
return Optional.ofNullable(jobStatus().get(jobType))
- .filter(JobStatus::isSuccess)
- .filter(status -> status.lastCompletedFor(change))
+ .flatMap(JobStatus::lastSuccess)
+ .filter(status -> status.lastCompletedWas(change))
.isPresent();
}
-
- /** Returns the oldest failingSince time of the jobs of this, or null if none are failing */
- public Instant failingSince() {
- Instant failingSince = null;
- for (JobStatus jobStatus : jobStatus().values()) {
- if (jobStatus.isSuccess()) continue;
- if (failingSince == null || failingSince.isAfter(jobStatus.firstFailing().get().at()))
- failingSince = jobStatus.firstFailing().get().at();
- }
- return failingSince;
- }
/**
* Returns the id of the Screwdriver project running these deployment jobs
@@ -166,39 +148,44 @@ public class DeploymentJobs {
public Optional<IssueId> issueId() { return issueId; }
+ private static Optional<Long> requireId(Optional<Long> id, String message) {
+ Objects.requireNonNull(id, message);
+ if ( ! id.isPresent()) {
+ return id;
+ }
+ if (id.get() <= 0) {
+ throw new IllegalArgumentException(message);
+ }
+ return id;
+ }
+
/** Job types that exist in the build system */
public enum JobType {
- component("component"),
- systemTest("system-test", zone(SystemName.cd, "test", "cd-us-central-1"), zone("test", "us-east-1")),
- stagingTest("staging-test", zone(SystemName.cd, "staging", "cd-us-central-1"), zone("staging", "us-east-3")),
- productionCorpUsEast1("production-corp-us-east-1", zone("prod", "corp-us-east-1")),
- productionUsEast3("production-us-east-3", zone("prod", "us-east-3")),
- productionUsWest1("production-us-west-1", zone("prod", "us-west-1")),
- productionUsCentral1("production-us-central-1", zone("prod", "us-central-1")),
- productionApNortheast1("production-ap-northeast-1", zone("prod", "ap-northeast-1")),
- productionApNortheast2("production-ap-northeast-2", zone("prod", "ap-northeast-2")),
- productionApSoutheast1("production-ap-southeast-1", zone("prod", "ap-southeast-1")),
- productionEuWest1("production-eu-west-1", zone("prod", "eu-west-1")),
- productionCdUsCentral1("production-cd-us-central-1", zone(SystemName.cd, "prod", "cd-us-central-1")),
- productionCdUsCentral2("production-cd-us-central-2", zone(SystemName.cd, "prod", "cd-us-central-2"));
-
- private final String id;
- private final Map<SystemName, Zone> zones;
-
- JobType(String id, Zone... zone) {
- this.id = id;
- Map<SystemName, Zone> zones = new HashMap<>();
- for (Zone z : zone) {
- if (zones.containsKey(z.system())) {
- throw new IllegalArgumentException("A job can only map to a single zone per system");
- }
- zones.put(z.system(), z);
- }
- this.zones = Collections.unmodifiableMap(zones);
+ component ("component" ),
+ systemTest ("system-test" , zone("test" , "us-east-1" ), zone(SystemName.cd, "test" , "cd-us-central-1")),
+ stagingTest ("staging-test" , zone("staging", "us-east-3" ), zone(SystemName.cd, "staging", "cd-us-central-1")),
+ productionCorpUsEast1 ("production-corp-us-east-1" , zone("prod" , "corp-us-east-1")),
+ productionUsEast3 ("production-us-east-3" , zone("prod" , "us-east-3" )),
+ productionUsWest1 ("production-us-west-1" , zone("prod" , "us-west-1" )),
+ productionUsCentral1 ("production-us-central-1" , zone("prod" , "us-central-1" )),
+ productionApNortheast1 ("production-ap-northeast-1" , zone("prod" , "ap-northeast-1")),
+ productionApNortheast2 ("production-ap-northeast-2" , zone("prod" , "ap-northeast-2")),
+ productionApSoutheast1 ("production-ap-southeast-1" , zone("prod" , "ap-southeast-1")),
+ productionEuWest1 ("production-eu-west-1" , zone("prod" , "eu-west-1" )),
+ productionCdUsCentral1 ("production-cd-us-central-1", zone(SystemName.cd, "prod", "cd-us-central-1")),
+ productionCdUsCentral2 ("production-cd-us-central-2", zone(SystemName.cd, "prod", "cd-us-central-2"));
+
+ private final String jobName;
+ private final ImmutableMap<SystemName, Zone> zones;
+
+ JobType(String jobName, Zone... zones) {
+ this.jobName = jobName;
+ this.zones = ImmutableMap.copyOf(Stream.of(zones).collect(Collectors.toMap(zone -> zone.system(),
+ zone -> zone)));
}
- public String id() { return id; }
+ public String jobName() { return jobName; }
/** Returns the zone for this job in the given system, or empty if this job does not have a zone */
public Optional<Zone> zone(SystemName system) {
@@ -223,42 +210,26 @@ public class DeploymentJobs {
return zone(system).map(Zone::region);
}
- public static JobType fromId(String id) {
- switch (id) {
- case "component" : return component;
- case "system-test" : return systemTest;
- case "staging-test" : return stagingTest;
- case "production-corp-us-east-1" : return productionCorpUsEast1;
- case "production-us-east-3" : return productionUsEast3;
- case "production-us-west-1" : return productionUsWest1;
- case "production-us-central-1" : return productionUsCentral1;
- case "production-ap-northeast-1" : return productionApNortheast1;
- case "production-ap-northeast-2" : return productionApNortheast2;
- case "production-ap-southeast-1" : return productionApSoutheast1;
- case "production-eu-west-1" : return productionEuWest1;
- case "production-cd-us-central-1" : return productionCdUsCentral1;
- case "production-cd-us-central-2" : return productionCdUsCentral2;
- default : throw new IllegalArgumentException("Unknown job id '" + id + "'");
- }
+ public static JobType fromJobName(String jobName) {
+ return Stream.of(values())
+ .filter(jobType -> jobType.jobName.equals(jobName))
+ .findAny().orElseThrow(() -> new IllegalArgumentException("Unknown job name '" + jobName + "'"));
}
- /** Returns the job type for the given zone, or null if none */
- public static JobType from(SystemName system, com.yahoo.config.provision.Zone zone) {
- for (JobType job : values()) {
- Optional<com.yahoo.config.provision.Zone> jobZone = job.zone(system);
- if (jobZone.isPresent() && jobZone.get().equals(zone))
- return job;
- }
- return null;
+ /** Returns the job type for the given zone */
+ public static Optional<JobType> from(SystemName system, Zone zone) {
+ return Stream.of(values())
+ .filter(job -> job.zone(system).filter(zone::equals).isPresent())
+ .findAny();
}
/** Returns the job job type for the given environment and region or null if none */
- public static JobType from(SystemName system, Environment environment, RegionName region) {
+ public static Optional<JobType> from(SystemName system, Environment environment, RegionName region) {
switch (environment) {
- case test: return systemTest;
- case staging: return stagingTest;
+ case test: return Optional.of(systemTest);
+ case staging: return Optional.of(stagingTest);
}
- return from(system, new com.yahoo.config.provision.Zone(environment, region));
+ return from(system, new Zone(environment, region));
}
private static Zone zone(SystemName system, String environment, String region) {
@@ -296,7 +267,7 @@ public class DeploymentJobs {
public JobType jobType() { return jobType; }
public long projectId() { return projectId; }
public long buildNumber() { return buildNumber; }
- public boolean success() { return !jobError.isPresent(); }
+ public boolean success() { return ! jobError.isPresent(); }
public Optional<JobError> jobError() { return jobError; }
}
@@ -304,23 +275,6 @@ public class DeploymentJobs {
public enum JobError {
unknown,
outOfCapacity;
-
- public static Optional<JobError> from(boolean success) {
- return Optional.of(success)
- .filter(b -> !b)
- .map(ignored -> unknown);
- }
- }
-
- private static Optional<Long> requireId(Optional<Long> id, String message) {
- Objects.requireNonNull(id, message);
- if (!id.isPresent()) {
- return id;
- }
- if (id.get() <= 0) {
- throw new IllegalArgumentException(message);
- }
- return id;
}
} \ No newline at end of file
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentMetrics.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentMetrics.java
index 504e2285a34..c0f7bd6c6a1 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentMetrics.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentMetrics.java
@@ -1,5 +1,5 @@
// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.application;// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.application;
/**
* @author smorgrav
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/JobList.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/JobList.java
new file mode 100644
index 00000000000..6223b07d27a
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/JobList.java
@@ -0,0 +1,189 @@
+package com.yahoo.vespa.hosted.controller.application;
+
+import com.google.common.collect.ImmutableList;
+import com.yahoo.component.Version;
+import com.yahoo.vespa.hosted.controller.Application;
+import com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobType;
+import com.yahoo.vespa.hosted.controller.application.JobStatus.JobRun;
+
+import java.time.Instant;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import static com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobError.outOfCapacity;
+
+/**
+ * A list of deployment jobs that can be filtered in various ways.
+ *
+ * @author jvenstad
+ */
+public class JobList {
+
+ private final ImmutableList<JobStatus> list;
+ private final boolean negate;
+
+ private JobList(Iterable<JobStatus> jobs, boolean negate) {
+ this.list = ImmutableList.copyOf(jobs);
+ this.negate = negate;
+ }
+
+ private JobList(Iterable<JobStatus> jobs) {
+ this(jobs, false);
+ }
+
+ // ----------------------------------- Factories
+
+ public static JobList from(Iterable<JobStatus> jobs) {
+ return new JobList(jobs);
+ }
+
+ public static JobList from(Application application) {
+ return from(application.deploymentJobs().jobStatus().values());
+ }
+
+ // ----------------------------------- Accessors
+
+ // TODO: Add sorting based on various stuff, such as deployment order, time of last completion, etc..
+
+ /** Returns the jobstatuses in this as an immutable list */
+ public List<JobStatus> asList() { return list; }
+
+ /** Returns the jobstatuses in this as an immutable list after mapping with the given function */
+ public <Type> List<Type> mapToList(Function<JobStatus, Type> mapper) {
+ return ImmutableList.copyOf(list.stream().map(mapper)::iterator);
+ }
+
+ public boolean isEmpty() { return list.isEmpty(); }
+
+ public boolean anyMatch() { return ! isEmpty(); }
+
+ public int size() { return list.size(); }
+
+ // ----------------------------------- Basic filters
+
+ /** Negates the next filter operation */
+ public JobList not() {
+ return new JobList(list, ! negate);
+ }
+
+ /** Returns the subset of jobs which are current upgrading */
+ public JobList upgrading() { // TODO: Centralise and standardise reasoning about upgrades and revisions.
+ return filter(job -> job.lastSuccess().isPresent()
+ && job.lastTriggered().isPresent()
+ && ! job.lastTriggered().get().at().isBefore(job.lastCompleted().get().at())
+ && job.lastSuccess().get().version().isBefore(job.lastTriggered().get().version()));
+ }
+
+ /** Returns the subset of jobs which are currently running, according to the given timeout */
+ public JobList running(Instant timeoutLimit) {
+ return filter(job -> job.isRunning(timeoutLimit));
+ }
+
+ /** Returns the subset of jobs which are currently failing */
+ public JobList failing() {
+ return filter(job -> ! job.isSuccess());
+ }
+
+ /** Returns the subset of jobs which must be failing due to an application change */
+ public JobList failingApplicationChange() {
+ return filter(job -> failingApplicationChange(job));
+ }
+
+ /** Returns the subset of jobs which are failing with the given job error */
+ public JobList failingBecause(DeploymentJobs.JobError error) {
+ return filter(job -> job.jobError().filter(error::equals).isPresent());
+ }
+
+ /** Returns the subset of jobs of the given type -- most useful when negated */
+ public JobList type(JobType type) {
+ return filter(job -> job.type() == type);
+ }
+
+ /** Returns the subset of jobs of which are production jobs */
+ public JobList production() {
+ return filter(job -> job.type().isProduction());
+ }
+
+ // ----------------------------------- JobRun filtering
+
+ /** Returns the list in a state where the next filter is for the lastTriggered run type */
+ public JobRunFilter lastTriggered() {
+ return new JobRunFilter(job -> job.lastTriggered());
+ }
+
+ /** Returns the list in a state where the next filter is for the lastCompleted run type */
+ public JobRunFilter lastCompleted() {
+ return new JobRunFilter(job -> job.lastCompleted());
+ }
+
+ /** Returns the list in a state where the next filter is for the lastSuccess run type */
+ public JobRunFilter lastSuccess() {
+ return new JobRunFilter(job -> job.lastSuccess());
+ }
+
+ /** Returns the list in a state where the next filter is for the firstFailing run type */
+ public JobRunFilter firstFailing() {
+ return new JobRunFilter(job -> job.firstFailing());
+ }
+
+
+ /** Allows sub-filters for runs of the given kind */
+ public class JobRunFilter {
+
+ private final Function<JobStatus, Optional<JobRun>> which;
+
+ private JobRunFilter(Function<JobStatus, Optional<JobRun>> which) {
+ this.which = which;
+ }
+
+ /** Returns the subset of jobs where the run of the given type exists */
+ public JobList present() {
+ return filter(run -> true);
+ }
+
+ /** Returns the subset of jobs where the run of the given type occurred before the given instant */
+ public JobList before(Instant threshold) {
+ return filter(run -> run.at().isBefore(threshold));
+ }
+
+ /** Returns the subset of jobs where the run of the given type occurred after the given instant */
+ public JobList after(Instant threshold) {
+ return filter(run -> run.at().isAfter(threshold));
+ }
+
+ /** Returns the subset of jobs where the run of the given type was on the given version */
+ public JobList on(Version version) {
+ return filter(run -> run.version().equals(version));
+ }
+
+ public JobList upgrade() {
+ return filter(run -> run.upgrade());
+ }
+
+ /** Transforms the JobRun condition to a JobStatus condition, by considering only the JobRun mapped by which, and executes */
+ private JobList filter(Predicate<JobRun> condition) {
+ return JobList.this.filter(job -> which.apply(job).filter(condition).isPresent());
+ }
+
+ }
+
+
+ // ----------------------------------- Internal helpers
+
+ private static boolean failingApplicationChange(JobStatus job) {
+ if ( job.isSuccess()) return false;
+ if ( ! job.lastSuccess().isPresent()) return true; // An application which never succeeded is surely bad.
+ if ( ! job.lastSuccess().get().revision().isPresent()) return true; // Indicates the component job, which is always an application change.
+ if ( ! job.firstFailing().get().version().equals(job.lastSuccess().get().version())) return false; // Version change may be to blame.
+ return ! job.firstFailing().get().revision().equals(job.lastSuccess().get().revision()); // Return whether there is an application change.
+ }
+
+ /** Returns a new JobList which is the result of filtering with the -- possibly negated -- condition */
+ private JobList filter(Predicate<JobStatus> condition) {
+ return from(list.stream().filter(negate ? condition.negate() : condition)::iterator);
+ }
+
+}
+
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/JobStatus.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/JobStatus.java
index 929bf186eea..ceb04d88026 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/JobStatus.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/JobStatus.java
@@ -4,7 +4,6 @@ package com.yahoo.vespa.hosted.controller.application;
import com.yahoo.component.Version;
import com.yahoo.vespa.hosted.controller.Controller;
-import java.time.Duration;
import java.time.Instant;
import java.util.Objects;
import java.util.Optional;
@@ -56,9 +55,9 @@ public class JobStatus {
return new JobStatus(type, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty());
}
- public JobStatus withTriggering(long runId, Version version, Optional<ApplicationRevision> revision,
+ public JobStatus withTriggering(Version version, Optional<ApplicationRevision> revision,
boolean upgrade, String reason, Instant triggerTime) {
- return new JobStatus(type, jobError, Optional.of(new JobRun(runId, version, revision, upgrade, reason, triggerTime)),
+ return new JobStatus(type, jobError, Optional.of(new JobRun(-1, version, revision, upgrade, reason, triggerTime)),
lastCompleted, firstFailing, lastSuccess);
}
@@ -75,7 +74,7 @@ public class JobStatus {
}
else if (! lastTriggered.isPresent()) {
throw new IllegalStateException("Got notified about completion of " + this +
- ", but that has not been triggered nor deployed");
+ ", but that has neither been triggered nor deployed");
}
else {
@@ -103,14 +102,16 @@ public class JobStatus {
public DeploymentJobs.JobType type() { return type; }
/** Returns true unless this job last completed with a failure */
- public boolean isSuccess() { return ! jobError.isPresent(); }
+ public boolean isSuccess() {
+ return lastCompleted().isPresent() && ! jobError.isPresent();
+ }
/** Returns true if last triggered is newer than last completed and was started after timeoutLimit */
public boolean isRunning(Instant timeoutLimit) {
if ( ! lastTriggered.isPresent()) return false;
if (lastTriggered.get().at().isBefore(timeoutLimit)) return false;
if ( ! lastCompleted.isPresent()) return true;
- return lastTriggered.get().at().isAfter(lastCompleted.get().at());
+ return ! lastTriggered.get().at().isBefore(lastCompleted.get().at());
}
/** The error of the last completion, or empty if the last run succeeded */
@@ -131,18 +132,6 @@ public class JobStatus {
/** Returns the run when this last succeeded, or empty if it has never succeeded */
public Optional<JobRun> lastSuccess() { return lastSuccess; }
- /** Returns whether the job last completed for the given change */
- public boolean lastCompletedFor(Change change) {
- if (change instanceof Change.ApplicationChange) {
- Change.ApplicationChange applicationChange = (Change.ApplicationChange) change;
- return lastCompleted().isPresent() && lastCompleted().get().revision().equals(applicationChange.revision());
- } else if (change instanceof Change.VersionChange) {
- Change.VersionChange versionChange = (Change.VersionChange) change;
- return lastCompleted().isPresent() && lastCompleted().get().version().equals(versionChange.version());
- }
- throw new IllegalArgumentException("Unexpected change: " + change.getClass());
- }
-
@Override
public String toString() {
return "job status of " + type + "[ " +
@@ -191,10 +180,12 @@ public class JobStatus {
this.reason = reason;
this.at = at;
}
-
+
+ // TODO: Replace with proper ID, and make the build number part optional, or something -- it's not there for lastTriggered!
/** Returns the id of this run of this job, or -1 if not known */
public long id() { return id; }
+ // TODO: Fix how this is set, and add an applicationChange() method as well, in the same vein.
/** Returns whether this job run was a Vespa upgrade */
public boolean upgrade() { return upgrade; }
@@ -210,6 +201,19 @@ public class JobStatus {
/** Returns the time if this triggering or completion */
public Instant at() { return at; }
+ // TODO: Consider a version and revision for each JobStatus, to compare against a Target (instead of Change, which is, really, a Target).
+ /** Returns whether the job last completed for the given change */
+ public boolean lastCompletedWas(Change change) {
+ if (change instanceof Change.ApplicationChange) {
+ Change.ApplicationChange applicationChange = (Change.ApplicationChange) change;
+ return revision().equals(applicationChange.revision());
+ } else if (change instanceof Change.VersionChange) {
+ Change.VersionChange versionChange = (Change.VersionChange) change;
+ return version().equals(versionChange.version());
+ }
+ throw new IllegalArgumentException("Unexpected change: " + change.getClass());
+ }
+
@Override
public int hashCode() {
return Objects.hash(version, revision, upgrade, at);
@@ -220,7 +224,7 @@ public class JobStatus {
if (this == o) return true;
if ( ! (o instanceof JobRun)) return false;
JobRun jobRun = (JobRun) o;
- return id == id &&
+ return id == jobRun.id &&
Objects.equals(version, jobRun.version) &&
Objects.equals(revision, jobRun.revision) &&
upgrade == jobRun.upgrade &&
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/AthenzPrincipalFilter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/AthenzPrincipalFilter.java
index de771ff2e17..51865be04fa 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/AthenzPrincipalFilter.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/AthenzPrincipalFilter.java
@@ -3,29 +3,27 @@ package com.yahoo.vespa.hosted.controller.athenz.filter;
import com.google.inject.Inject;
import com.yahoo.jdisc.Response;
-import com.yahoo.jdisc.handler.FastContentWriter;
-import com.yahoo.jdisc.handler.ResponseDispatch;
import com.yahoo.jdisc.handler.ResponseHandler;
import com.yahoo.jdisc.http.filter.DiscFilterRequest;
import com.yahoo.jdisc.http.filter.SecurityRequestFilter;
-import com.yahoo.jdisc.http.server.jetty.ErrorResponseContentCreator;
import com.yahoo.vespa.hosted.controller.athenz.AthenzPrincipal;
import com.yahoo.vespa.hosted.controller.athenz.InvalidTokenException;
import com.yahoo.vespa.hosted.controller.athenz.NToken;
import com.yahoo.vespa.hosted.controller.athenz.ZmsKeystore;
import com.yahoo.vespa.hosted.controller.athenz.config.AthenzConfig;
-import java.util.Optional;
import java.util.concurrent.Executor;
+import static com.yahoo.vespa.hosted.controller.athenz.filter.SecurityFilterUtils.sendErrorResponse;
+
/**
* Performs authentication by validating the principal token (NToken) header.
*
* @author bjorncs
*/
+// TODO bjorncs: Move this class into separate container-security bundle
public class AthenzPrincipalFilter implements SecurityRequestFilter {
- private final ErrorResponseContentCreator responseCreator = new ErrorResponseContentCreator();
private final NTokenValidator validator;
private final String principalTokenHeader;
@@ -47,7 +45,7 @@ public class AthenzPrincipalFilter implements SecurityRequestFilter {
public void filter(DiscFilterRequest request, ResponseHandler responseHandler) {
String rawToken = request.getHeader(principalTokenHeader);
if (rawToken == null || rawToken.isEmpty()) {
- sendUnauthorized(request, responseHandler, "NToken is missing");
+ sendErrorResponse(responseHandler, Response.Status.UNAUTHORIZED, "NToken is missing");
return;
}
try {
@@ -55,16 +53,7 @@ public class AthenzPrincipalFilter implements SecurityRequestFilter {
request.setUserPrincipal(principal);
request.setRemoteUser(principal.getName());
} catch (InvalidTokenException e) {
- sendUnauthorized(request, responseHandler, e.getMessage());
- }
- }
-
- private void sendUnauthorized(DiscFilterRequest request, ResponseHandler responseHandler, String message) {
- try (FastContentWriter writer = ResponseDispatch.newInstance(Response.Status.UNAUTHORIZED)
- .connectFastWriter(responseHandler)) {
- writer.write(
- responseCreator.createErrorContent(
- request.getRequestURI(), Response.Status.UNAUTHORIZED, Optional.of(message)));
+ sendErrorResponse(responseHandler,Response.Status.UNAUTHORIZED, e.getMessage());
}
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/SecurityFilterUtils.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/SecurityFilterUtils.java
new file mode 100644
index 00000000000..8e193d3848f
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/SecurityFilterUtils.java
@@ -0,0 +1,32 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.athenz.filter;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.handler.FastContentWriter;
+import com.yahoo.jdisc.handler.ResponseDispatch;
+import com.yahoo.jdisc.handler.ResponseHandler;
+
+/**
+ * @author bjorncs
+ */
+class SecurityFilterUtils {
+ private static final ObjectMapper mapper = new ObjectMapper();
+
+ private SecurityFilterUtils() {}
+
+ static void sendErrorResponse(ResponseHandler responseHandler, int statusCode, String message) {
+ Response response = new Response(statusCode);
+ response.headers().put("Content-Type", "application/json");
+ ObjectNode errorMessage = mapper.createObjectNode();
+ errorMessage.put("message", message);
+ try (FastContentWriter writer = ResponseDispatch.newInstance(response).connectFastWriter(responseHandler)) {
+ writer.write(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(errorMessage));
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/UserAuthWithAthenzPrincipalFilter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/UserAuthWithAthenzPrincipalFilter.java
new file mode 100644
index 00000000000..bfa543f160a
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/filter/UserAuthWithAthenzPrincipalFilter.java
@@ -0,0 +1,106 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.athenz.filter;
+
+import com.google.inject.Inject;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.jdisc.Response;
+import com.yahoo.jdisc.handler.ResponseHandler;
+import com.yahoo.jdisc.http.filter.DiscFilterRequest;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.hosted.controller.api.identifiers.UserId;
+import com.yahoo.vespa.hosted.controller.athenz.AthenzPrincipal;
+import com.yahoo.vespa.hosted.controller.athenz.AthenzUtils;
+import com.yahoo.vespa.hosted.controller.athenz.ZmsKeystore;
+import com.yahoo.vespa.hosted.controller.athenz.config.AthenzConfig;
+import com.yahoo.vespa.hosted.controller.restapi.application.Authorizer;
+
+import java.security.Principal;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+import java.util.stream.Stream;
+
+import static com.yahoo.vespa.hosted.controller.athenz.filter.SecurityFilterUtils.sendErrorResponse;
+
+/**
+ * A variant of the {@link AthenzPrincipalFilter} to be used in combination with a cookie-based
+ * security filter for user authentication
+ * Assumes that the user authentication filter configured in the same filter chain and is configured to run before this filter.
+ *
+ * @author bjorncs
+ */
+// TODO Remove this filter once migrated to Okta
+public class UserAuthWithAthenzPrincipalFilter extends AthenzPrincipalFilter {
+
+ private static final Logger log = Logger.getLogger(UserAuthWithAthenzPrincipalFilter.class.getName());
+
+ private final String userAuthenticationPassThruAttribute;
+
+ @Inject
+ public UserAuthWithAthenzPrincipalFilter(ZmsKeystore zmsKeystore, Executor executor, AthenzConfig config) {
+ super(zmsKeystore, executor, config);
+ this.userAuthenticationPassThruAttribute = config.userAuthenticationPassThruAttribute();
+ }
+
+ @Override
+ public void filter(DiscFilterRequest request, ResponseHandler responseHandler) {
+ if (request.getMethod().equals("OPTIONS")) return; // Skip authentication on OPTIONS - required for Javascript CORS
+
+ try {
+ switch (getUserAuthenticationResult(request)) {
+ case USER_COOKIE_MISSING:
+ case USER_COOKIE_ALTERNATIVE_MISSING:
+ super.filter(request, responseHandler); // Cookie-based authentication failed, delegate to Athenz
+ break;
+ case USER_COOKIE_OK:
+ rewriteUserPrincipalToAthenz(request);
+ return; // Authenticated using user cookie
+ case USER_COOKIE_INVALID:
+ sendErrorResponse(responseHandler,
+ Response.Status.UNAUTHORIZED,
+ "Your user cookie is invalid (either expired, tampered or invalid ip)");
+ break;
+ }
+ } catch (Exception e) {
+ log.log(LogLevel.WARNING, "Authentication failed: " + e.getMessage(), e);
+ sendErrorResponse(responseHandler, Response.Status.INTERNAL_SERVER_ERROR, e.getMessage());
+ }
+ }
+
+ private UserAuthenticationResult getUserAuthenticationResult(DiscFilterRequest request) {
+ if (!request.containsAttribute(userAuthenticationPassThruAttribute)) {
+ throw new IllegalStateException("User authentication filter passthru attribute missing");
+ }
+ Integer statusCode = (Integer) request.getAttribute(userAuthenticationPassThruAttribute);
+ return Stream.of(UserAuthenticationResult.values())
+ .filter(uar -> uar.statusCode == statusCode)
+ .findAny()
+ .orElseThrow(() -> new IllegalStateException("Invalid status code: " + statusCode));
+ }
+
+ /**
+ * NOTE: The Bouncer user roles ({@link DiscFilterRequest#roles} are still intact as they are required
+ * for {@link Authorizer#isMemberOfVespaBouncerGroup(HttpRequest)}.
+ */
+ private static void rewriteUserPrincipalToAthenz(DiscFilterRequest request) {
+ Principal userPrincipal = request.getUserPrincipal();
+ log.log(LogLevel.DEBUG, () -> "Original user principal: " + userPrincipal.toString());
+ UserId userId = new UserId(userPrincipal.getName());
+ AthenzPrincipal athenzPrincipal = AthenzUtils.createPrincipal(userId);
+ request.setUserPrincipal(athenzPrincipal);
+ request.setRemoteUser(athenzPrincipal.toYRN());
+ }
+
+ private enum UserAuthenticationResult {
+ USER_COOKIE_MISSING(0),
+ USER_COOKIE_OK(1),
+ USER_COOKIE_INVALID(-1),
+ USER_COOKIE_ALTERNATIVE_MISSING(-2);
+
+ final int statusCode;
+
+ UserAuthenticationResult(int statusCode) {
+ this.statusCode = statusCode;
+ }
+
+ }
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentOrder.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentOrder.java
index bb84c9e17d4..7c06ef27ce9 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentOrder.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentOrder.java
@@ -2,7 +2,6 @@
package com.yahoo.vespa.hosted.controller.deployment;
import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.Zone;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.Controller;
@@ -16,19 +15,16 @@ import com.yahoo.vespa.hosted.controller.application.JobStatus;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
+import java.util.Collection;
import java.util.Collections;
-import java.util.Comparator;
-import java.util.LinkedHashMap;
import java.util.List;
-import java.util.Map;
import java.util.Objects;
import java.util.Optional;
-import java.util.function.Function;
import java.util.logging.Logger;
-import java.util.stream.Collector;
-import java.util.stream.Collectors;
+import static java.util.Comparator.comparingInt;
import static java.util.stream.Collectors.collectingAndThen;
+import static java.util.stream.Collectors.toList;
/**
* This class determines the order of deployments according to an application's deployment spec.
@@ -50,6 +46,7 @@ public class DeploymentOrder {
/** Returns a list of jobs to trigger after the given job */
// TODO: This does too much - should just tell us the order, as advertised
+ // TODO: You're next!
public List<JobType> nextAfter(JobType job, LockedApplication application) {
if ( ! application.deploying().isPresent()) { // Change was cancelled
return Collections.emptyList();
@@ -64,7 +61,7 @@ public class DeploymentOrder {
// At this point we have deployed to system test, so deployment spec is available
List<DeploymentSpec.Step> deploymentSteps = deploymentSteps(application);
Optional<DeploymentSpec.Step> currentStep = fromJob(job, application);
- if (!currentStep.isPresent()) {
+ if ( ! currentStep.isPresent()) {
return Collections.emptyList();
}
@@ -75,13 +72,13 @@ public class DeploymentOrder {
}
// Postpone if step hasn't completed all its jobs for this change
- if (!completedSuccessfully(currentStep.get(), application.deploying().get(), application)) {
+ if ( ! completedSuccessfully(currentStep.get(), application.deploying().get(), application)) {
return Collections.emptyList();
}
// Postpone next job if delay has not passed yet
Duration delay = delayAfter(currentStep.get(), application);
- if (postponeDeployment(delay, job, application)) {
+ if (shouldPostponeDeployment(delay, job, application)) {
log.info(String.format("Delaying next job after %s of %s by %s", job, application, delay));
return Collections.emptyList();
}
@@ -89,59 +86,40 @@ public class DeploymentOrder {
DeploymentSpec.Step nextStep = deploymentSteps.get(currentIndex + 1);
return nextStep.zones().stream()
.map(this::toJob)
- .collect(collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
- }
-
- /** Returns whether the given job is first in a deployment */
- public boolean isFirst(JobType job) {
- return job == JobType.component;
- }
-
- /** Returns whether the given job is last in a deployment */
- public boolean isLast(JobType job, Application application) {
- List<DeploymentSpec.Step> deploymentSteps = deploymentSteps(application);
- if (deploymentSteps.isEmpty()) { // Deployment spec not yet available
- return false;
- }
- DeploymentSpec.Step lastStep = deploymentSteps.get(deploymentSteps.size() - 1);
- Optional<DeploymentSpec.Step> step = fromJob(job, application);
- // Step may not exist for all jobs, e.g. component
- return step.map(s -> s.equals(lastStep)).orElse(false);
+ .collect(collectingAndThen(toList(), Collections::unmodifiableList));
}
/** Returns jobs for given deployment spec, in the order they are declared */
public List<JobType> jobsFrom(DeploymentSpec deploymentSpec) {
return deploymentSpec.steps().stream()
.flatMap(step -> jobsFrom(step).stream())
- .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
+ .collect(collectingAndThen(toList(), Collections::unmodifiableList));
}
/** Returns job status sorted according to deployment spec */
- public Map<JobType, JobStatus> sortBy(DeploymentSpec deploymentSpec, Map<JobType, JobStatus> jobStatus) {
- List<DeploymentJobs.JobType> jobs = jobsFrom(deploymentSpec);
- return jobStatus.entrySet().stream()
- .sorted(Comparator.comparingInt(kv -> jobs.indexOf(kv.getKey())))
- .collect(Collectors.collectingAndThen(toLinkedMap(Map.Entry::getKey, Map.Entry::getValue),
- Collections::unmodifiableMap));
+ public List<JobStatus> sortBy(DeploymentSpec deploymentSpec, Collection<JobStatus> jobStatus) {
+ List<DeploymentJobs.JobType> sortedJobs = jobsFrom(deploymentSpec);
+ return jobStatus.stream()
+ .sorted(comparingInt(job -> sortedJobs.indexOf(job.type())))
+ .collect(collectingAndThen(toList(), Collections::unmodifiableList));
}
/** Returns deployments sorted according to declared zones */
- public Map<Zone, Deployment> sortBy(List<DeploymentSpec.DeclaredZone> zones, Map<Zone, Deployment> deployments) {
+ public List<Deployment> sortBy(List<DeploymentSpec.DeclaredZone> zones, Collection<Deployment> deployments) {
List<Zone> productionZones = zones.stream()
- .filter(z -> z.environment() == Environment.prod && z.region().isPresent())
+ .filter(z -> z.region().isPresent())
.map(z -> new Zone(z.environment(), z.region().get()))
- .collect(Collectors.toList());
- return deployments.entrySet().stream()
- .sorted(Comparator.comparingInt(kv -> productionZones.indexOf(kv.getKey())))
- .collect(Collectors.collectingAndThen(toLinkedMap(Map.Entry::getKey, Map.Entry::getValue),
- Collections::unmodifiableMap));
+ .collect(toList());
+ return deployments.stream()
+ .sorted(comparingInt(deployment -> productionZones.indexOf(deployment.zone())))
+ .collect(collectingAndThen(toList(), Collections::unmodifiableList));
}
/** Returns jobs for the given step */
private List<JobType> jobsFrom(DeploymentSpec.Step step) {
return step.zones().stream()
.map(this::toJob)
- .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
+ .collect(collectingAndThen(toList(), Collections::unmodifiableList));
}
/** Returns whether all jobs have completed successfully for given step */
@@ -162,11 +140,12 @@ public class DeploymentOrder {
/** Resolve job from deployment step */
private JobType toJob(DeploymentSpec.DeclaredZone zone) {
- return JobType.from(controller.system(), zone.environment(), zone.region().orElse(null));
+ return JobType.from(controller.system(), zone.environment(), zone.region().orElse(null))
+ .orElseThrow(() -> new IllegalArgumentException("Invalid zone " + zone));
}
/** Returns whether deployment should be postponed according to delay */
- private boolean postponeDeployment(Duration delay, JobType job, Application application) {
+ private boolean shouldPostponeDeployment(Duration delay, JobType job, Application application) {
Optional<Instant> lastSuccess = Optional.ofNullable(application.deploymentJobs().jobStatus().get(job))
.flatMap(JobStatus::lastSuccess)
.map(JobStatus.JobRun::at);
@@ -176,9 +155,8 @@ public class DeploymentOrder {
/** Find all steps that deploy to one or more zones */
private static List<DeploymentSpec.Step> deploymentSteps(Application application) {
return application.deploymentSpec().steps().stream()
- .filter(step -> step instanceof DeploymentSpec.DeclaredZone ||
- step instanceof DeploymentSpec.ParallelZones)
- .collect(Collectors.toList());
+ .filter(step -> ! step.zones().isEmpty())
+ .collect(toList());
}
/** Determines the delay that should pass after the given step */
@@ -191,7 +169,7 @@ public class DeploymentOrder {
List<DeploymentSpec.Step> remainingSteps = application.deploymentSpec().steps()
.subList(stepIndex + 1, application.deploymentSpec().steps().size());
for (DeploymentSpec.Step s : remainingSteps) {
- if (!(s instanceof DeploymentSpec.Delay)) {
+ if (! (s instanceof DeploymentSpec.Delay)) {
break;
}
totalDelay = totalDelay.plus(((DeploymentSpec.Delay) s).duration());
@@ -199,13 +177,4 @@ public class DeploymentOrder {
return totalDelay;
}
- private static <T, K, U> Collector<T, ?, Map<K,U>> toLinkedMap(Function<? super T, ? extends K> keyMapper,
- Function<? super T, ? extends U> valueMapper) {
- return Collectors.toMap(keyMapper, valueMapper,
- (u, v) -> {
- throw new IllegalStateException(String.format("Duplicate key %s", u));
- },
- LinkedHashMap::new);
- }
-
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java
index e5c7b7f1c2f..1eee727214b 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java
@@ -2,9 +2,7 @@
package com.yahoo.vespa.hosted.controller.deployment;
import com.yahoo.component.Version;
-import com.yahoo.config.application.api.DeploymentSpec;
import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.SystemName;
import com.yahoo.config.provision.Zone;
import com.yahoo.vespa.curator.Lock;
@@ -14,11 +12,12 @@ import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.LockedApplication;
import com.yahoo.vespa.hosted.controller.application.ApplicationList;
import com.yahoo.vespa.hosted.controller.application.Change;
+import com.yahoo.vespa.hosted.controller.application.Change.VersionChange;
import com.yahoo.vespa.hosted.controller.application.Deployment;
-import com.yahoo.vespa.hosted.controller.application.DeploymentJobs;
import com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobError;
import com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobReport;
import com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobType;
+import com.yahoo.vespa.hosted.controller.application.JobList;
import com.yahoo.vespa.hosted.controller.application.JobStatus;
import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
@@ -26,10 +25,10 @@ import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
-import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
+import java.util.function.Consumer;
import java.util.logging.Logger;
/**
@@ -66,7 +65,11 @@ public class DeploymentTrigger {
/** Returns the time in the past before which jobs are at this moment considered unresponsive */
public Instant jobTimeoutLimit() { return clock.instant().minus(jobTimeout); }
-
+
+ public BuildSystem buildSystem() { return buildSystem; }
+
+ public DeploymentOrder deploymentOrder() { return order; }
+
//--- Start of methods which triggers deployment jobs -------------------------
/**
@@ -76,16 +79,15 @@ public class DeploymentTrigger {
* @param report information about the job that just completed
*/
public void triggerFromCompletion(JobReport report) {
- try (Lock lock = applications().lock(report.applicationId())) {
- LockedApplication application = applications().require(report.applicationId(), lock);
+ applications().lockedOrThrow(report.applicationId(), application -> {
application = application.withJobCompletion(report, clock.instant(), controller);
-
+
// Handle successful starting and ending
if (report.success()) {
- if (order.isFirst(report.jobType())) { // the first job tells us that a change occurred
+ if (report.jobType() == JobType.component) {
if (acceptNewRevisionNow(application)) {
// Set this as the change we are doing, unless we are already pushing a platform change
- if ( ! ( application.deploying().isPresent() &&
+ if ( ! ( application.deploying().isPresent() &&
(application.deploying().get() instanceof Change.VersionChange)))
application = application.withDeploying(Optional.of(Change.ApplicationChange.unknown()));
}
@@ -93,8 +95,8 @@ public class DeploymentTrigger {
applications().store(application.withOutstandingChange(true));
return;
}
- }
- else if (order.isLast(report.jobType(), application) && application.deployingCompleted()) {
+ }
+ else if (deploymentComplete(application)) {
// change completed
application = application.withDeploying(Optional.empty());
}
@@ -103,19 +105,24 @@ public class DeploymentTrigger {
// Trigger next
if (report.success())
application = trigger(order.nextAfter(report.jobType(), application), application,
- String.format("%s completed successfully in build %d",
- report.jobType(), report.buildNumber()));
- else if (isCapacityConstrained(report.jobType()) && shouldRetryOnOutOfCapacity(application, report.jobType()))
+ report.jobType().jobName() + " completed");
+ else if (retryBecauseOutOfCapacity(application, report.jobType()))
application = trigger(report.jobType(), application, true,
- String.format("Retrying due to out of capacity in build %d",
- report.buildNumber()));
- else if (shouldRetryNow(application))
+ "Retrying on out of capacity");
+ else if (retryBecauseNewFailure(application, report.jobType()))
application = trigger(report.jobType(), application, false,
- String.format("Retrying as build %d just started failing",
- report.buildNumber()));
+ "Immediate retry on failure");
applications().store(application);
- }
+ });
+ }
+
+ /** Returns whether all production zones listed in deployment spec last were successful on the currently deploying change. */
+ private boolean deploymentComplete(LockedApplication application) {
+ if ( ! application.deploying().isPresent()) return true;
+ return order.jobsFrom(application.deploymentSpec()).stream()
+ .filter(JobType::isProduction)
+ .allMatch(jobType -> application.deploymentJobs().isSuccessful(application.deploying().get(), jobType));
}
/**
@@ -124,13 +131,8 @@ public class DeploymentTrigger {
public void triggerReadyJobs() {
ApplicationList applications = ApplicationList.from(applications().asList());
applications = applications.notPullRequest();
- for (Application application : applications.asList()) {
- try (Lock lock = applications().lock(application.id())) {
- Optional<LockedApplication> lockedApplication = controller.applications().get(application.id(), lock);
- if ( ! lockedApplication.isPresent()) continue; // application removed
- triggerReadyJobs(lockedApplication.get());
- }
- }
+ for (Application application : applications.asList())
+ applications().lockedIfPresent(application.id(), this::triggerReadyJobs);
}
/** Find the next step to trigger if any, and triggers it */
@@ -139,14 +141,24 @@ public class DeploymentTrigger {
List<JobType> jobs = order.jobsFrom(application.deploymentSpec());
// Should the first step be triggered?
- if ( ! jobs.isEmpty() && jobs.get(0).equals(JobType.systemTest) &&
- application.deploying().get() instanceof Change.VersionChange) {
- Version target = ((Change.VersionChange)application.deploying().get()).version();
- JobStatus jobStatus = application.deploymentJobs().jobStatus().get(JobType.systemTest);
- if (jobStatus == null || ! jobStatus.lastTriggered().isPresent()
- || ! jobStatus.lastTriggered().get().version().equals(target)) {
- application = trigger(JobType.systemTest, application, false, "Upgrade to " + target);
- controller.applications().store(application);
+ if ( ! jobs.isEmpty() && jobs.get(0).equals(JobType.systemTest) ) {
+ JobStatus systemTestStatus = application.deploymentJobs().jobStatus().get(JobType.systemTest);
+ if (application.deploying().get() instanceof Change.VersionChange) {
+ Version target = ((Change.VersionChange) application.deploying().get()).version();
+ if (systemTestStatus == null
+ || ! systemTestStatus.lastTriggered().isPresent()
+ || ! systemTestStatus.isSuccess()
+ || ! systemTestStatus.lastTriggered().get().version().equals(target)) {
+ application = trigger(JobType.systemTest, application, false, "Upgrade to " + target);
+ controller.applications().store(application);
+ }
+ }
+ else {
+ JobStatus componentStatus = application.deploymentJobs().jobStatus().get(JobType.component);
+ if (changesAvailable(application, componentStatus, systemTestStatus)) {
+ application = trigger(JobType.systemTest, application, false, "Available change in component");
+ controller.applications().store(application);
+ }
}
}
@@ -164,7 +176,7 @@ public class DeploymentTrigger {
nextToTrigger.add(nextJobType);
}
// Trigger them in parallel
- application = trigger(nextToTrigger, application, "Triggering previously blocked jobs");
+ application = trigger(nextToTrigger, application, "Available change in " + jobType.jobName());
controller.applications().store(application);
}
}
@@ -177,14 +189,14 @@ public class DeploymentTrigger {
if ( ! application.deploying().isPresent()) return false;
Change change = application.deploying().get();
- if ( ! previous.lastSuccess().isPresent() &&
- ! productionJobHasSucceededFor(previous, change)) return false;
+ if ( ! previous.lastSuccess().isPresent()) return false;
if (change instanceof Change.VersionChange) {
Version targetVersion = ((Change.VersionChange)change).version();
if ( ! (targetVersion.equals(previous.lastSuccess().get().version())) )
return false; // version is outdated
- if (isOnNewerVersionInProductionThan(targetVersion, application, next.type()))
+ // The below is checked again in allowedTriggering, right before actual triggering.
+ if (next != null && isOnNewerVersionInProductionThan(targetVersion, application, next.type()))
return false; // Don't downgrade
}
@@ -193,7 +205,7 @@ public class DeploymentTrigger {
JobStatus.JobRun previousSuccess = previous.lastSuccess().get();
JobStatus.JobRun nextSuccess = next.lastSuccess().get();
- if (previousSuccess.revision().isPresent() && ! previousSuccess.revision().get().equals(nextSuccess.revision().get()))
+ if (previousSuccess.revision().isPresent() && ! previousSuccess.revision().equals(nextSuccess.revision()))
return true;
if ( ! previousSuccess.version().equals(nextSuccess.version()))
return true;
@@ -201,79 +213,23 @@ public class DeploymentTrigger {
}
/**
- * Called periodically to cause triggering of jobs in the background
- */
- public void triggerFailing(ApplicationId applicationId) {
- try (Lock lock = applications().lock(applicationId)) {
- LockedApplication application = applications().require(applicationId, lock);
- if ( ! application.deploying().isPresent()) return; // No ongoing change, no need to retry
-
- // Retry first failing job
- for (JobType jobType : order.jobsFrom(application.deploymentSpec())) {
- JobStatus jobStatus = application.deploymentJobs().jobStatus().get(jobType);
- if (isFailing(application.deploying().get(), jobStatus)) {
- if (shouldRetryNow(jobStatus)) {
- application = trigger(jobType, application, false, "Retrying failing job");
- applications().store(application);
- }
- break;
- }
- }
-
- // Retry dead job
- Optional<JobStatus> firstDeadJob = firstDeadJob(application.deploymentJobs());
- if (firstDeadJob.isPresent()) {
- application = trigger(firstDeadJob.get().type(), application, false, "Retrying dead job");
- applications().store(application);
- }
- }
- }
-
- /** Triggers jobs that have been delayed according to deployment spec */
- public void triggerDelayed() {
- for (Application application : applications().asList()) {
- if ( ! application.deploying().isPresent() ) continue;
- if (application.deploymentJobs().hasFailures()) continue;
- if (application.deploymentJobs().isRunning(controller.applications().deploymentTrigger().jobTimeoutLimit())) continue;
- if (application.deploymentSpec().steps().stream().noneMatch(step -> step instanceof DeploymentSpec.Delay)) {
- continue; // Application does not have any delayed deployments
- }
-
- Optional<JobStatus> lastSuccessfulJob = application.deploymentJobs().jobStatus().values()
- .stream()
- .filter(j -> j.lastSuccess().isPresent())
- .sorted(Comparator.<JobStatus, Instant>comparing(j -> j.lastSuccess().get().at()).reversed())
- .findFirst();
- if ( ! lastSuccessfulJob.isPresent() ) continue;
-
- // Trigger next
- try (Lock lock = applications().lock(application.id())) {
- LockedApplication lockedApplication = applications().require(application.id(), lock);
- lockedApplication = trigger(order.nextAfter(lastSuccessfulJob.get().type(), lockedApplication),
- lockedApplication, "Resuming delayed deployment");
- applications().store(lockedApplication);
- }
- }
- }
-
- /**
* Triggers a change of this application
*
* @param applicationId the application to trigger
* @throws IllegalArgumentException if this application already have an ongoing change
*/
public void triggerChange(ApplicationId applicationId, Change change) {
- try (Lock lock = applications().lock(applicationId)) {
- LockedApplication application = applications().require(applicationId, lock);
+ applications().lockedOrThrow(applicationId, application -> {
if (application.deploying().isPresent() && ! application.deploymentJobs().hasFailures())
- throw new IllegalArgumentException("Could not start " + change + " on " + application + ": " +
+ throw new IllegalArgumentException("Could not start " + change + " on " + application + ": " +
application.deploying().get() + " is already in progress");
application = application.withDeploying(Optional.of(change));
if (change instanceof Change.ApplicationChange)
application = application.withOutstandingChange(false);
- application = trigger(JobType.systemTest, application, false, "Deploying change");
+ application = trigger(JobType.systemTest, application, false,
+ (change instanceof Change.VersionChange ? "Upgrading to " + ((Change.VersionChange)change).version() : "Deploying " + change));
applications().store(application);
- }
+ });
}
/**
@@ -282,81 +238,34 @@ public class DeploymentTrigger {
* @param applicationId the application to trigger
*/
public void cancelChange(ApplicationId applicationId) {
- try (Lock lock = applications().lock(applicationId)) {
- LockedApplication application = applications().require(applicationId, lock);
+ applications().lockedOrThrow(applicationId, application -> {
buildSystem.removeJobs(application.id());
- application = application.withDeploying(Optional.empty());
- applications().store(application);
- }
+ applications().store(application.withDeploying(Optional.empty()));
+ });
}
//--- End of methods which triggers deployment jobs ----------------------------
private ApplicationController applications() { return controller.applications(); }
- /** Returns whether a job is failing for the current change in the given application */
- private boolean isFailing(Change change, JobStatus status) {
- return status != null &&
- !status.isSuccess() &&
- status.lastCompletedFor(change);
- }
-
- private boolean isCapacityConstrained(JobType jobType) {
- return jobType == JobType.stagingTest || jobType == JobType.systemTest;
- }
-
- /** Returns the first job that has been running for more than the given timeout */
- private Optional<JobStatus> firstDeadJob(DeploymentJobs jobs) {
- Optional<JobStatus> oldestRunningJob = jobs.jobStatus().values().stream()
- .filter(job -> job.isRunning(Instant.ofEpochMilli(0)))
- .sorted(Comparator.comparing(status -> status.lastTriggered().get().at()))
- .findFirst();
- return oldestRunningJob.filter(job -> job.lastTriggered().get().at().isBefore(jobTimeoutLimit()));
- }
-
- /** Decide whether the job should be triggered by the periodic trigger */
- private boolean shouldRetryNow(JobStatus job) {
- if (job.isSuccess()) return false;
- if (job.isRunning(jobTimeoutLimit())) return false;
-
- // Retry after 10% of the time since it started failing
- Duration aTenthOfFailTime = Duration.ofMillis( (clock.millis() - job.firstFailing().get().at().toEpochMilli()) / 10);
- if (job.lastCompleted().get().at().isBefore(clock.instant().minus(aTenthOfFailTime))) return true;
-
- // ... or retry anyway if we haven't tried in 4 hours
- if (job.lastCompleted().get().at().isBefore(clock.instant().minus(Duration.ofHours(4)))) return true;
-
- return false;
- }
-
- /** Retry immediately only if this just started failing. Otherwise retry periodically */
- private boolean shouldRetryNow(Application application) {
- return application.deploymentJobs().failingSince().isAfter(clock.instant().minus(Duration.ofSeconds(10)));
+ /** Retry immediately only if this job just started failing. Otherwise retry periodically */
+ private boolean retryBecauseNewFailure(Application application, JobType jobType) {
+ JobStatus jobStatus = application.deploymentJobs().jobStatus().get(jobType);
+ return (jobStatus != null && jobStatus.firstFailing().get().at().isAfter(clock.instant().minus(Duration.ofSeconds(10))));
}
/** Decide whether to retry due to capacity restrictions */
- private boolean shouldRetryOnOutOfCapacity(Application application, JobType jobType) {
- Optional<JobError> outOfCapacityError = Optional.ofNullable(application.deploymentJobs().jobStatus().get(jobType))
- .flatMap(JobStatus::jobError)
- .filter(e -> e.equals(JobError.outOfCapacity));
-
- if ( ! outOfCapacityError.isPresent()) return false;
-
+ private boolean retryBecauseOutOfCapacity(Application application, JobType jobType) {
+ JobStatus jobStatus = application.deploymentJobs().jobStatus().get(jobType);
+ if (jobStatus == null || ! jobStatus.jobError().equals(Optional.of(JobError.outOfCapacity))) return false;
// Retry the job if it failed recently
- return application.deploymentJobs().jobStatus().get(jobType).firstFailing().get().at()
- .isAfter(clock.instant().minus(Duration.ofMinutes(15)));
+ return jobStatus.firstFailing().get().at().isAfter(clock.instant().minus(Duration.ofMinutes(15)));
}
/** Returns whether the given job type should be triggered according to deployment spec */
- private boolean deploysTo(Application application, JobType jobType) {
- Optional<Zone> zone = jobType.zone(controller.system());
- if (zone.isPresent() && jobType.isProduction()) {
- // Skip triggering of jobs for zones where the application should not be deployed
- if ( ! application.deploymentSpec().includes(jobType.environment(), Optional.of(zone.get().region()))) {
- return false;
- }
- }
- return true;
+ private boolean hasJob(JobType jobType, Application application) {
+ if ( ! jobType.isProduction()) return true; // Deployment spec only determines this for production jobs.
+ return application.deploymentSpec().includes(jobType.environment(), jobType.region(controller.system()));
}
/**
@@ -364,19 +273,19 @@ public class DeploymentTrigger {
*
* @param jobType the type of the job to trigger, or null to trigger nothing
* @param application the application to trigger the job for
- * @param first whether to trigger the job before other jobs
- * @param cause describes why the job is triggered
+ * @param first whether to put the job at the front of the build system queue (or the back)
+ * @param reason describes why the job is triggered
* @return the application in the triggered state, which *must* be stored by the caller
*/
- private LockedApplication trigger(JobType jobType, LockedApplication application, boolean first, String cause) {
- if (isRunningProductionJob(application)) return application;
- return triggerAllowParallel(jobType, application, first, false, cause);
+ private LockedApplication trigger(JobType jobType, LockedApplication application, boolean first, String reason) {
+ if (jobType.isProduction() && isRunningProductionJob(application)) return application;
+ return triggerAllowParallel(jobType, application, first, false, reason);
}
- private LockedApplication trigger(List<JobType> jobs, LockedApplication application, String cause) {
- if (isRunningProductionJob(application)) return application;
+ private LockedApplication trigger(List<JobType> jobs, LockedApplication application, String reason) {
+ if (jobs.stream().anyMatch(JobType::isProduction) && isRunningProductionJob(application)) return application;
for (JobType job : jobs)
- application = triggerAllowParallel(job, application, false, false, cause);
+ application = triggerAllowParallel(job, application, false, false, reason);
return application;
}
@@ -406,8 +315,12 @@ public class DeploymentTrigger {
application.deploying().map(d -> "deploying " + d).orElse("restarted deployment"),
reason));
buildSystem.addJob(application.id(), jobType, first);
- return application.withJobTriggering(-1, jobType, application.deploying(), reason, clock.instant(),
- controller);
+ return application.withJobTriggering(jobType,
+ application.deploying(),
+ clock.instant(),
+ application.deployVersionFor(jobType, controller),
+ application.deployRevisionFor(jobType, controller),
+ reason);
}
/** Returns true if the given proposed job triggering should be effected */
@@ -416,37 +329,29 @@ public class DeploymentTrigger {
// by instead basing the decision on what is currently deployed in the zone. However,
// this leads to some additional corner cases, and the possibility of blocking an application
// fix to a version upgrade, so not doing it now
- if (jobType.isProduction() && application.deployingBlocked(clock.instant())) return false;
+
+ if (jobType.isProduction() && application.deploying().isPresent() &&
+ application.deploying().get().blockedBy(application.deploymentSpec(), clock.instant())) return false;
+
+ if (application.deploying().isPresent() && application.deploying().get() instanceof VersionChange &&
+ isOnNewerVersionInProductionThan(((VersionChange) application.deploying().get()).version(), application, jobType)) return false;
+
if (application.deploymentJobs().isRunning(jobType, jobTimeoutLimit())) return false;
- if ( ! deploysTo(application, jobType)) return false;
+ if ( ! hasJob(jobType, application)) return false;
// Ignore applications that are not associated with a project
if ( ! application.deploymentJobs().projectId().isPresent()) return false;
- if (application.deploying().isPresent() && application.deploying().get() instanceof Change.VersionChange) {
- Version targetVersion = ((Change.VersionChange)application.deploying().get()).version();
- if (isOnNewerVersionInProductionThan(targetVersion, application, jobType)) return false; // Don't downgrade
- }
-
+
return true;
}
private boolean isRunningProductionJob(Application application) {
- return application.deploymentJobs().jobStatus().entrySet().stream()
- .anyMatch(entry -> entry.getKey().isProduction() && entry.getValue().isRunning(jobTimeoutLimit()));
+ return JobList.from(application)
+ .production()
+ .running(jobTimeoutLimit())
+ .anyMatch();
}
/**
- * When upgrading it is ok to trigger the next job even if the previous failed if the previous has earlier succeeded
- * on the version we are currently upgrading to
- */
- private boolean productionJobHasSucceededFor(JobStatus jobStatus, Change change) {
- if ( ! (change instanceof Change.VersionChange) ) return false;
- if ( ! isProduction(jobStatus.type())) return false;
- Optional<JobStatus.JobRun> lastSuccess = jobStatus.lastSuccess();
- if ( ! lastSuccess.isPresent()) return false;
- return lastSuccess.get().version().equals(((Change.VersionChange)change).version());
- }
-
- /**
* Returns whether the current deployed version in the zone given by the job
* is newer than the given version. This may be the case even if the production job
* in question failed, if the failure happens after deployment.
@@ -454,7 +359,7 @@ public class DeploymentTrigger {
* downgrade production nodes which we are not guaranteed to support.
*/
private boolean isOnNewerVersionInProductionThan(Version version, Application application, JobType job) {
- if ( ! isProduction(job)) return false;
+ if ( ! job.isProduction()) return false;
Optional<Zone> zone = job.zone(controller.system());
if ( ! zone.isPresent()) return false;
Deployment existingDeployment = application.deployments().get(zone.get());
@@ -462,23 +367,17 @@ public class DeploymentTrigger {
return existingDeployment.version().isAfter(version);
}
- private boolean isProduction(JobType job) {
- Optional<Zone> zone = job.zone(controller.system());
- if ( ! zone.isPresent()) return false; // arbitrary
- return zone.get().environment() == Environment.prod;
- }
-
private boolean acceptNewRevisionNow(LockedApplication application) {
if ( ! application.deploying().isPresent()) return true;
- if ( application.deploying().get() instanceof Change.ApplicationChange) return true; // more changes are ok
-
- if ( application.deploymentJobs().hasFailures()) return true; // allow changes to fix upgrade problems
- if ( application.isBlocked(clock.instant())) return true; // allow testing changes while upgrade blocked (debatable)
+
+ if (application.deploying().get() instanceof Change.ApplicationChange) return true; // more changes are ok
+
+ if (application.deploymentJobs().hasFailures()) return true; // allow changes to fix upgrade problems
+
+ if (application.isBlocked(clock.instant())) return true; // allow testing changes while upgrade blocked (debatable)
+
+ // Otherwise, the application is currently upgrading, without failures, and we should wait with the revision.
return false;
}
- public BuildSystem buildSystem() { return buildSystem; }
-
- public DeploymentOrder deploymentOrder() { return order; }
-
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/PolledBuildSystem.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/PolledBuildSystem.java
index 56b4023f932..e25db10a8cd 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/PolledBuildSystem.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/PolledBuildSystem.java
@@ -87,9 +87,9 @@ public class PolledBuildSystem implements BuildSystem {
Optional<Long> projectId = projectId(application);
if (projectId.isPresent()) {
- jobsToRun.add(new BuildJob(projectId.get(), jobType.id()));
+ jobsToRun.add(new BuildJob(projectId.get(), jobType.jobName()));
} else {
- log.warning("Not queuing " + jobType.id() + " for " + application.toShortString() +
+ log.warning("Not queuing " + jobType.jobName() + " for " + application.toShortString() +
" because project ID is missing");
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmer.java
new file mode 100644
index 00000000000..09f8df58205
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmer.java
@@ -0,0 +1,92 @@
+package com.yahoo.vespa.hosted.controller.maintenance;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.curator.Lock;
+import com.yahoo.vespa.hosted.controller.Application;
+import com.yahoo.vespa.hosted.controller.Controller;
+import com.yahoo.vespa.hosted.controller.api.Tenant;
+import com.yahoo.vespa.hosted.controller.api.application.v4.model.TenantType;
+import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
+import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
+import com.yahoo.vespa.hosted.controller.api.integration.organization.OwnershipIssues;
+import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
+import com.yahoo.vespa.hosted.controller.api.integration.organization.User;
+
+import java.time.Duration;
+import java.util.NoSuchElementException;
+import java.util.Optional;
+import java.util.logging.Level;
+
+/**
+ * Periodically request application ownership confirmation through filing issues.
+ *
+ * When to file new issues, escalate inactive ones, etc., is handled by the enclosed OwnershipIssues.
+ *
+ * @author jvenstad
+ */
+public class ApplicationOwnershipConfirmer extends Maintainer {
+
+ private final OwnershipIssues ownershipIssues;
+
+ public ApplicationOwnershipConfirmer(Controller controller, Duration interval, JobControl jobControl, OwnershipIssues ownershipIssues) {
+ super(controller, interval, jobControl);
+ this.ownershipIssues = ownershipIssues;
+ }
+
+ @Override
+ protected void maintain() {
+ confirmApplicationOwnerships();
+ ensureConfirmationResponses();
+ }
+
+ /** File an ownership issue with the owners of all applications we know about. */
+ private void confirmApplicationOwnerships() {
+ for (Application application : controller().applications().asList())
+ if (application.id().instance().value().startsWith("default-pr") || application.productionDeployments().isEmpty())
+ store(null, application.id());
+ else
+ try {
+ Tenant tenant = ownerOf(application.id());
+ Optional<IssueId> ourIssueId = application.ownershipIssueId();
+ ourIssueId = tenant.tenantType() == TenantType.USER
+ ? ownershipIssues.confirmOwnership(ourIssueId, application.id(), userFor(tenant))
+ : ownershipIssues.confirmOwnership(ourIssueId, application.id(), propertyIdFor(tenant));
+ ourIssueId.ifPresent(issueId -> store(issueId, application.id()));
+ }
+ catch (RuntimeException e) { // Catch errors due to wrong data in the controller, or issues client timeout.
+ log.log(Level.WARNING, "Exception caught when attempting to file an issue for " + application.id(), e);
+ }
+ }
+
+ /** Escalate ownership issues which have not been closed before a defined amount of time has passed. */
+ private void ensureConfirmationResponses() {
+ for (Application application : controller().applications().asList())
+ application.ownershipIssueId().ifPresent(issueId -> {
+ try {
+ ownershipIssues.ensureResponse(issueId, ownerOf(application.id()).getPropertyId());
+ }
+ catch (RuntimeException e) {
+ log.log(Level.WARNING, "Exception caught when attempting to escalate issue with id " + issueId, e);
+ }
+ });
+ }
+
+ private Tenant ownerOf(ApplicationId applicationId) {
+ return controller().tenants().tenant(new TenantId(applicationId.tenant().value()))
+ .orElseThrow(() -> new IllegalStateException("No tenant found for application " + applicationId));
+ }
+
+ protected User userFor(Tenant tenant) {
+ return User.from(tenant.getId().id().replaceFirst("by-", ""));
+ }
+
+ protected PropertyId propertyIdFor(Tenant tenant) {
+ return tenant.getPropertyId()
+ .orElseThrow(() -> new NoSuchElementException("No PropertyId is listed for non-user tenant " + tenant));
+ }
+
+ protected void store(IssueId issueId, ApplicationId applicationId) {
+ controller().applications().lockedIfPresent(applicationId, application ->
+ controller().applications().store(application.withOwnershipIssueId(issueId)));
+ }
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ClusterInfoMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ClusterInfoMaintainer.java
index 275aedfc812..ae617f87be6 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ClusterInfoMaintainer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ClusterInfoMaintainer.java
@@ -10,6 +10,7 @@ import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.LockedApplication;
import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeList;
+import com.yahoo.vespa.hosted.controller.application.ApplicationList;
import com.yahoo.vespa.hosted.controller.application.ClusterInfo;
import com.yahoo.vespa.hosted.controller.application.Deployment;
@@ -86,22 +87,17 @@ public class ClusterInfoMaintainer extends Maintainer {
@Override
protected void maintain() {
- for (Application application : controller().applications().asList()) {
- try (Lock lock = controller().applications().lock(application.id())) {
- Optional<LockedApplication> lockedApplication = controller.applications().get(application.id(), lock);
- if (!lockedApplication.isPresent()) continue; // application removed
-
- for (Deployment deployment : lockedApplication.get().deployments().values()) {
- DeploymentId deploymentId = new DeploymentId(application.id(), deployment.zone());
- try {
- NodeList nodes = controller().applications().configserverClient().getNodeList(deploymentId);
- Map<ClusterSpec.Id, ClusterInfo> clusterInfo = getClusterInfo(nodes, deployment.zone());
- controller.applications().store(lockedApplication.get()
- .with(deployment.withClusterInfo(clusterInfo)));
- }
- catch (IOException | IllegalArgumentException e) {
- log.log(Level.WARNING, "Failing getting cluster info of for " + deploymentId, e);
- }
+ for (Application application : ApplicationList.from(controller().applications().asList()).notPullRequest().asList()) {
+ for (Deployment deployment : application.deployments().values()) {
+ DeploymentId deploymentId = new DeploymentId(application.id(), deployment.zone());
+ try {
+ NodeList nodes = controller().applications().configserverClient().getNodeList(deploymentId);
+ Map<ClusterSpec.Id, ClusterInfo> clusterInfo = getClusterInfo(nodes, deployment.zone());
+ controller().applications().lockedIfPresent(application.id(), lockedApplication ->
+ controller.applications().store(lockedApplication.withClusterInfo(deployment.zone(), clusterInfo)));
+ }
+ catch (IOException | IllegalArgumentException e) {
+ log.log(Level.WARNING, "Failing getting cluster info of for " + deploymentId, e);
}
}
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ClusterUtilizationMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ClusterUtilizationMaintainer.java
index 60b890f10fb..3744be67135 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ClusterUtilizationMaintainer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ClusterUtilizationMaintainer.java
@@ -7,15 +7,14 @@ import com.yahoo.config.provision.Zone;
import com.yahoo.vespa.curator.Lock;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.LockedApplication;
import com.yahoo.vespa.hosted.controller.api.integration.MetricsService;
+import com.yahoo.vespa.hosted.controller.application.ApplicationList;
import com.yahoo.vespa.hosted.controller.application.ClusterUtilization;
import com.yahoo.vespa.hosted.controller.application.Deployment;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
-import java.util.Optional;
/**
* Fetch utilization metrics and update applications with this data.
@@ -46,16 +45,15 @@ public class ClusterUtilizationMaintainer extends Maintainer {
@Override
protected void maintain() {
- for (Application application : controller().applications().asList()) {
- try (Lock lock = controller().applications().lock(application.id())) {
- Optional<LockedApplication> lockedApplication = controller.applications().get(application.id(), lock);
- if (!lockedApplication.isPresent()) continue; // application removed
- for (Deployment deployment : application.deployments().values()) {
- Map<ClusterSpec.Id, ClusterUtilization> clusterUtilization = getUpdatedClusterUtilizations(application.id(), deployment.zone());
- controller.applications().store(lockedApplication.get()
- .with(deployment.withClusterUtils(clusterUtilization)));
- }
+ for (Application application : ApplicationList.from(controller().applications().asList()).notPullRequest().asList()) {
+ for (Deployment deployment : application.deployments().values()) {
+
+ Map<ClusterSpec.Id, ClusterUtilization> clusterUtilization = getUpdatedClusterUtilizations(application.id(), deployment.zone());
+
+ controller().applications().lockedIfPresent(application.id(), lockedApplication ->
+ controller().applications().store(lockedApplication.withClusterUtilization(deployment.zone(), clusterUtilization)));
}
}
}
+
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java
index 2fdce2802ab..bc2112ac0ca 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java
@@ -4,6 +4,7 @@ package com.yahoo.vespa.hosted.controller.maintenance;
import com.yahoo.component.AbstractComponent;
import com.yahoo.jdisc.Metric;
import com.yahoo.vespa.hosted.controller.Controller;
+import com.yahoo.vespa.hosted.controller.api.integration.organization.OwnershipIssues;
import com.yahoo.vespa.hosted.controller.api.integration.organization.DeploymentIssues;
import com.yahoo.vespa.hosted.controller.api.integration.chef.Chef;
import com.yahoo.vespa.hosted.controller.maintenance.config.MaintainerConfig;
@@ -25,34 +26,32 @@ public class ControllerMaintenance extends AbstractComponent {
private final DeploymentExpirer deploymentExpirer;
private final DeploymentIssueReporter deploymentIssueReporter;
private final MetricsReporter metricsReporter;
- private final FailureRedeployer failureRedeployer;
private final OutstandingChangeDeployer outstandingChangeDeployer;
private final VersionStatusUpdater versionStatusUpdater;
private final Upgrader upgrader;
- private final DelayedDeployer delayedDeployer;
- private final BlockedChangeDeployer blockedChangeDeployer;
+ private final ReadyJobsTrigger readyJobsTrigger;
private final ClusterInfoMaintainer clusterInfoMaintainer;
private final ClusterUtilizationMaintainer clusterUtilizationMaintainer;
private final DeploymentMetricsMaintainer deploymentMetricsMaintainer;
+ private final ApplicationOwnershipConfirmer applicationOwnershipConfirmer;
@SuppressWarnings("unused") // instantiated by Dependency Injection
public ControllerMaintenance(MaintainerConfig maintainerConfig, Controller controller, CuratorDb curator,
JobControl jobControl, Metric metric, Chef chefClient,
- DeploymentIssues deploymentIssues) {
+ DeploymentIssues deploymentIssues, OwnershipIssues ownershipIssues) {
Duration maintenanceInterval = Duration.ofMinutes(maintainerConfig.intervalMinutes());
this.jobControl = jobControl;
deploymentExpirer = new DeploymentExpirer(controller, maintenanceInterval, jobControl);
deploymentIssueReporter = new DeploymentIssueReporter(controller, deploymentIssues, maintenanceInterval, jobControl);
metricsReporter = new MetricsReporter(controller, metric, chefClient, jobControl, controller.system());
- failureRedeployer = new FailureRedeployer(controller, maintenanceInterval, jobControl);
outstandingChangeDeployer = new OutstandingChangeDeployer(controller, maintenanceInterval, jobControl);
versionStatusUpdater = new VersionStatusUpdater(controller, Duration.ofMinutes(3), jobControl);
upgrader = new Upgrader(controller, maintenanceInterval, jobControl, curator);
- delayedDeployer = new DelayedDeployer(controller, maintenanceInterval, jobControl);
- blockedChangeDeployer = new BlockedChangeDeployer(controller, maintenanceInterval, jobControl);
+ readyJobsTrigger = new ReadyJobsTrigger(controller, maintenanceInterval, jobControl);
clusterInfoMaintainer = new ClusterInfoMaintainer(controller, Duration.ofHours(2), jobControl);
clusterUtilizationMaintainer = new ClusterUtilizationMaintainer(controller, Duration.ofHours(2), jobControl);
deploymentMetricsMaintainer = new DeploymentMetricsMaintainer(controller, Duration.ofMinutes(10), jobControl);
+ applicationOwnershipConfirmer = new ApplicationOwnershipConfirmer(controller, Duration.ofHours(12), jobControl, ownershipIssues);
}
public Upgrader upgrader() { return upgrader; }
@@ -65,15 +64,14 @@ public class ControllerMaintenance extends AbstractComponent {
deploymentExpirer.deconstruct();
deploymentIssueReporter.deconstruct();
metricsReporter.deconstruct();
- failureRedeployer.deconstruct();
outstandingChangeDeployer.deconstruct();
versionStatusUpdater.deconstruct();
upgrader.deconstruct();
- delayedDeployer.deconstruct();
- blockedChangeDeployer.deconstruct();
+ readyJobsTrigger.deconstruct();
clusterUtilizationMaintainer.deconstruct();
clusterInfoMaintainer.deconstruct();
deploymentMetricsMaintainer.deconstruct();
+ applicationOwnershipConfirmer.deconstruct();
}
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DelayedDeployer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DelayedDeployer.java
deleted file mode 100644
index cb09c41a034..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DelayedDeployer.java
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.vespa.hosted.controller.Controller;
-
-import java.time.Duration;
-
-/**
- * Maintenance job which triggers jobs that have been delayed according to the applications deployment spec.
- *
- * @author mpolden
- */
-public class DelayedDeployer extends Maintainer {
-
- public DelayedDeployer(Controller controller, Duration interval, JobControl jobControl) {
- super(controller, interval, jobControl);
- }
-
- @Override
- protected void maintain() {
- controller().applications().deploymentTrigger().triggerDelayed();
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporter.java
index b4708dccb6b..ae6ba364d25 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporter.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporter.java
@@ -67,7 +67,7 @@ public class DeploymentIssueReporter extends Maintainer {
if (failingApplications.contains(application.id()))
fileDeploymentIssueFor(application.id());
else
- storeIssueId(application.id(), null);
+ store(application.id(), null);
}
/**
@@ -111,7 +111,7 @@ public class DeploymentIssueReporter extends Maintainer {
IssueId issueId = tenant.tenantType() == TenantType.USER
? deploymentIssues.fileUnlessOpen(ourIssueId, applicationId, userFor(tenant))
: deploymentIssues.fileUnlessOpen(ourIssueId, applicationId, propertyIdFor(tenant));
- storeIssueId(applicationId, issueId);
+ store(applicationId, issueId);
}
catch (RuntimeException e) { // Catch errors due to wrong data in the controller, or issues client timeout.
log.log(Level.WARNING, "Exception caught when attempting to file an issue for " + applicationId, e);
@@ -130,12 +130,9 @@ public class DeploymentIssueReporter extends Maintainer {
}));
}
- private void storeIssueId(ApplicationId id, IssueId issueId) {
- try (Lock lock = controller().applications().lock(id)) {
- controller().applications().get(id, lock).ifPresent(
- application -> controller().applications().store(application.with(issueId))
- );
- }
+ private void store(ApplicationId id, IssueId issueId) {
+ controller().applications().lockedIfPresent(id, application ->
+ controller().applications().store(application.withDeploymentIssueId(issueId)));
}
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentMetricsMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentMetricsMaintainer.java
index 2e6e378272d..13eb5075f34 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentMetricsMaintainer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentMetricsMaintainer.java
@@ -4,15 +4,14 @@ package com.yahoo.vespa.hosted.controller.maintenance;// Copyright 2017 Yahoo Ho
import com.yahoo.vespa.curator.Lock;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.LockedApplication;
import com.yahoo.vespa.hosted.controller.api.integration.MetricsService;
+import com.yahoo.vespa.hosted.controller.application.ApplicationList;
import com.yahoo.vespa.hosted.controller.application.Deployment;
import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics;
import com.yahoo.yolean.Exceptions;
import java.io.UncheckedIOException;
import java.time.Duration;
-import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -33,38 +32,31 @@ public class DeploymentMetricsMaintainer extends Maintainer {
@Override
protected void maintain() {
boolean hasWarned = false;
- for (Application application : controller().applications().asList()) {
- for (Deployment deployment : application.deployments().values()) {
- try {
- MetricsService.DeploymentMetrics metrics = controller().metricsService()
- .getDeploymentMetrics(application.id(), deployment.zone());
- DeploymentMetrics appMetrics = new DeploymentMetrics(metrics.queriesPerSecond(), metrics.writesPerSecond(),
- metrics.documentCount(), metrics.queryLatencyMillis(), metrics.writeLatencyMillis());
-
- try (Lock lock = controller().applications().lock(application.id())) {
-
- // Deployment or application may have changed (or be gone) now:
- Optional<LockedApplication> lockedApplication = controller().applications()
- .get(application.id(), lock);
- if (!lockedApplication.isPresent()) continue;
+ for (Application application : ApplicationList.from(controller().applications().asList()).notPullRequest().asList()) {
+ try {
+ controller().applications().lockedIfPresent(application.id(), lockedApplication ->
+ controller().applications().store(lockedApplication.with(controller().metricsService().getApplicationMetrics(application.id()))));
- deployment = lockedApplication.get().deployments().get(deployment.zone());
- if (deployment == null) continue;
-
- controller().applications().store(lockedApplication.get()
- .with(deployment.withMetrics(appMetrics)));
- }
- }
- catch (UncheckedIOException e) {
- if ( ! hasWarned) // produce only one warning per maintenance interval
- log.log(Level.WARNING, "Failed talking to YAMAS: " + Exceptions.toMessageString(e) +
- ". Retrying in " + maintenanceInterval());
- hasWarned = true;
+ for (Deployment deployment : application.deployments().values()) {
+ MetricsService.DeploymentMetrics deploymentMetrics = controller().metricsService()
+ .getDeploymentMetrics(application.id(), deployment.zone());
+ DeploymentMetrics appMetrics = new DeploymentMetrics(deploymentMetrics.queriesPerSecond(),
+ deploymentMetrics.writesPerSecond(),
+ deploymentMetrics.documentCount(),
+ deploymentMetrics.queryLatencyMillis(),
+ deploymentMetrics.writeLatencyMillis());
+
+ controller().applications().lockedIfPresent(application.id(), lockedApplication ->
+ controller().applications().store(lockedApplication.with(deployment.zone(), appMetrics)));
}
}
+ catch (UncheckedIOException e) {
+ if (!hasWarned) // produce only one warning per maintenance interval
+ log.log(Level.WARNING, "Failed talking to YAMAS: " + Exceptions.toMessageString(e) +
+ ". Retrying in " + maintenanceInterval());
+ hasWarned = true;
+ }
}
-
}
}
-
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/FailureRedeployer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/FailureRedeployer.java
deleted file mode 100644
index 72f8faa5180..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/FailureRedeployer.java
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.application.ApplicationList;
-
-import java.time.Duration;
-import java.util.List;
-
-/**
- * Attempts redeployment of failed jobs and deployments.
- *
- * @author bratseth
- * @author mpolden
- */
-public class FailureRedeployer extends Maintainer {
-
- public FailureRedeployer(Controller controller, Duration interval, JobControl jobControl) {
- super(controller, interval, jobControl);
- }
-
- @Override
- public void maintain() {
- List<Application> applications = ApplicationList.from(controller().applications().asList())
- .notPullRequest()
- .asList();
- applications.forEach(application -> triggerFailing(application));
- }
-
- private void triggerFailing(Application application) {
- controller().applications().deploymentTrigger().triggerFailing(application.id());
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/JobControl.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/JobControl.java
index d7396cb2acb..6aa1b89c605 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/JobControl.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/JobControl.java
@@ -5,6 +5,7 @@ import com.yahoo.vespa.curator.Lock;
import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
import java.util.HashSet;
+import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.logging.Logger;
@@ -40,7 +41,7 @@ public class JobControl {
* Returns a snapshot of the set of jobs started on this system (whether deactivated or not).
* Each job is represented by its simple (omitting package) class name.
*/
- public Set<String> jobs() { return new HashSet<>(startedJobs); }
+ public Set<String> jobs() { return new LinkedHashSet<>(startedJobs); }
/** Returns an unmodifiable set containing the currently inactive jobs in this */
public Set<String> inactiveJobs() { return curator.readInactiveJobs(); }
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/Maintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/Maintainer.java
index bbef7980273..ebab2054d4f 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/Maintainer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/Maintainer.java
@@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.controller.maintenance;
import com.google.common.util.concurrent.UncheckedTimeoutException;
import com.yahoo.component.AbstractComponent;
+import com.yahoo.component.ComponentId;
import com.yahoo.vespa.curator.Lock;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporter.java
index 3d0cd284c55..01e53ce4f79 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporter.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporter.java
@@ -9,6 +9,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.chef.AttributeMapping;
import com.yahoo.vespa.hosted.controller.api.integration.chef.Chef;
import com.yahoo.vespa.hosted.controller.api.integration.chef.rest.PartialNode;
import com.yahoo.vespa.hosted.controller.api.integration.chef.rest.PartialNodeResult;
+import com.yahoo.vespa.hosted.controller.application.ApplicationList;
import java.time.Clock;
import java.time.Duration;
@@ -102,9 +103,12 @@ public class MetricsReporter extends Maintainer {
}
private double deploymentFailRatio() {
- List<Application> applications = controller().applications().asList();
+ List<Application> applications = ApplicationList.from(controller().applications().asList())
+ .notPullRequest()
+ .hasProductionDeployment()
+ .asList();
if (applications.isEmpty()) return 0;
-
+
return (double)applications.stream().filter(a -> a.deploymentJobs().hasFailures()).count() /
(double)applications.size();
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/BlockedChangeDeployer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ReadyJobsTrigger.java
index 4a68fd6cfab..f165b4e4ea3 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/BlockedChangeDeployer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ReadyJobsTrigger.java
@@ -14,14 +14,14 @@ import java.time.Duration;
* @author bratseth
*/
@SuppressWarnings("unused")
-public class BlockedChangeDeployer extends Maintainer {
+public class ReadyJobsTrigger extends Maintainer {
- public BlockedChangeDeployer(Controller controller, Duration interval, JobControl jobControl) {
+ public ReadyJobsTrigger(Controller controller, Duration interval, JobControl jobControl) {
super(controller, interval, jobControl);
}
@Override
- protected void maintain() {
+ public void maintain() {
controller().applications().deploymentTrigger().triggerReadyJobs();
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/Upgrader.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/Upgrader.java
index 36b87e4cead..5b87f9eaa86 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/Upgrader.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/Upgrader.java
@@ -26,8 +26,6 @@ import java.util.logging.Logger;
*/
public class Upgrader extends Maintainer {
- private static final Duration upgradeTimeout = Duration.ofHours(12);
-
private static final Logger log = Logger.getLogger(Upgrader.class.getName());
private final CuratorDb curator;
@@ -41,23 +39,32 @@ public class Upgrader extends Maintainer {
* Schedule application upgrades. Note that this implementation must be idempotent.
*/
@Override
- public void maintain() {
- ApplicationList applications = applications();
-
+ public void maintain() {
// Determine target versions for each upgrade policy
Optional<Version> canaryTarget = controller().versionStatus().systemVersion().map(VespaVersion::versionNumber);
Optional<Version> defaultTarget = newestVersionWithConfidence(VespaVersion.Confidence.normal);
Optional<Version> conservativeTarget = newestVersionWithConfidence(VespaVersion.Confidence.high);
- // Cancel any upgrades to the wrong targets
- cancelUpgradesOf(applications.with(UpgradePolicy.canary).upgrading().notUpgradingTo(canaryTarget));
- cancelUpgradesOf(applications.with(UpgradePolicy.defaultPolicy).upgrading().notUpgradingTo(defaultTarget));
- cancelUpgradesOf(applications.with(UpgradePolicy.conservative).upgrading().notUpgradingTo(conservativeTarget));
+ // Cancel upgrades to broken targets (let other ongoing upgrades complete to avoid starvation
+ for (VespaVersion version : controller().versionStatus().versions()) {
+ if (version.confidence() == VespaVersion.Confidence.broken)
+ cancelUpgradesOf(applications().without(UpgradePolicy.canary).upgradingTo(version.versionNumber()),
+ version.versionNumber() + " is broken");
+ }
+
+ // Canaries should always try the canary target
+ cancelUpgradesOf(applications().with(UpgradePolicy.canary).upgrading().notUpgradingTo(canaryTarget),
+ "Outdated target version for Canaries");
+
+ // Cancel *failed* upgrades to earlier versions, as the new version may fix it
+ String reason = "Failing on outdated version";
+ cancelUpgradesOf(applications().with(UpgradePolicy.defaultPolicy).upgrading().failing().notUpgradingTo(defaultTarget), reason);
+ cancelUpgradesOf(applications().with(UpgradePolicy.conservative).upgrading().failing().notUpgradingTo(conservativeTarget), reason);
// Schedule the right upgrades
- canaryTarget.ifPresent(target -> upgrade(applications.with(UpgradePolicy.canary), target));
- defaultTarget.ifPresent(target -> upgrade(applications.with(UpgradePolicy.defaultPolicy), target));
- conservativeTarget.ifPresent(target -> upgrade(applications.with(UpgradePolicy.conservative), target));
+ canaryTarget.ifPresent(target -> upgrade(applications().with(UpgradePolicy.canary), target));
+ defaultTarget.ifPresent(target -> upgrade(applications().with(UpgradePolicy.defaultPolicy), target));
+ conservativeTarget.ifPresent(target -> upgrade(applications().with(UpgradePolicy.conservative), target));
}
private Optional<Version> newestVersionWithConfidence(VespaVersion.Confidence confidence) {
@@ -79,13 +86,11 @@ public class Upgrader extends Maintainer {
private void upgrade(ApplicationList applications, Version version) {
Change.VersionChange change = new Change.VersionChange(version);
- cancelUpgradesOf(applications.upgradingToLowerThan(version));
applications = applications.notPullRequest(); // Pull requests are deployed as separate applications to test then deleted; No need to upgrade
applications = applications.hasProductionDeployment();
applications = applications.onLowerVersionThan(version);
- applications = applications.notDeployingApplication(); // wait with applications deploying an application change
+ applications = applications.notDeploying(); // wait with applications deploying an application change or already upgrading
applications = applications.notFailingOn(version); // try to upgrade only if it hasn't failed on this version
- applications = applications.notCurrentlyUpgrading(change, controller().applications().deploymentTrigger().jobTimeoutLimit());
applications = applications.canUpgradeAt(controller().clock().instant()); // wait with applications that are currently blocking upgrades
applications = applications.byIncreasingDeployedVersion(); // start with lowest versions
applications = applications.first(numberOfApplicationsToUpgrade()); // throttle upgrades
@@ -98,9 +103,9 @@ public class Upgrader extends Maintainer {
}
}
- private void cancelUpgradesOf(ApplicationList applications) {
+ private void cancelUpgradesOf(ApplicationList applications, String reason) {
if (applications.isEmpty()) return;
- log.info("Cancelling upgrading of " + applications.asList().size() + " applications");
+ log.info("Cancelling upgrading of " + applications.asList().size() + " applications: " + reason);
for (Application application : applications.asList())
controller().applications().deploymentTrigger().cancelChange(application.id());
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java
index 3bd1abdf607..23316a74aae 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java
@@ -15,6 +15,7 @@ import com.yahoo.slime.Inspector;
import com.yahoo.slime.Slime;
import com.yahoo.vespa.config.SlimeUtils;
import com.yahoo.vespa.hosted.controller.Application;
+import com.yahoo.vespa.hosted.controller.api.integration.MetricsService.ApplicationMetrics;
import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
import com.yahoo.vespa.hosted.controller.application.ApplicationRevision;
import com.yahoo.vespa.hosted.controller.application.Change;
@@ -51,7 +52,10 @@ public class ApplicationSerializer {
private final String deploymentJobsField = "deploymentJobs";
private final String deployingField = "deployingField";
private final String outstandingChangeField = "outstandingChangeField";
-
+ private final String ownershipIssueIdField = "ownershipIssueId";
+ private final String writeQualityField = "writeQuality";
+ private final String queryQualityField = "queryQuality";
+
// Deployment fields
private final String zoneField = "zone";
private final String environmentField = "environment";
@@ -123,6 +127,9 @@ public class ApplicationSerializer {
toSlime(application.deploymentJobs(), root.setObject(deploymentJobsField));
toSlime(application.deploying(), root);
root.setBool(outstandingChangeField, application.hasOutstandingChange());
+ application.ownershipIssueId().ifPresent(issueId -> root.setString(ownershipIssueIdField, issueId.value()));
+ root.setDouble(queryQualityField, application.metrics().queryServiceQuality());
+ root.setDouble(writeQualityField, application.metrics().writeServiceQuality());
return slime;
}
@@ -202,9 +209,7 @@ public class ApplicationSerializer {
}
private void toSlime(DeploymentJobs deploymentJobs, Cursor cursor) {
- deploymentJobs.projectId()
- .filter(id -> id > 0) // TODO: Discards invalid data. Remove filter after October 2017
- .ifPresent(projectId -> cursor.setLong(projectIdField, projectId));
+ deploymentJobs.projectId().ifPresent(projectId -> cursor.setLong(projectIdField, projectId));
jobStatusToSlime(deploymentJobs.jobStatus().values(), cursor.setArray(jobStatusField));
deploymentJobs.issueId().ifPresent(jiraIssueId -> cursor.setString(issueIdField, jiraIssueId.value()));
}
@@ -215,7 +220,7 @@ public class ApplicationSerializer {
}
private void toSlime(JobStatus jobStatus, Cursor object) {
- object.setString(jobTypeField, jobStatus.type().id());
+ object.setString(jobTypeField, jobStatus.type().jobName());
if (jobStatus.jobError().isPresent())
object.setString(errorField, jobStatus.jobError().get().name());
@@ -259,9 +264,12 @@ public class ApplicationSerializer {
DeploymentJobs deploymentJobs = deploymentJobsFromSlime(root.field(deploymentJobsField));
Optional<Change> deploying = changeFromSlime(root.field(deployingField));
boolean outstandingChange = root.field(outstandingChangeField).asBool();
+ Optional<IssueId> ownershipIssueId = optionalString(root.field(ownershipIssueIdField)).map(IssueId::from);
+ ApplicationMetrics metrics = new ApplicationMetrics(root.field(queryQualityField).asDouble(),
+ root.field(writeQualityField).asDouble());
- return new Application(id, deploymentSpec, validationOverrides, deployments,
- deploymentJobs, deploying, outstandingChange);
+ return new Application(id, deploymentSpec, validationOverrides, deployments,
+ deploymentJobs, deploying, outstandingChange, ownershipIssueId, metrics);
}
private List<Deployment> deploymentsFromSlime(Inspector array) {
@@ -347,8 +355,7 @@ public class ApplicationSerializer {
}
private DeploymentJobs deploymentJobsFromSlime(Inspector object) {
- Optional<Long> projectId = optionalLong(object.field(projectIdField))
- .filter(id -> id > 0); // TODO: Discards invalid data. Remove filter after October 2017
+ Optional<Long> projectId = optionalLong(object.field(projectIdField));
List<JobStatus> jobStatusList = jobStatusListFromSlime(object.field(jobStatusField));
Optional<IssueId> issueId = optionalString(object.field(issueIdField)).map(IssueId::from);
@@ -373,7 +380,7 @@ public class ApplicationSerializer {
}
private JobStatus jobStatusFromSlime(Inspector object) {
- DeploymentJobs.JobType jobType = DeploymentJobs.JobType.fromId(object.field(jobTypeField).asString());
+ DeploymentJobs.JobType jobType = DeploymentJobs.JobType.fromJobName(object.field(jobTypeField).asString());
Optional<JobError> jobError = Optional.empty();
if (object.field(errorField).valid())
@@ -388,7 +395,7 @@ public class ApplicationSerializer {
private Optional<JobStatus.JobRun> jobRunFromSlime(Inspector object) {
if ( ! object.valid()) return Optional.empty();
- return Optional.of(new JobStatus.JobRun(optionalLong(object.field(jobRunIdField)).orElse(-1L), // TODO: Make non-optional after November 2017
+ return Optional.of(new JobStatus.JobRun(optionalLong(object.field(jobRunIdField)).orElse(-1L), // TODO: Make non-optional after November 2017 -- what about lastTriggered?
new Version(object.field(versionField).asString()),
applicationRevisionFromSlime(object.field(revisionField)),
object.field(upgradeField).asBool(),
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java
index 68df16504a8..e5616f025ce 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java
@@ -2,10 +2,7 @@
package com.yahoo.vespa.hosted.controller.persistence;
import com.google.inject.Inject;
-import com.yahoo.cloud.config.ClusterInfoConfig;
-import com.yahoo.cloud.config.ZookeeperServerConfig;
import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.net.HostName;
import com.yahoo.path.Path;
import com.yahoo.transaction.NestedTransaction;
import com.yahoo.vespa.config.SlimeUtils;
@@ -14,7 +11,6 @@ import com.yahoo.vespa.curator.Lock;
import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
import com.yahoo.vespa.hosted.controller.application.DeploymentJobs;
import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
-import com.yahoo.vespa.zookeeper.ZooKeeperServer;
import java.io.IOException;
import java.io.UncheckedIOException;
@@ -30,7 +26,6 @@ import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
-import java.util.stream.Collectors;
/**
* Curator backed database for storing working state shared between controller servers.
@@ -40,9 +35,6 @@ import java.util.stream.Collectors;
*/
public class CuratorDb {
- /** Use a nonstandard zk port to avoid interfering with connection to the config server zk cluster */
- private static final int zooKeeperPort = 2281;
-
private static final Logger log = Logger.getLogger(CuratorDb.class.getName());
private static final Path root = Path.fromString("/controller/v1");
@@ -52,9 +44,6 @@ public class CuratorDb {
private final StringSetSerializer stringSetSerializer = new StringSetSerializer();
private final JobQueueSerializer jobQueueSerializer = new JobQueueSerializer();
- @SuppressWarnings("unused") // This server is used (only) from the curator instance of this over the network */
- private final ZooKeeperServer zooKeeperServer;
-
private final Curator curator;
/**
@@ -63,54 +52,11 @@ public class CuratorDb {
*/
private final ConcurrentHashMap<Path, Lock> locks = new ConcurrentHashMap<>();
- /** Create a curator db which also set up a ZooKeeper server (such that this instance is both client and server) */
@Inject
- public CuratorDb(ClusterInfoConfig clusterInfo) {
- this.zooKeeperServer = new ZooKeeperServer(toZookeeperServerConfig(clusterInfo));
- this.curator = new Curator(toConnectionSpec(clusterInfo));
- }
-
- /** Create a curator db which does not set up a server, using the given Curator instance */
- protected CuratorDb(Curator curator) {
- this.zooKeeperServer = null;
+ public CuratorDb(Curator curator) {
this.curator = curator;
}
- private static ZookeeperServerConfig toZookeeperServerConfig(ClusterInfoConfig clusterInfo) {
- ZookeeperServerConfig.Builder b = new ZookeeperServerConfig.Builder();
- b.zooKeeperConfigFile("conf/zookeeper/controller-zookeeper.cfg");
- b.dataDir("var/controller-zookeeper");
- b.clientPort(zooKeeperPort);
- b.myidFile("var/controller-zookeeper/myid");
- b.myid(myIndex(clusterInfo));
-
- for (ClusterInfoConfig.Services clusterMember : clusterInfo.services()) {
- ZookeeperServerConfig.Server.Builder server = new ZookeeperServerConfig.Server.Builder();
- server.id(clusterMember.index());
- server.hostname(clusterMember.hostname());
- server.quorumPort(zooKeeperPort + 1);
- server.electionPort(zooKeeperPort + 2);
- b.server(server);
- }
- return new ZookeeperServerConfig(b);
- }
-
- private static Integer myIndex(ClusterInfoConfig clusterInfo) {
- String hostname = HostName.getLocalhost();
- return clusterInfo.services().stream()
- .filter(service -> service.hostname().equals(hostname))
- .map(ClusterInfoConfig.Services::index)
- .findFirst()
- .orElseThrow(() -> new IllegalStateException("Unable to find index for this node by hostname '" +
- hostname + "'"));
- }
-
- private static String toConnectionSpec(ClusterInfoConfig clusterInfo) {
- return clusterInfo.services().stream()
- .map(member -> member.hostname() + ":" + zooKeeperPort)
- .collect(Collectors.joining(","));
- }
-
// -------------- Locks --------------------------------------------------
public Lock lock(TenantId id, Duration timeout) {
@@ -230,10 +176,6 @@ public class CuratorDb {
VersionStatusSerializer serializer = new VersionStatusSerializer();
NestedTransaction transaction = new NestedTransaction();
try {
- // TODO: Removes unused data. Remove after October 2017
- if (curator.getData(systemVersionPath()).isPresent()) {
- curator.delete(systemVersionPath());
- }
curator.set(versionStatusPath(), SlimeUtils.toJsonBytes(serializer.toSlime(status)));
} catch (IOException e) {
throw new UncheckedIOException("Failed to serialize version status", e);
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/MemoryControllerDb.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/MemoryControllerDb.java
index 37677a5e393..ab240b9dea9 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/MemoryControllerDb.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/MemoryControllerDb.java
@@ -24,9 +24,9 @@ import java.util.stream.Collectors;
*/
public class MemoryControllerDb extends ControllerDb {
- private Map<TenantId, Tenant> tenants = new HashMap<>();
- private Map<String, Application> applications = new HashMap<>();
- private Map<RotationId, ApplicationId> rotationAssignments = new HashMap<>();
+ private final Map<TenantId, Tenant> tenants = new HashMap<>();
+ private final Map<String, Application> applications = new HashMap<>();
+ private final Map<RotationId, ApplicationId> rotationAssignments = new HashMap<>();
@Override
public void createTenant(Tenant tenant) {
@@ -46,23 +46,14 @@ public class MemoryControllerDb extends ControllerDb {
@Override
public void deleteTenant(TenantId tenantId) {
- Object removed = tenants.remove(tenantId);
- if (removed == null)
+ if (tenants.remove(tenantId) == null) {
throw new NotExistsException(tenantId);
+ }
}
@Override
public Optional<Tenant> getTenant(TenantId tenantId) throws PersistenceException {
- Optional<Tenant> tenant = Optional.ofNullable(tenants.get(tenantId));
- if(tenant.isPresent()) {
- Tenant t_noquota = tenant.get();
- Tenant t_withquota = new Tenant(
- t_noquota.getId(), t_noquota.getUserGroup(), t_noquota.getProperty(),
- t_noquota.getAthensDomain(), t_noquota.getPropertyId());
- return Optional.of(t_withquota);
- } else {
- return tenant;
- }
+ return Optional.ofNullable(tenants.get(tenantId));
}
@Override
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializer.java
index 81c3bb963db..6b60b49e1ef 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializer.java
@@ -13,6 +13,7 @@ import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
import java.time.Instant;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
@@ -40,6 +41,7 @@ public class VersionStatusSerializer {
private static final String versionField = "version";
private static final String failingField = "failing";
private static final String productionField = "production";
+ private static final String deployingField = "deploying";
public Slime toSlime(VersionStatus status) {
Slime slime = new Slime();
@@ -74,9 +76,10 @@ public class VersionStatusSerializer {
object.setString(versionField, statistics.version().toString());
applicationsToSlime(statistics.failing(), object.setArray(failingField));
applicationsToSlime(statistics.production(), object.setArray(productionField));
+ applicationsToSlime(statistics.deploying(), object.setArray(deployingField));
}
- private void applicationsToSlime(List<ApplicationId> applications, Cursor array) {
+ private void applicationsToSlime(Collection<ApplicationId> applications, Cursor array) {
applications.forEach(application -> array.addString(application.serializedForm()));
}
@@ -105,7 +108,8 @@ public class VersionStatusSerializer {
private DeploymentStatistics deploymentStatisticsFromSlime(Inspector object) {
return new DeploymentStatistics(Version.fromString(object.field(versionField).asString()),
applicationsFromSlime(object.field(failingField)),
- applicationsFromSlime(object.field(productionField)));
+ applicationsFromSlime(object.field(productionField)),
+ applicationsFromSlime(object.field(deployingField)));
}
private List<ApplicationId> applicationsFromSlime(Inspector array) {
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutor.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutor.java
new file mode 100644
index 00000000000..529acc48cbe
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutor.java
@@ -0,0 +1,14 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.proxy;
+
+import com.yahoo.container.jdisc.HttpResponse;
+
+/**
+ * Executes call against config servers and handles discovery requests. Rest URIs in the response are
+ * rewritten.
+ *
+ * @author Haakon Dybdahl
+ */
+public interface ConfigServerRestExecutor {
+ HttpResponse handle(ProxyRequest proxyRequest) throws ProxyException;
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutorImpl.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutorImpl.java
new file mode 100644
index 00000000000..e8b68d0c55a
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutorImpl.java
@@ -0,0 +1,243 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.proxy;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.yahoo.config.provision.Environment;
+import com.yahoo.config.provision.RegionName;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.io.IOUtils;
+import com.yahoo.jdisc.http.HttpRequest.Method;
+import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry;
+import org.apache.http.Header;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpDelete;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPatch;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.client.methods.HttpRequestBase;
+import org.apache.http.entity.InputStreamEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * @author Haakon Dybdahl
+ */
+@SuppressWarnings("unused") // Injected
+public class ConfigServerRestExecutorImpl implements ConfigServerRestExecutor {
+
+ private static final Duration PROXY_REQUEST_TIMEOUT = Duration.ofSeconds(10);
+
+ private final ZoneRegistry zoneRegistry;
+
+ public ConfigServerRestExecutorImpl(ZoneRegistry zoneRegistry) {
+ this.zoneRegistry = zoneRegistry;
+ }
+
+ @Override
+ public ProxyResponse handle(ProxyRequest proxyRequest) throws ProxyException {
+ if (proxyRequest.isDiscoveryRequest()) {
+ return createDiscoveryResponse(proxyRequest);
+ }
+
+ Environment environment = Environment.from(proxyRequest.getEnvironment());
+ RegionName region = RegionName.from(proxyRequest.getRegion());
+
+ // Make a local copy of the list as we want to manipulate it in case of ping problems.
+ final List<URI> allServers = new ArrayList<>(zoneRegistry.getConfigServerUris(environment, region));
+
+ StringBuilder errorBuilder = new StringBuilder();
+ if (queueFirstServerIfDown(allServers)) {
+ errorBuilder.append("Change ordering due to failed ping.");
+ }
+ for (URI uri : allServers) {
+ Optional<ProxyResponse> proxyResponse = proxyCall(uri, proxyRequest, errorBuilder);
+ if (proxyResponse.isPresent()) {
+ return proxyResponse.get();
+ }
+ }
+ // TODO Add logging, for now, experimental and we want to not add more noise.
+ throw new ProxyException(ErrorResponse.internalServerError("Failed talking to config servers: "
+ + errorBuilder.toString()));
+ }
+
+ private static class DiscoveryResponseStructure {
+ public List<String> uris = new ArrayList<>();
+ }
+
+ private ProxyResponse createDiscoveryResponse(ProxyRequest proxyRequest) {
+ ObjectMapper mapper = new ObjectMapper();
+ DiscoveryResponseStructure responseStructure = new DiscoveryResponseStructure();
+
+ List<Zone> zones = zoneRegistry.zones();
+ for (Zone zone : zones) {
+ if (!"".equals(proxyRequest.getEnvironment()) &&
+ !proxyRequest.getEnvironment().equals(zone.environment().value())) {
+ continue;
+ }
+ responseStructure.uris.add(proxyRequest.getScheme() + "://" + proxyRequest.getControllerPrefix() +
+ zone.environment().name() + "/" + zone.region().value());
+ }
+ JsonNode node = mapper.valueToTree(responseStructure);
+ return new ProxyResponse(proxyRequest, node.toString(), 200, Optional.empty(), "application/json");
+ }
+
+ private String removeFirstSlashIfAny(String url) {
+ if (url.startsWith("/")) {
+ return url.substring(1);
+ }
+ return url;
+ }
+
+ private Optional<ProxyResponse> proxyCall(URI uri, ProxyRequest proxyRequest, StringBuilder errorBuilder)
+ throws ProxyException {
+ String fullUri = uri.toString() + removeFirstSlashIfAny(proxyRequest.getConfigServerRequest());
+ final HttpRequestBase requestBase = createHttpBaseRequest(
+ proxyRequest.getMethod(), fullUri, proxyRequest.getData());
+ // Empty list of headers to copy for now, add headers when needed, or rewrite logic.
+ copyHeaders(proxyRequest.getHeaders(), requestBase, new HashSet<>());
+
+ RequestConfig config = RequestConfig.custom()
+ .setConnectTimeout((int) PROXY_REQUEST_TIMEOUT.toMillis())
+ .setConnectionRequestTimeout((int) PROXY_REQUEST_TIMEOUT.toMillis())
+ .setSocketTimeout((int) PROXY_REQUEST_TIMEOUT.toMillis()).build();
+ try (
+ CloseableHttpClient client = createHttpClient(config);
+ CloseableHttpResponse response = client.execute(requestBase);
+ ) {
+ if (response.getStatusLine().getStatusCode() / 100 == 5) {
+ errorBuilder.append("Talking to server ").append(uri.getHost());
+ errorBuilder.append(", got ").append(response.getStatusLine().getStatusCode()).append(" ")
+ .append(streamToString(response.getEntity().getContent())).append("\n");
+ return Optional.empty();
+ }
+ final Header contentHeader = response.getLastHeader("Content-Type");
+ final String contentType;
+ if (contentHeader != null && contentHeader.getValue() != null && ! contentHeader.getValue().isEmpty()) {
+ contentType = contentHeader.getValue().replace("; charset=UTF-8","");
+ } else {
+ contentType = "application/json";
+ }
+ return Optional.of(new ProxyResponse(
+ proxyRequest,
+ streamToString(response.getEntity().getContent()),
+ response.getStatusLine().getStatusCode(),
+ Optional.of(uri),
+ contentType));
+
+ // Send response back
+ } catch (IOException|RuntimeException e) {
+ errorBuilder.append("Talking to server ").append(uri.getHost());
+ errorBuilder.append(" got exception ").append(e.getMessage());
+ return Optional.empty();
+ }
+ }
+
+ private HttpRequestBase createHttpBaseRequest(String method, String uri, InputStream data) throws ProxyException {
+ Method enumMethod = Method.valueOf(method);
+ switch (enumMethod) {
+ case GET:
+ return new HttpGet(uri);
+ case POST:
+ HttpPost post = new HttpPost(uri);
+ if (data != null) {
+ post.setEntity(new InputStreamEntity(data));
+ }
+ return post;
+ case PUT:
+ HttpPut put = new HttpPut(uri);
+ if (data != null) {
+ put.setEntity(new InputStreamEntity(data));
+ }
+ return put;
+ case DELETE:
+ return new HttpDelete(uri);
+ case PATCH:
+ HttpPatch patch = new HttpPatch(uri);
+ if (data != null) {
+ patch.setEntity(new InputStreamEntity(data));
+ }
+ return patch;
+ default:
+ throw new ProxyException(ErrorResponse.methodNotAllowed("Will not proxy such calls."));
+ }
+ }
+
+ private void copyHeaders(Map<String, List<String>> headers, HttpRequestBase toRequest, Set<String> headersToCopy) {
+ for (Map.Entry<String, List<String>> headerEntry : headers.entrySet()) {
+ for (String value : headerEntry.getValue()) {
+ if (headersToCopy.contains(value)) {
+ toRequest.addHeader(headerEntry.getKey(), value);
+ }
+ }
+ }
+ }
+
+ public static String streamToString(final InputStream inputStream) throws IOException {
+ final StringBuilder out = new StringBuilder();
+ while (true) {
+ byte[] bytesFromStream = IOUtils.readBytes(inputStream, 1024);
+ if (bytesFromStream.length == 0) {
+ return out.toString();
+ }
+ out.append(new String(bytesFromStream, StandardCharsets.UTF_8));
+ }
+ }
+
+ /**
+ * During upgrade, one server can be down, this is normal. Therefor we do a quick ping on the first server,
+ * if it is not responding, we try the other servers first. False positive/negatives are not critical,
+ * but will increase latency to some extent.
+ */
+ private boolean queueFirstServerIfDown(List<URI> allServers) {
+ if (allServers.size() < 2) {
+ return false;
+ }
+ URI uri = allServers.get(0);
+ HttpGet httpget = new HttpGet(uri);
+
+ int timeout = 500;
+ RequestConfig config = RequestConfig.custom()
+ .setConnectTimeout(timeout)
+ .setConnectionRequestTimeout(timeout)
+ .setSocketTimeout(timeout).build();
+ try (
+ CloseableHttpClient client = createHttpClient(config);
+ CloseableHttpResponse response = client.execute(httpget);
+
+ ) {
+ if (response.getStatusLine().getStatusCode() == 200) {
+ return false;
+ }
+
+ } catch (IOException e) {
+ // We ignore this, if server is restarting this might happen.
+ }
+ // Some error happened, move this server to the back. The other servers should be running.
+ allServers.remove(0);
+ allServers.add(uri);
+ return true;
+ }
+
+ private static CloseableHttpClient createHttpClient(RequestConfig config) {
+ return HttpClientBuilder.create()
+ .setUserAgent("config-server-client")
+ .setDefaultRequestConfig(config)
+ .build();
+ }
+
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ErrorResponse.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ErrorResponse.java
new file mode 100644
index 00000000000..3673c0227a3
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ErrorResponse.java
@@ -0,0 +1,66 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.proxy;
+
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.JsonFormat;
+import com.yahoo.slime.Slime;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import static com.yahoo.jdisc.Response.Status.BAD_REQUEST;
+import static com.yahoo.jdisc.Response.Status.INTERNAL_SERVER_ERROR;
+import static com.yahoo.jdisc.Response.Status.METHOD_NOT_ALLOWED;
+import static com.yahoo.jdisc.Response.Status.NOT_FOUND;
+
+/**
+ * Class for generating error responses.
+ *
+ * @author Haakon Dybdahl
+ */
+public class ErrorResponse extends HttpResponse {
+
+ private final Slime slime = new Slime();
+ public final String message;
+
+ public ErrorResponse(int code, String errorType, String message) {
+ super(code);
+ this.message = message;
+ Cursor root = slime.setObject();
+ root.setString("error-code", errorType);
+ root.setString("message", message);
+ }
+
+ public enum errorCodes {
+ NOT_FOUND,
+ BAD_REQUEST,
+ METHOD_NOT_ALLOWED,
+ INTERNAL_SERVER_ERROR,
+
+ }
+
+ public static ErrorResponse notFoundError(String message) {
+ return new ErrorResponse(NOT_FOUND, errorCodes.NOT_FOUND.name(), message);
+ }
+
+ public static ErrorResponse internalServerError(String message) {
+ return new ErrorResponse(INTERNAL_SERVER_ERROR, errorCodes.INTERNAL_SERVER_ERROR.name(), message);
+ }
+
+ public static ErrorResponse badRequest(String message) {
+ return new ErrorResponse(BAD_REQUEST, errorCodes.BAD_REQUEST.name(), message);
+ }
+
+ public static ErrorResponse methodNotAllowed(String message) {
+ return new ErrorResponse(METHOD_NOT_ALLOWED, errorCodes.METHOD_NOT_ALLOWED.name(), message);
+ }
+
+ @Override
+ public void render(OutputStream stream) throws IOException {
+ new JsonFormat(true).encode(stream, slime);
+ }
+
+ @Override
+ public String getContentType() { return "application/json"; }
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ProxyException.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ProxyException.java
new file mode 100644
index 00000000000..aa828bc0c83
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ProxyException.java
@@ -0,0 +1,16 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.proxy;
+
+/**
+ * Exceptions related to proxying calls to config servers.
+ *
+ * @author Haakon Dybdahl
+ */
+public class ProxyException extends Exception {
+ public final ErrorResponse errorResponse;
+
+ public ProxyException(ErrorResponse errorResponse) {
+ super(errorResponse.message);
+ this.errorResponse = errorResponse;
+ }
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ProxyRequest.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ProxyRequest.java
new file mode 100644
index 00000000000..6854d583222
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ProxyRequest.java
@@ -0,0 +1,119 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.proxy;
+
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.net.HostName;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URLDecoder;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Keeping information about the calls that are being proxied.
+ * A request is of form /zone/v2/[environment]/[region]/[config-server-path]
+ *
+ * @author Haakon Dybdahl
+ */
+public class ProxyRequest {
+
+ private final String environment;
+ private final String region;
+ private final String configServerRequest;
+ private final InputStream requestData;
+ private final Map<String, List<String>> headers;
+ private final String method;
+ private final String controllerPrefix;
+ private final String scheme;
+
+ /**
+ * The constructor calls exception if the request is invalid.
+ *
+ * @param request the request from the jdisc framework.
+ * @param pathPrefix the path prefix of the proxy.
+ * @throws ProxyException on errors
+ */
+ public ProxyRequest(HttpRequest request, String pathPrefix) throws ProxyException, IOException {
+ this(request.getUri(), request.getJDiscRequest().headers(), request.getData(), request.getMethod().name(),
+ pathPrefix);
+ }
+
+ ProxyRequest(URI requestUri, Map<String, List<String>> headers, InputStream body, String method,
+ String pathPrefix) throws ProxyException, IOException {
+ if (requestUri == null) {
+ throw new ProxyException(ErrorResponse.badRequest("Request not set."));
+ }
+ final String path = URLDecoder.decode(requestUri.getPath(),"UTF-8");
+ if (! path.startsWith(pathPrefix)) {
+ // This has to be caused by wrong mapping of path in services.xml.
+ throw new ProxyException(ErrorResponse.notFoundError("Request not starting with " + pathPrefix));
+ }
+ final String uriNoPrefix = path.replaceFirst(pathPrefix, "")
+ + (requestUri.getRawQuery() == null ? "" : "?" + requestUri.getRawQuery());
+
+ final String[] parts = uriNoPrefix.split("/");
+
+ this.environment = parts.length > 0 ? parts[0] : "";
+ this.region = parts.length > 1 ? parts[1] : "";
+ this.configServerRequest = parts.length > 2 ? uriNoPrefix.replace(environment + "/" + region, "") : "";
+ this.requestData = body;
+ this.headers = headers;
+ this.method = method;
+
+ String hostPort = headers.containsKey("host")
+ ? headers.get("host").get(0)
+ : HostName.getLocalhost() + ":" + requestUri.getPort();
+ StringBuilder prefix = new StringBuilder(hostPort + pathPrefix);
+ if (! environment.isEmpty()) {
+ prefix.append(environment).append("/").append(region);
+ }
+
+ this.controllerPrefix = prefix.toString();
+ this.scheme = requestUri.getScheme();
+ }
+
+ /**
+ * A discovery query lists environments and regions.
+ */
+ public boolean isDiscoveryRequest() {
+ return region.isEmpty();
+ }
+
+ public String getRegion() {
+ return region;
+ }
+
+ public String getEnvironment() {
+ return environment;
+ }
+
+ public String getConfigServerRequest() {
+ return configServerRequest;
+ }
+
+ public InputStream getData() {
+ return requestData;
+ }
+
+ @Override
+ public String toString() {
+ return "[ region: " + region + " env: " + environment + " request: " + configServerRequest + "]";
+ }
+
+ public Map<String, List<String>> getHeaders() {
+ return headers;
+ }
+
+ public String getMethod() {
+ return method;
+ }
+
+ public String getControllerPrefix() {
+ return controllerPrefix;
+ }
+
+ public String getScheme() { return scheme; }
+
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ProxyResponse.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ProxyResponse.java
new file mode 100644
index 00000000000..3f878740ff0
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ProxyResponse.java
@@ -0,0 +1,64 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.proxy;
+
+import com.yahoo.container.jdisc.HttpResponse;
+import org.apache.http.client.utils.URIBuilder;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
+import java.util.Optional;
+
+/**
+ * Response class that also rewrites URL from config server.
+ *
+ * @author Haakon Dybdahl
+ */
+public class ProxyResponse extends HttpResponse {
+
+ private final String bodyResponseRewritten;
+ private final String contentType;
+
+ public ProxyResponse(
+ ProxyRequest controllerRequest,
+ String bodyResponse,
+ int statusResponse,
+ Optional<URI> configServer,
+ String contentType) {
+ super(statusResponse);
+ this.contentType = contentType;
+
+ if (! configServer.isPresent() || controllerRequest.getControllerPrefix().isEmpty()) {
+ bodyResponseRewritten = bodyResponse;
+ return;
+ }
+
+ final String configServerPrefix;
+ final String controllerRequestPrefix;
+ try {
+ configServerPrefix = new URIBuilder()
+ .setScheme(configServer.get().getScheme())
+ .setHost(configServer.get().getHost())
+ .setPort(configServer.get().getPort())
+ .build().toString();
+ controllerRequestPrefix = new URIBuilder()
+ .setScheme(controllerRequest.getScheme())
+ // controller prefix is more than host, so it is a bit hackish, but verified by tests.
+ .setHost(controllerRequest.getControllerPrefix())
+ .build().toString();
+ } catch (URISyntaxException e) {
+ throw new RuntimeException(e);
+ }
+ bodyResponseRewritten = bodyResponse.replace(configServerPrefix, controllerRequestPrefix);
+ }
+
+ @Override
+ public void render(OutputStream stream) throws IOException {
+ stream.write(bodyResponseRewritten.getBytes(StandardCharsets.UTF_8));
+ }
+
+ @Override
+ public String getContentType() { return contentType; }
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/package-info.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/package-info.java
new file mode 100644
index 00000000000..f6c300268a2
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/package-info.java
@@ -0,0 +1,8 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.vespa.hosted.controller.proxy;
+
+/**
+ * @author Haakon Dybdahl
+ */
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/Path.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/Path.java
index c8c027d91c9..e9db4f9b717 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/Path.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/Path.java
@@ -76,8 +76,8 @@ public class Path {
StringBuilder rest = new StringBuilder();
for (int i = specElements.length; i < this.elements.length; i++)
rest.append(elements[i]).append("/");
- if ( ! pathString.endsWith("/"))
- rest.setLength(rest.length() -1);
+ if ( ! pathString.endsWith("/") && rest.length() > 0)
+ rest.setLength(rest.length() - 1);
this.rest = rest.toString();
}
@@ -98,9 +98,6 @@ public class Path {
*/
public String getRest() { return rest; }
- /** Returns this path as a string */
- public String asString() { return pathString; }
-
@Override
public String toString() {
return "path '" + Arrays.stream(elements).collect(Collectors.joining("/")) + "'";
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 a259e221a1e..d7324450d4c 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
@@ -2,6 +2,7 @@
package com.yahoo.vespa.hosted.controller.restapi.application;
import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableSet;
import com.google.inject.Inject;
import com.yahoo.component.Version;
import com.yahoo.config.provision.ApplicationId;
@@ -50,9 +51,9 @@ import com.yahoo.vespa.hosted.controller.api.identifiers.ScrewdriverId;
import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
import com.yahoo.vespa.hosted.controller.api.identifiers.UserGroup;
import com.yahoo.vespa.hosted.controller.api.identifiers.UserId;
-import com.yahoo.vespa.hosted.controller.api.integration.MetricsService;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServerException;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.Log;
+import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
import com.yahoo.vespa.hosted.controller.api.integration.organization.User;
import com.yahoo.vespa.hosted.controller.api.integration.routing.RotationStatus;
import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
@@ -62,7 +63,6 @@ import com.yahoo.vespa.hosted.controller.application.ClusterCost;
import com.yahoo.vespa.hosted.controller.application.ClusterUtilization;
import com.yahoo.vespa.hosted.controller.application.Deployment;
import com.yahoo.vespa.hosted.controller.application.DeploymentCost;
-import com.yahoo.vespa.hosted.controller.application.DeploymentJobs;
import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics;
import com.yahoo.vespa.hosted.controller.application.JobStatus;
import com.yahoo.vespa.hosted.controller.application.SourceRevision;
@@ -169,7 +169,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
if (path.matches("/application/v4/cookiefreshness")) return cookieFreshness(request);
if (path.matches("/application/v4/tenant/{tenant}")) return tenant(path.get("tenant"), request);
if (path.matches("/application/v4/tenant/{tenant}/application")) return applications(path.get("tenant"), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}")) return application(path.get("tenant"), path.get("application"), path, request);
+ if (path.matches("/application/v4/tenant/{tenant}/application/{application}")) return application(path.get("tenant"), path.get("application"), request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}")) return deployment(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/converge")) return waitForConvergence(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/service")) return services(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request);
@@ -222,10 +222,19 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
response.headers().put("Allow", "GET,PUT,POST,DELETE,OPTIONS");
return response;
}
-
+
+ private HttpResponse recursiveRoot(HttpRequest request) {
+ Slime slime = new Slime();
+ Cursor tenantArray = slime.setArray();
+ for (Tenant tenant : controller.tenants().asList())
+ toSlime(tenantArray.addObject(), tenant, request, true);
+ return new SlimeJsonResponse(slime);
+ }
+
private HttpResponse root(HttpRequest request) {
- return new ResourceResponse(request, "user", "tenant", "tenant-pipeline", "athensDomain",
- "property", "cookiefreshness");
+ return recurseOverTenants(request)
+ ? recursiveRoot(request)
+ : new ResourceResponse(request, "user", "tenant", "tenant-pipeline", "athensDomain", "property", "cookiefreshness");
}
private HttpResponse authenticatedUser(HttpRequest request) {
@@ -310,20 +319,9 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
}
private HttpResponse tenant(Tenant tenant, HttpRequest request, boolean listApplications) {
- Slime tenantSlime = toSlime(tenant, request, listApplications);
- tenant.getPropertyId().ifPresent(propertyId -> {
- try {
- toSlime(tenantSlime.get(),
- controller.organization().propertyUri(propertyId),
- controller.organization().contactsUri(propertyId),
- controller.organization().issueCreationUri(propertyId),
- controller.organization().contactsFor(propertyId));
- }
- catch (RuntimeException e) {
- log.log(Level.WARNING, "Error fetching property info for " + tenant + " with propertyId " + propertyId, e);
- }
- });
- return new SlimeJsonResponse(tenantSlime);
+ Slime slime = new Slime();
+ toSlime(slime.setObject(), tenant, request, listApplications);
+ return new SlimeJsonResponse(slime);
}
private HttpResponse applications(String tenantName, HttpRequest request) {
@@ -335,18 +333,23 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
return new SlimeJsonResponse(slime);
}
- private HttpResponse application(String tenantName, String applicationName, Path path, HttpRequest request) {
- Slime slime = new Slime();
- Cursor response = slime.setObject();
-
- com.yahoo.config.provision.ApplicationId applicationId = com.yahoo.config.provision.ApplicationId.from(tenantName, applicationName, "default");
+ private HttpResponse application(String tenantName, String applicationName, HttpRequest request) {
+ ApplicationId applicationId = ApplicationId.from(tenantName, applicationName, "default");
Application application =
controller.applications().get(applicationId)
.orElseThrow(() -> new NotExistsException(applicationId + " not found"));
-
+
+ Slime slime = new Slime();
+ toSlime(slime.setObject(), application, request);
+ return new SlimeJsonResponse(slime);
+ }
+
+ private void toSlime(Cursor object, Application application, HttpRequest request) {
+ object.setString("application", application.id().application().value());
+ object.setString("instance", application.id().instance().value());
// Currently deploying change
if (application.deploying().isPresent()) {
- Cursor deployingObject = response.setObject("deploying");
+ Cursor deployingObject = object.setObject("deploying");
if (application.deploying().get() instanceof Change.VersionChange)
deployingObject.setString("version", ((Change.VersionChange)application.deploying().get()).version().toString());
else if (((Change.ApplicationChange)application.deploying().get()).revision().isPresent())
@@ -354,14 +357,14 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
}
// Jobs sorted according to deployment spec
- Map<DeploymentJobs.JobType, JobStatus> jobStatus = controller.applications().deploymentTrigger()
+ List<JobStatus> jobStatus = controller.applications().deploymentTrigger()
.deploymentOrder()
- .sortBy(application.deploymentSpec(), application.deploymentJobs().jobStatus());
+ .sortBy(application.deploymentSpec(), application.deploymentJobs().jobStatus().values());
- Cursor deploymentsArray = response.setArray("deploymentJobs");
- for (JobStatus job : jobStatus.values()) {
- Cursor jobObject = deploymentsArray.addObject();
- jobObject.setString("type", job.type().id());
+ Cursor deploymentsArray = object.setArray("deploymentJobs");
+ for (JobStatus job : jobStatus) {
+ Cursor jobObject = deploymentsArray.addObject();
+ jobObject.setString("type", job.type().jobName());
jobObject.setBool("success", job.isSuccess());
job.lastTriggered().ifPresent(jobRun -> toSlime(jobRun, jobObject.setObject("lastTriggered")));
@@ -371,47 +374,47 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
}
// Compile version. The version that should be used when building an application
- response.setString("compileVersion", application.compileVersion(controller).toFullString());
+ object.setString("compileVersion", application.oldestDeployedVersion().orElse(controller.systemVersion()).toFullString());
// Rotations
- Cursor globalRotationsArray = response.setArray("globalRotations");
- Set<URI> rotations = controller.getRotationUris(applicationId);
+ Cursor globalRotationsArray = object.setArray("globalRotations");
+ Set<URI> rotations = controller.getRotationUris(application.id());
Map<String, RotationStatus> rotationHealthStatus =
rotations.isEmpty() ? Collections.emptyMap() : controller.getHealthStatus(rotations.iterator().next().getHost());
for (URI rotation : rotations)
globalRotationsArray.addString(rotation.toString());
// Deployments sorted according to deployment spec
- Map<Zone, Deployment> deployments = controller.applications().deploymentTrigger()
+ List<Deployment> deployments = controller.applications().deploymentTrigger()
.deploymentOrder()
- .sortBy(application.deploymentSpec().zones(), application.deployments());
- Cursor instancesArray = response.setArray("instances");
- for (Deployment deployment : deployments.values()) {
+ .sortBy(application.deploymentSpec().zones(), application.deployments().values());
+ Cursor instancesArray = object.setArray("instances");
+ for (Deployment deployment : deployments) {
Cursor deploymentObject = instancesArray.addObject();
+
deploymentObject.setString("environment", deployment.zone().environment().value());
deploymentObject.setString("region", deployment.zone().region().value());
deploymentObject.setString("instance", application.id().instance().value()); // pointless
if ( ! rotations.isEmpty())
setRotationStatus(deployment, rotationHealthStatus, deploymentObject);
- deploymentObject.setString("url", withPath(path.asString() +
- "/environment/" + deployment.zone().environment().value() +
- "/region/" + deployment.zone().region().value() +
- "/instance/" + application.id().instance().value(),
- request.getUri()).toString());
+
+ if (recurseOverDeployments(request)) // List full deployment information when recursive.
+ toSlime(deploymentObject, new DeploymentId(application.id(), deployment.zone()), deployment, request);
+ else
+ deploymentObject.setString("url", withPath(request.getUri().getPath() +
+ "/environment/" + deployment.zone().environment().value() +
+ "/region/" + deployment.zone().region().value() +
+ "/instance/" + application.id().instance().value(),
+ request.getUri()).toString());
}
-
+
// Metrics
- try {
- MetricsService.ApplicationMetrics metrics = controller.metricsService().getApplicationMetrics(applicationId);
- Cursor metricsObject = response.setObject("metrics");
- metricsObject.setDouble("queryServiceQuality", metrics.queryServiceQuality());
- metricsObject.setDouble("writeServiceQuality", metrics.writeServiceQuality());
- }
- catch (RuntimeException e) {
- log.log(Level.WARNING, "Failed getting Yamas metrics", Exceptions.toMessageString(e));
- }
+ Cursor metricsObject = object.setObject("metrics");
+ metricsObject.setDouble("queryServiceQuality", application.metrics().queryServiceQuality());
+ metricsObject.setDouble("writeServiceQuality", application.metrics().writeServiceQuality());
- return new SlimeJsonResponse(slime);
+ application.ownershipIssueId().ifPresent(issueId -> object.setString("ownershipIssueId", issueId.value()));
+ application.deploymentJobs().issueId().ifPresent(issueId -> object.setString("deploymentIssueId", issueId.value()));
}
private HttpResponse deployment(String tenantName, String applicationName, String instanceName, String environment, String region, HttpRequest request) {
@@ -426,21 +429,23 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
if (deployment == null)
throw new NotExistsException(application + " is not deployed in " + deploymentId.zone());
- Optional<InstanceEndpoints> deploymentEndpoints = controller.applications().getDeploymentEndpoints(deploymentId);
-
Slime slime = new Slime();
- Cursor response = slime.setObject();
+ toSlime(slime.setObject(), deploymentId, deployment, request);
+ return new SlimeJsonResponse(slime);
+ }
+
+ private void toSlime(Cursor response, DeploymentId deploymentId, Deployment deployment, HttpRequest request) {
+
+ Optional<InstanceEndpoints> deploymentEndpoints = controller.applications().getDeploymentEndpoints(deploymentId);
Cursor serviceUrlArray = response.setArray("serviceUrls");
if (deploymentEndpoints.isPresent()) {
for (URI uri : deploymentEndpoints.get().getContainerEndpoints())
serviceUrlArray.addString(uri.toString());
}
- response.setString("nodes", withPath("/zone/v2/" + environment + "/" + region + "/nodes/v2/node/?&recursive=true&application=" + tenantName + "." + applicationName + "." + instanceName, request.getUri()).toString());
+ response.setString("nodes", withPath("/zone/v2/" + deploymentId.zone().environment() + "/" + deploymentId.zone().region() + "/nodes/v2/node/?&recursive=true&application=" + deploymentId.applicationId().tenant() + "." + deploymentId.applicationId().application() + "." + deploymentId.applicationId().instance(), request.getUri()).toString());
- Environment env = Environment.from(environment);
- RegionName regionName = RegionName.from(region);
- URI elkUrl = controller.getElkUri(env, regionName, deploymentId);
+ URI elkUrl = controller.getElkUri(deploymentId);
if (elkUrl != null)
response.setString("elkUrl", elkUrl.toString());
@@ -448,10 +453,11 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
response.setString("version", deployment.version().toFullString());
response.setString("revision", deployment.revision().id());
response.setLong("deployTimeEpochMs", deployment.at().toEpochMilli());
- Optional<Duration> deploymentTimeToLive = controller.zoneRegistry().getDeploymentTimeToLive(Environment.from(environment), RegionName.from(region));
+ Optional<Duration> deploymentTimeToLive = controller.zoneRegistry().getDeploymentTimeToLive(deploymentId.zone().environment(), deploymentId.zone().region());
deploymentTimeToLive.ifPresent(duration -> response.setLong("expiryTimeEpochMs", deployment.at().plus(duration).toEpochMilli()));
- application.deploymentJobs().projectId().ifPresent(i -> response.setString("screwdriverId", String.valueOf(i)));
+ controller.applications().get(deploymentId.applicationId()).flatMap(application -> application.deploymentJobs().projectId())
+ .ifPresent(i -> response.setString("screwdriverId", String.valueOf(i)));
sourceRevisionToSlime(deployment.revision().source(), response);
// Cost
@@ -467,8 +473,6 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
metricsObject.setDouble("documentCount", metrics.documentCount());
metricsObject.setDouble("queryLatencyMillis", metrics.queryLatencyMillis());
metricsObject.setDouble("writeLatencyMillis", metrics.writeLatencyMillis());
-
- return new SlimeJsonResponse(slime);
}
private void toSlime(ApplicationRevision revision, Cursor object) {
@@ -575,7 +579,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
private HttpResponse services(String tenantName, String applicationName, String instanceName, String environment, String region, HttpRequest request) {
ApplicationView applicationView = controller.getApplicationView(tenantName, applicationName, instanceName, environment, region);
ServiceApiResponse response = new ServiceApiResponse(new Zone(Environment.from(environment), RegionName.from(region)),
- new com.yahoo.config.provision.ApplicationId.Builder().tenant(tenantName).applicationName(applicationName).instanceName(instanceName).build(),
+ new ApplicationId.Builder().tenant(tenantName).applicationName(applicationName).instanceName(instanceName).build(),
controller.getConfigServerUris(Environment.from(environment), RegionName.from(region)),
request.getUri());
response.setResponse(applicationView);
@@ -585,7 +589,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
private HttpResponse service(String tenantName, String applicationName, String instanceName, String environment, String region, String serviceName, String restPath, HttpRequest request) {
Map<?,?> result = controller.getServiceApiResponse(tenantName, applicationName, instanceName, environment, region, serviceName, restPath);
ServiceApiResponse response = new ServiceApiResponse(new Zone(Environment.from(environment), RegionName.from(region)),
- new com.yahoo.config.provision.ApplicationId.Builder().tenant(tenantName).applicationName(applicationName).instanceName(instanceName).build(),
+ new ApplicationId.Builder().tenant(tenantName).applicationName(applicationName).instanceName(instanceName).build(),
controller.getConfigServerUris(Environment.from(environment), RegionName.from(region)),
request.getUri());
response.setResponse(result, serviceName, restPath);
@@ -619,8 +623,8 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
}
case OPSDB: {
UserGroup userGroup = new UserGroup(mandatory("userGroup", requestData).asString());
- updatedTenant = Tenant.createOpsDbTenant(new TenantId(tenantName),
- userGroup,
+ updatedTenant = Tenant.createOpsDbTenant(new TenantId(tenantName),
+ userGroup,
new Property(mandatory("property", requestData).asString()),
optional("propertyId", requestData).map(PropertyId::new));
throwIfNotSuperUserOrPartOfOpsDbGroup(userGroup, request);
@@ -630,7 +634,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
case ATHENS: {
if (requestData.field("userGroup").valid())
throw new BadRequestException("Cannot set OpsDB user group to Athens tenant");
- updatedTenant = Tenant.createAthensTenant(new TenantId(tenantName),
+ updatedTenant = Tenant.createAthensTenant(new TenantId(tenantName),
new AthenzDomain(mandatory("athensDomain", requestData).asString()),
new Property(mandatory("property", requestData).asString()),
optional("propertyId", requestData).map(PropertyId::new));
@@ -659,7 +663,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
throwIfNotSuperUserOrPartOfOpsDbGroup(new UserGroup(mandatory("userGroup", requestData).asString()), request);
if (tenant.isAthensTenant())
throwIfNotAthenzDomainAdmin(new AthenzDomain(mandatory("athensDomain", requestData).asString()), request);
-
+
controller.tenants().addTenant(tenant, authorizer.getNToken(request));
return tenant(tenant, request, true);
}
@@ -675,7 +679,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
throwIfNotAthenzDomainAdmin(tenantDomain, request);
NToken nToken = authorizer.getNToken(request)
.orElseThrow(() ->
- new BadRequestException("The NToken for a domain admin is required to migrate tenant to Athens"));
+ new BadRequestException("The NToken for a domain admin is required to migrate tenant to Athens"));
Tenant tenant = controller.tenants().migrateTenantToAthenz(tenantid, tenantDomain, propertyId, property, nToken);
return tenant(tenant, request, true);
}
@@ -684,7 +688,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
authorizer.throwIfUnauthorized(new TenantId(tenantName), request);
Application application;
try {
- application = controller.applications().createApplication(com.yahoo.config.provision.ApplicationId.from(tenantName, applicationName, "default"), authorizer.getNToken(request));
+ application = controller.applications().createApplication(ApplicationId.from(tenantName, applicationName, "default"), authorizer.getNToken(request));
}
catch (ZmsException e) { // TODO: Push conversion down
if (e.getCode() == com.yahoo.jdisc.Response.Status.FORBIDDEN)
@@ -700,36 +704,35 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
/** Trigger deployment of the last built application package, on a given version */
private HttpResponse deploy(String tenantName, String applicationName, HttpRequest request) {
+ Version version = decideDeployVersion(request);
+ if ( ! systemHasVersion(version))
+ throw new IllegalArgumentException("Cannot trigger deployment of version '" + version + "': " +
+ "Version is not active in this system. " +
+ "Active versions: " + controller.versionStatus().versions());
+
ApplicationId id = ApplicationId.from(tenantName, applicationName, "default");
- try (Lock lock = controller.applications().lock(id)) {
- Application application = controller.applications().require(id);
+ controller.applications().lockedOrThrow(id, application -> {
if (application.deploying().isPresent())
throw new IllegalArgumentException("Can not start a deployment of " + application + " at this time: " +
- application.deploying().get() + " is in progress");
-
- Version version = decideDeployVersion(request);
- if ( ! systemHasVersion(version))
- throw new IllegalArgumentException("Cannot trigger deployment of version '" + version + "': " +
- "Version is not active in this system. " +
- "Active versions: " + controller.versionStatus().versions());
+ application.deploying().get() + " is in progress");
controller.applications().deploymentTrigger().triggerChange(application.id(), new Change.VersionChange(version));
- return new MessageResponse("Triggered deployment of " + application + " on version " + version);
- }
+ });
+ return new MessageResponse("Triggered deployment of application '" + id + "' on version " + version);
}
/** Cancel any ongoing change for given application */
private HttpResponse cancelDeploy(String tenantName, String applicationName) {
ApplicationId id = ApplicationId.from(tenantName, applicationName, "default");
- try (Lock lock = controller.applications().lock(id)) {
- Application application = controller.applications().require(id);
- Optional<Change> change = application.deploying();
- if (!change.isPresent()) {
- return new MessageResponse("No deployment in progress for " + application + " at this time");
- }
- controller.applications().deploymentTrigger().cancelChange(id);
- return new MessageResponse("Cancelled " + change.get() + " for " + application);
- }
+ Application application = controller.applications().require(id);
+ Optional<Change> change = application.deploying();
+ if ( ! change.isPresent())
+ return new MessageResponse("No deployment in progress for " + application + " at this time");
+
+ controller.applications().lockedOrThrow(id, lockedApplication ->
+ controller.applications().deploymentTrigger().cancelChange(id));
+
+ return new MessageResponse("Cancelled " + change.get() + " for " + application);
}
/** Schedule restart of deployment, or specific host in a deployment */
@@ -789,10 +792,12 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
DeployOptions deployOptionsJsonClass = new DeployOptions(screwdriverBuildJobFromSlime(deployOptions.field("screwdriverBuildJob")),
optional("vespaVersion", deployOptions).map(Version::new),
deployOptions.field("ignoreValidationErrors").asBool(),
- deployOptions.field("deployCurrentVersion").asBool());
- ActivateResult result = controller.applications().deployApplication(applicationId,
- zone,
- new ApplicationPackage(dataParts.get("applicationZip")),
+ deployOptions.field("deployCurrentVersion").asBool());
+ ApplicationPackage applicationPackage = new ApplicationPackage(dataParts.get("applicationZip"));
+ controller.applications().validate(applicationPackage.deploymentSpec());
+ ActivateResult result = controller.applications().deployApplication(applicationId,
+ zone,
+ applicationPackage,
deployOptionsJsonClass);
return new SlimeJsonResponse(toSlime(result, dataParts.get("applicationZip").length));
}
@@ -811,10 +816,8 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
private HttpResponse deleteApplication(String tenantName, String applicationName, HttpRequest request) {
authorizer.throwIfUnauthorized(new TenantId(tenantName), request);
- com.yahoo.config.provision.ApplicationId id = com.yahoo.config.provision.ApplicationId.from(tenantName, applicationName, "default");
- Application deleted = controller.applications().deleteApplication(id, authorizer.getNToken(request));
- if (deleted == null)
- return ErrorResponse.notFoundError("Could not delete application '" + id + "': Application not found");
+ ApplicationId id = ApplicationId.from(tenantName, applicationName, "default");
+ controller.applications().deleteApplication(id, authorizer.getNToken(request));
return new EmptyJsonResponse(); // TODO: Replicates current behavior but should return a message response instead
}
@@ -874,7 +877,8 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
return authorizer.getPrincipalIfAny(request).map(Principal::getName);
}
- private void toSlime(Tenant tenant, Cursor object, HttpRequest request, boolean listApplications) {
+ private void toSlime(Cursor object, Tenant tenant, HttpRequest request, boolean listApplications) {
+ object.setString("tenant", tenant.getId().id());
object.setString("type", tenant.tenantType().name());
tenant.getAthensDomain().ifPresent(a -> object.setString("athensDomain", a.id()));
tenant.getProperty().ifPresent(p -> object.setString("property", p.id()));
@@ -883,10 +887,31 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
Cursor applicationArray = object.setArray("applications");
if (listApplications) { // This cludge is needed because we call this after deleting the tenant. As this call makes another tenant lookup it will fail. TODO is to support lookup on tenant
for (Application application : controller.applications().asList(TenantName.from(tenant.getId().id()))) {
- if (application.id().instance().isDefault()) // TODO: Skip non-default applications until supported properly
- toSlime(application, applicationArray.addObject(), request);
+ if (application.id().instance().isDefault()) {// TODO: Skip non-default applications until supported properly
+ if (recurseOverApplications(request))
+ toSlime(applicationArray.addObject(), application, request);
+ else
+ toSlime(application, applicationArray.addObject(), request);
+ }
}
}
+ tenant.getPropertyId().ifPresent(propertyId -> {
+ try {
+ object.setString("propertyUrl", controller.organization().propertyUri(propertyId).toString());
+ object.setString("contactsUrl", controller.organization().contactsUri(propertyId).toString());
+ object.setString("issueCreationUrl", controller.organization().issueCreationUri(propertyId).toString());
+ Cursor lists = object.setArray("contacts");
+ for (List<? extends User> contactList : controller.organization().contactsFor(propertyId)) {
+ Cursor list = lists.addArray();
+ for (User contact : contactList)
+ list.addString(contact.displayName());
+ }
+ }
+ catch (RuntimeException e) {
+ log.log(Level.WARNING, "Error fetching property info for " + tenant + " with propertyId " + propertyId + ": " +
+ Exceptions.toMessageString(e));
+ }
+ });
}
// A tenant has different content when in a list ... antipattern, but not solvable before application/v5
@@ -989,24 +1014,6 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
return Joiner.on("/").join(elements);
}
- private Slime toSlime(Tenant tenant, HttpRequest request, boolean listApplications) {
- Slime slime = new Slime();
- toSlime(tenant, slime.setObject(), request, listApplications);
- return slime;
- }
-
- private void toSlime(Cursor root, URI propertyUri, URI contactsUri, URI issueCreationUri, List<? extends List<? extends User>> contacts) {
- root.setString("propertyUrl", propertyUri.toString());
- root.setString("contactsUrl", contactsUri.toString());
- root.setString("issueCreationUrl", issueCreationUri.toString());
- Cursor lists = root.setArray("contacts");
- for (List<? extends User> contactList : contacts) {
- Cursor list = lists.addArray();
- for (User contact : contactList)
- list.addString(contact.displayName());
- }
- }
-
private void toSlime(Application application, Cursor object, HttpRequest request) {
object.setString("application", application.id().application().value());
object.setString("instance", application.id().instance().value());
@@ -1152,4 +1159,17 @@ public class ApplicationApiHandler extends LoggingRequestHandler {
return name;
}
+
+ private static boolean recurseOverTenants(HttpRequest request) {
+ return recurseOverApplications(request) || "tenant".equals(request.getProperty("recursive"));
+ }
+
+ private static boolean recurseOverApplications(HttpRequest request) {
+ return recurseOverDeployments(request) || "application".equals(request.getProperty("recursive"));
+ }
+
+ private static boolean recurseOverDeployments(HttpRequest request) {
+ return ImmutableSet.of("all", "true", "deployment").contains(request.getProperty("recursive"));
+ }
+
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiHandler.java
index 8a5f1e4639a..27b219cd892 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiHandler.java
@@ -1,6 +1,7 @@
// Copyright 2017 Yahoo Holdings. 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.component.Version;
import com.yahoo.config.application.api.DeploymentSpec;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.container.jdisc.HttpRequest;
@@ -11,6 +12,8 @@ import com.yahoo.slime.Cursor;
import com.yahoo.slime.Slime;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.Controller;
+import com.yahoo.vespa.hosted.controller.application.JobList;
+import com.yahoo.vespa.hosted.controller.application.JobStatus;
import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
import com.yahoo.vespa.hosted.controller.restapi.ErrorResponse;
import com.yahoo.vespa.hosted.controller.restapi.SlimeJsonResponse;
@@ -19,17 +22,20 @@ import com.yahoo.vespa.hosted.controller.restapi.application.EmptyJsonResponse;
import com.yahoo.vespa.hosted.controller.restapi.Path;
import com.yahoo.yolean.Exceptions;
-import java.time.Instant;
import java.util.Optional;
import java.util.concurrent.Executor;
import java.util.logging.Level;
+import static com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobError.outOfCapacity;
+import static java.util.Comparator.comparing;
+
/**
* This implements the deployment/v1 API which provides information about the status of Vespa platform and
* application deployments.
- *
+ *
* @author bratseth
*/
+@SuppressWarnings("unused") // Injected
public class DeploymentApiHandler extends LoggingRequestHandler {
private final Controller controller;
@@ -56,7 +62,7 @@ public class DeploymentApiHandler extends LoggingRequestHandler {
return ErrorResponse.internalServerError(Exceptions.toMessageString(e));
}
}
-
+
private HttpResponse handleGET(HttpRequest request) {
Path path = new Path(request.getUri().getPath());
if (path.matches("/deployment/v1/")) return root(request);
@@ -70,7 +76,7 @@ public class DeploymentApiHandler extends LoggingRequestHandler {
response.headers().put("Allow", "GET,OPTIONS");
return response;
}
-
+
private HttpResponse root(HttpRequest request) {
Slime slime = new Slime();
Cursor root = slime.setObject();
@@ -83,7 +89,7 @@ public class DeploymentApiHandler extends LoggingRequestHandler {
versionObject.setLong("date", version.releasedAt().toEpochMilli());
versionObject.setBool("controllerVersion", version.isSelfVersion());
versionObject.setBool("systemVersion", version.isCurrentSystemVersion());
-
+
Cursor configServerArray = versionObject.setArray("configServers");
for (String configServerHostnames : version.configServerHostnames()) {
Cursor configServerObject = configServerArray.addObject();
@@ -92,29 +98,42 @@ public class DeploymentApiHandler extends LoggingRequestHandler {
Cursor failingArray = versionObject.setArray("failingApplications");
for (ApplicationId id : version.statistics().failing()) {
- Optional<Application> application = controller.applications().get(id);
- if ( ! application.isPresent()) continue; // deleted just now
-
- Instant failingSince = application.get().deploymentJobs().failingSince();
- if (failingSince == null) continue; // started working just now
-
- Cursor applicationObject = failingArray.addObject();
- toSlime(application.get(), applicationObject, request);
- applicationObject.setLong("failingSince", failingSince.toEpochMilli());
-
+ controller.applications().get(id).ifPresent(application -> {
+ firstFailingOn(version.versionNumber(), application).ifPresent(firstFailing -> {
+ Cursor applicationObject = failingArray.addObject();
+ toSlime(applicationObject, application, request);
+ applicationObject.setString("failing", firstFailing.type().jobName());
+ });
+ });
}
Cursor productionArray = versionObject.setArray("productionApplications");
for (ApplicationId id : version.statistics().production()) {
- Optional<Application> application = controller.applications().get(id);
- if ( ! application.isPresent()) continue; // deleted just now
- toSlime(application.get(), productionArray.addObject(), request);
+ controller.applications().get(id).ifPresent(application -> {
+ int successes = productionSuccessesFor(version.versionNumber(), application);
+ if (successes == 0) return; // Just upgraded to a newer version.
+ Cursor applicationObject = productionArray.addObject();
+ toSlime(applicationObject, application, request);
+ applicationObject.setLong("productionJobs", productionJobsFor(application));
+ applicationObject.setLong("productionSuccesses", productionSuccessesFor(version.versionNumber(), application));
+ });
+ }
+
+ Cursor runningArray = versionObject.setArray("deployingApplications");
+ for (ApplicationId id : version.statistics().deploying()) {
+ controller.applications().get(id).ifPresent(application -> {
+ lastDeployingTo(version.versionNumber(), application).ifPresent(lastDeploying -> {
+ Cursor applicationObject = runningArray.addObject();
+ toSlime(applicationObject, application, request);
+ applicationObject.setString("running", lastDeploying.type().jobName());
+ });
+ });
}
}
return new SlimeJsonResponse(slime);
}
- private void toSlime(Application application, Cursor object, HttpRequest request) {
+ private void toSlime(Cursor object, Application application, HttpRequest request) {
object.setString("tenant", application.id().tenant().value());
object.setString("application", application.id().application().value());
object.setString("instance", application.id().instance().value());
@@ -132,4 +151,40 @@ public class DeploymentApiHandler extends LoggingRequestHandler {
return upgradePolicy.name();
}
+ // ----------------------------- Utilities to pick out the relevant JobStatus -- filter chains should mirror the ones in VersionStatus
+
+ /** The first upgrade job to fail on this version, for this application */
+ private Optional<JobStatus> firstFailingOn(Version version, Application application) {
+ return JobList.from(application)
+ .failing()
+ .not().failingApplicationChange()
+ .not().failingBecause(outOfCapacity)
+ .lastCompleted().on(version)
+ .asList().stream()
+ .min(comparing(job -> job.lastCompleted().get().at()));
+ }
+
+ /** The number of production jobs for this application */
+ private int productionJobsFor(Application application) {
+ return JobList.from(application)
+ .production()
+ .size();
+ }
+
+ /** The number of production jobs with last success on the given version, for this application */
+ private int productionSuccessesFor(Version version, Application application) {
+ return JobList.from(application)
+ .production()
+ .lastSuccess().on(version)
+ .size();
+ }
+
+ /** The last triggered upgrade to this version, for this application */
+ private Optional<JobStatus> lastDeployingTo(Version version, Application application) {
+ return JobList.from(application)
+ .upgrading()
+ .asList().stream()
+ .max(comparing(job -> job.lastTriggered().get().at()));
+ }
+
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AccessControlHeaders.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AccessControlHeaders.java
index aea59c16cd5..8a539720a21 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AccessControlHeaders.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AccessControlHeaders.java
@@ -3,16 +3,15 @@ package com.yahoo.vespa.hosted.controller.restapi.filter;
import com.google.common.collect.ImmutableMap;
+import java.time.Duration;
import java.util.Map;
-import static java.util.concurrent.TimeUnit.DAYS;
-
/**
* @author gv
*/
public interface AccessControlHeaders {
- String CORS_PREFLIGHT_REQUEST_CACHE_TTL = Long.toString(DAYS.toSeconds(7));
+ String CORS_PREFLIGHT_REQUEST_CACHE_TTL = Long.toString(Duration.ofDays(7).getSeconds());
String ALLOW_ORIGIN_HEADER = "Access-Control-Allow-Origin";
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/securitycontext/CreateSecurityContextFilter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/securitycontext/CreateSecurityContextFilter.java
index 850130ca970..6073307bafa 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/securitycontext/CreateSecurityContextFilter.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/securitycontext/CreateSecurityContextFilter.java
@@ -19,6 +19,7 @@ import java.security.Principal;
*/
@After("BouncerFilter")
@Provides("SecurityContext")
+@SuppressWarnings("unused") // Injected
public class CreateSecurityContextFilter implements SecurityRequestFilter {
@Override
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/ScrewdriverApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/ScrewdriverApiHandler.java
index 3dbff0b4aa3..e350b98adb9 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/ScrewdriverApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/ScrewdriverApiHandler.java
@@ -103,26 +103,26 @@ public class ScrewdriverApiHandler extends LoggingRequestHandler {
}
private HttpResponse trigger(HttpRequest request, String tenantName, String applicationName) {
+ JobType jobType = Optional.of(asString(request.getData()))
+ .filter(s -> !s.isEmpty())
+ .map(JobType::fromJobName)
+ .orElse(JobType.component);
+
ApplicationId applicationId = ApplicationId.from(tenantName, applicationName, "default");
- try (Lock lock = controller.applications().lock(applicationId)) {
- LockedApplication application = controller.applications().require(applicationId, lock);
- JobType jobType = Optional.of(asString(request.getData()))
- .filter(s -> !s.isEmpty())
- .map(JobType::fromId)
- .orElse(JobType.component);
+ controller.applications().lockedOrThrow(applicationId, application -> {
// Since this is a manual operation we likely want it to trigger as soon as possible so we add it at to the
// front of the queue
application = controller.applications().deploymentTrigger().triggerAllowParallel(
jobType, application, true, true,
- "Triggered from the screwdriver/v1 web service"
+ "Triggered from screwdriver/v1"
);
controller.applications().store(application);
+ });
- Slime slime = new Slime();
- Cursor cursor = slime.setObject();
- cursor.setString("message", "Triggered " + jobType.id() + " for " + applicationId);
- return new SlimeJsonResponse(slime);
- }
+ Slime slime = new Slime();
+ Cursor cursor = slime.setObject();
+ cursor.setString("message", "Triggered " + jobType.jobName() + " for " + applicationId);
+ return new SlimeJsonResponse(slime);
}
private HttpResponse vespaVersion() {
@@ -174,7 +174,7 @@ public class ScrewdriverApiHandler extends LoggingRequestHandler {
report.field("tenant").asString(),
report.field("application").asString(),
report.field("instance").asString()),
- JobType.fromId(report.field("jobName").asString()),
+ JobType.fromJobName(report.field("jobName").asString()),
report.field("projectId").asLong(),
report.field("buildNumber").asLong(),
jobError
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiHandler.java
new file mode 100644
index 00000000000..3a3fd445bcf
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiHandler.java
@@ -0,0 +1,131 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.restapi.zone.v1;
+
+import com.yahoo.config.provision.Environment;
+import com.yahoo.config.provision.RegionName;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.jdisc.LoggingRequestHandler;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.Slime;
+import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry;
+import com.yahoo.vespa.hosted.controller.restapi.ErrorResponse;
+import com.yahoo.vespa.hosted.controller.restapi.Path;
+import com.yahoo.vespa.hosted.controller.restapi.SlimeJsonResponse;
+import com.yahoo.yolean.Exceptions;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.logging.Level;
+import java.util.stream.Collectors;
+
+/**
+ * REST API that provides information about Hosted Vespa zones (version 1)
+ *
+ * @author mpolden
+ */
+@SuppressWarnings("unused")
+public class ZoneApiHandler extends LoggingRequestHandler {
+
+ private final ZoneRegistry zoneRegistry;
+
+ public ZoneApiHandler(Executor executor, AccessLog accessLog, ZoneRegistry zoneRegistry) {
+ super(executor, accessLog);
+ this.zoneRegistry = zoneRegistry;
+ }
+
+ @Override
+ public HttpResponse handle(HttpRequest request) {
+ try {
+ switch (request.getMethod()) {
+ case GET:
+ return get(request);
+ default:
+ return ErrorResponse.methodNotAllowed("Method '" + request.getMethod() + "' is unsupported");
+ }
+ } catch (IllegalArgumentException e) {
+ return ErrorResponse.badRequest(Exceptions.toMessageString(e));
+ } catch (RuntimeException e) {
+ log.log(Level.WARNING, "Unexpected error handling '" + request.getUri() + "'", e);
+ return ErrorResponse.internalServerError(Exceptions.toMessageString(e));
+ }
+ }
+
+ private HttpResponse get(HttpRequest request) {
+ Path path = new Path(request.getUri().getPath());
+ if (path.matches("/zone/v1")) {
+ return root(request);
+ }
+ if (path.matches("/zone/v1/environment/{environment}")) {
+ return environment(request, Environment.from(path.get("environment")));
+ }
+ if (path.matches("/zone/v1/environment/{environment}/default")) {
+ return defaultRegion(request, Environment.from(path.get("environment")));
+ }
+ return notFound(path);
+ }
+
+ private HttpResponse root(HttpRequest request) {
+ List<Environment> environments = zoneRegistry.zones().stream()
+ .map(Zone::environment)
+ .distinct()
+ .sorted(Comparator.comparing(Environment::value))
+ .collect(Collectors.toList());
+ Slime slime = new Slime();
+ Cursor root = slime.setArray();
+ environments.forEach(environment -> {
+ Cursor object = root.addObject();
+ object.setString("name", environment.value());
+ // Returning /zone/v2 is a bit strange, but that's what the original Jersey implementation did
+ object.setString("url", request.getUri()
+ .resolve("/zone/v2/environment/")
+ .resolve(environment.value())
+ .toString());
+ });
+ return new SlimeJsonResponse(slime);
+ }
+
+ private HttpResponse environment(HttpRequest request, Environment environment) {
+ List<Zone> zones = zoneRegistry.zones().stream()
+ .filter(zone -> zone.environment() == environment)
+ .collect(Collectors.toList());
+ Slime slime = new Slime();
+ Cursor root = slime.setArray();
+ zones.forEach(zone -> {
+ Cursor object = root.addObject();
+ object.setString("name", zone.region().value());
+ object.setString("url", request.getUri()
+ .resolve("/zone/v2/environment/")
+ .resolve(environment.value() + "/")
+ .resolve("region/")
+ .resolve(zone.region().value())
+ .toString());
+ });
+ return new SlimeJsonResponse(slime);
+ }
+
+ private HttpResponse defaultRegion(HttpRequest request, Environment environment) {
+ RegionName region = zoneRegistry.getDefaultRegion(environment)
+ .orElseThrow(() -> new IllegalArgumentException(
+ "No default region for environment: " + environment
+ ));
+ Slime slime = new Slime();
+ Cursor root = slime.setObject();
+ root.setString("name", region.value());
+ root.setString("url", request.getUri()
+ .resolve("/zone/v2/environment/")
+ .resolve(environment.value() + "/")
+ .resolve("region/")
+ .resolve(region.value())
+ .toString());
+ return new SlimeJsonResponse(slime);
+ }
+
+ private HttpResponse notFound(Path path) {
+ return ErrorResponse.notFoundError("Nothing at " + path);
+ }
+
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/package-info.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/package-info.java
new file mode 100644
index 00000000000..7793548766e
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+/**
+ * @author mpolden
+ */
+package com.yahoo.vespa.hosted.controller.restapi.zone.v1;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiHandler.java
new file mode 100644
index 00000000000..529b2b25785
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiHandler.java
@@ -0,0 +1,116 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.restapi.zone.v2;
+
+import com.yahoo.config.provision.Environment;
+import com.yahoo.config.provision.RegionName;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.container.jdisc.LoggingRequestHandler;
+import com.yahoo.container.logging.AccessLog;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.Slime;
+import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry;
+import com.yahoo.vespa.hosted.controller.proxy.ConfigServerRestExecutor;
+import com.yahoo.vespa.hosted.controller.proxy.ProxyException;
+import com.yahoo.vespa.hosted.controller.proxy.ProxyRequest;
+import com.yahoo.vespa.hosted.controller.restapi.ErrorResponse;
+import com.yahoo.vespa.hosted.controller.restapi.Path;
+import com.yahoo.vespa.hosted.controller.restapi.SlimeJsonResponse;
+import com.yahoo.yolean.Exceptions;
+
+import java.io.IOException;
+import java.util.Optional;
+import java.util.concurrent.Executor;
+import java.util.logging.Level;
+
+/**
+ * REST API for proxying requests to config servers in a given zone (version 2).
+ *
+ * This API does something completely different from /zone/v1, but such is the world.
+ *
+ * @author mpolden
+ */
+@SuppressWarnings("unused")
+public class ZoneApiHandler extends LoggingRequestHandler {
+
+ private final ZoneRegistry zoneRegistry;
+ private final ConfigServerRestExecutor proxy;
+
+ public ZoneApiHandler(Executor executor, AccessLog accessLog, ZoneRegistry zoneRegistry,
+ ConfigServerRestExecutor proxy) {
+ super(executor, accessLog);
+ this.zoneRegistry = zoneRegistry;
+ this.proxy = proxy;
+ }
+
+ @Override
+ public HttpResponse handle(HttpRequest request) {
+ try {
+ switch (request.getMethod()) {
+ case GET:
+ return get(request);
+ case POST:
+ case PUT:
+ case DELETE:
+ case PATCH:
+ return proxy(request);
+ default:
+ return ErrorResponse.methodNotAllowed("Method '" + request.getMethod() + "' is unsupported");
+ }
+ } catch (IllegalArgumentException e) {
+ return ErrorResponse.badRequest(Exceptions.toMessageString(e));
+ } catch (RuntimeException e) {
+ log.log(Level.WARNING, "Unexpected error handling '" + request.getUri() + "'", e);
+ return ErrorResponse.internalServerError(Exceptions.toMessageString(e));
+ }
+ }
+
+ private HttpResponse get(HttpRequest request) {
+ Path path = new Path(request.getUri().getPath());
+ if (path.matches("/zone/v2")) {
+ return root(request);
+ }
+ return proxy(request);
+ }
+
+ private HttpResponse proxy(HttpRequest request) {
+ Path path = new Path(request.getUri().getPath());
+ if (!path.matches("/zone/v2/{environment}/{region}/{*}")) {
+ return notFound(path);
+ }
+ Environment environment = Environment.from(path.get("environment"));
+ RegionName region = RegionName.from(path.get("region"));
+ Optional<Zone> zone = zoneRegistry.getZone(environment, region);
+ if (!zone.isPresent()) {
+ throw new IllegalArgumentException("No such zone: " + environment.value() + "." + region.value());
+ }
+ try {
+ return proxy.handle(new ProxyRequest(request, "/zone/v2/"));
+ } catch (ProxyException | IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private HttpResponse root(HttpRequest request) {
+ Slime slime = new Slime();
+ Cursor root = slime.setObject();
+ Cursor uris = root.setArray("uris");
+ zoneRegistry.zones().forEach(zone -> uris.addString(request.getUri()
+ .resolve("/zone/v2/")
+ .resolve(zone.environment().value() + "/")
+ .resolve(zone.region().value())
+ .toString()));
+ Cursor zones = root.setArray("zones");
+ zoneRegistry.zones().forEach(zone -> {
+ Cursor object = zones.addObject();
+ object.setString("environment", zone.environment().value());
+ object.setString("region", zone.region().value());
+ });
+ return new SlimeJsonResponse(slime);
+ }
+
+ private HttpResponse notFound(Path path) {
+ return ErrorResponse.notFoundError("Nothing at " + path);
+ }
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/package-info.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/package-info.java
new file mode 100644
index 00000000000..95dfed8b7b2
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+/**
+ * @author mpolden
+ */
+package com.yahoo.vespa.hosted.controller.restapi.zone.v2;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/DeploymentStatistics.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/DeploymentStatistics.java
index 6174a017a54..ae7223489c2 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/DeploymentStatistics.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/DeploymentStatistics.java
@@ -1,12 +1,13 @@
// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.versions;
-import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import com.yahoo.component.Version;
import com.yahoo.config.provision.ApplicationId;
-import java.util.List;
+import java.util.Collection;
import java.util.Objects;
+import java.util.Set;
/**
* Statistics about deployments on a platform version. This is immutable.
@@ -16,20 +17,22 @@ import java.util.Objects;
public class DeploymentStatistics {
private final Version version;
- private final ImmutableList<ApplicationId> failing;
- private final ImmutableList<ApplicationId> production;
+ private final ImmutableSet<ApplicationId> failing;
+ private final ImmutableSet<ApplicationId> production;
+ private final ImmutableSet<ApplicationId> deploying;
/** DO NOT USE. Public for serialization purposes */
- public DeploymentStatistics(Version version, List<ApplicationId> failingApplications,
- List<ApplicationId> production) {
+ public DeploymentStatistics(Version version, Collection<ApplicationId> failingApplications,
+ Collection<ApplicationId> production, Collection<ApplicationId> deploying) {
this.version = version;
- this.failing = ImmutableList.copyOf(failingApplications);
- this.production = ImmutableList.copyOf(production);
+ this.failing = ImmutableSet.copyOf(failingApplications);
+ this.production = ImmutableSet.copyOf(production);
+ this.deploying = ImmutableSet.copyOf(deploying);
}
/** Returns a statistics instance with the values as 0 */
public static DeploymentStatistics empty(Version version) {
- return new DeploymentStatistics(version, ImmutableList.of(), ImmutableList.of());
+ return new DeploymentStatistics(version, ImmutableSet.of(), ImmutableSet.of(), ImmutableSet.of());
}
/** Returns the version these statistics are for */
@@ -39,23 +42,31 @@ public class DeploymentStatistics {
* Returns the applications which have at least one job (of any type) which fails on this version,
* excluding errors known to not be caused by this version
*/
- public List<ApplicationId> failing() { return failing; }
+ public Set<ApplicationId> failing() { return failing; }
/** Returns the applications which have this version in production in at least one zone */
- public List<ApplicationId> production() { return production; }
-
+ public Set<ApplicationId> production() { return production; }
+
+ /** Returns the applications which are currently upgrading to this version */
+ public Set<ApplicationId> deploying() { return deploying; }
+
/** Returns a version of this with the given failing application added */
public DeploymentStatistics withFailing(ApplicationId application) {
- return new DeploymentStatistics(version, add(application, failing), production);
+ return new DeploymentStatistics(version, add(application, failing), production, deploying);
}
/** Returns a version of this with the given production application added */
public DeploymentStatistics withProduction(ApplicationId application) {
- return new DeploymentStatistics(version, failing, add(application, production));
+ return new DeploymentStatistics(version, failing, add(application, production), deploying);
+ }
+
+ /** Returns a version of this with the given deploying application added */
+ public DeploymentStatistics withDeploying(ApplicationId application) {
+ return new DeploymentStatistics(version, failing, production, add(application, deploying));
}
- private ImmutableList<ApplicationId> add(ApplicationId application, ImmutableList<ApplicationId> list) {
- ImmutableList.Builder<ApplicationId> b = new ImmutableList.Builder<>();
+ private ImmutableSet<ApplicationId> add(ApplicationId application, ImmutableSet<ApplicationId> list) {
+ ImmutableSet.Builder<ApplicationId> b = new ImmutableSet.Builder<>();
b.addAll(list);
b.add(application);
return b.build();
@@ -75,4 +86,5 @@ public class DeploymentStatistics {
public int hashCode() {
return Objects.hash(version, failing, production);
}
+
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VersionStatus.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VersionStatus.java
index 73e4eb4d527..d152cf80472 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VersionStatus.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VersionStatus.java
@@ -11,7 +11,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.github.GitSha;
import com.yahoo.vespa.hosted.controller.application.ApplicationList;
import com.yahoo.vespa.hosted.controller.application.Deployment;
import com.yahoo.vespa.hosted.controller.application.DeploymentJobs;
-import com.yahoo.vespa.hosted.controller.application.JobStatus;
+import com.yahoo.vespa.hosted.controller.application.JobList;
import java.net.URI;
import java.time.Instant;
@@ -28,6 +28,8 @@ import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
+import static com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobError.outOfCapacity;
+
/**
* Information about the current platform versions in use.
* The versions in use are the set of all versions running in current applications, versions
@@ -92,7 +94,8 @@ public class VersionStatus {
Version systemVersion = infrastructureVersions.stream().sorted().findFirst().get();
Collection<DeploymentStatistics> deploymentStatistics = computeDeploymentStatistics(infrastructureVersions,
- controller.applications().asList());
+ controller.applications().asList(),
+ controller.applications().deploymentTrigger().jobTimeoutLimit());
List<VespaVersion> versions = new ArrayList<>();
for (DeploymentStatistics statistics : deploymentStatistics) {
@@ -126,7 +129,8 @@ public class VersionStatus {
}
private static Collection<DeploymentStatistics> computeDeploymentStatistics(Set<Version> infrastructureVersions,
- List<Application> applications) {
+ List<Application> applications,
+ Instant jobTimeoutLimit) {
Map<Version, DeploymentStatistics> versionMap = new HashMap<>();
for (Version infrastructureVersion : infrastructureVersions) {
@@ -142,41 +146,38 @@ public class VersionStatus {
versionMap.computeIfAbsent(deployment.version(), DeploymentStatistics::empty);
}
- // List versions which have failing jobs, and versions which are in production
+ // List versions which have failing jobs, versions which are in production, and versions for which there are running deployment jobs
// Failing versions
- Map<Version, List<JobStatus>> failingJobsByVersion = jobs.jobStatus().values().stream()
- .filter(jobStatus -> jobStatus.lastCompleted().isPresent())
- .filter(jobStatus -> jobStatus.lastCompleted().get().upgrade())
- .filter(jobStatus -> jobStatus.jobError().isPresent())
- .filter(jobStatus -> jobStatus.jobError().get() != DeploymentJobs.JobError.outOfCapacity)
- .collect(Collectors.groupingBy(jobStatus -> jobStatus.lastCompleted().get().version()));
- for (Version v : failingJobsByVersion.keySet()) {
- versionMap.compute(v, (version, statistics) -> emptyIfMissing(version, statistics).withFailing(application.id()));
- }
+ JobList.from(application)
+ .failing()
+ .not().failingApplicationChange()
+ .not().failingBecause(outOfCapacity)
+ .mapToList(job -> job.lastCompleted().get().version())
+ .forEach(version -> versionMap.put(version, versionMap.getOrDefault(version, DeploymentStatistics.empty(version)).withFailing(application.id())));
// Succeeding versions
- Map<Version, List<JobStatus>> succeedingJobsByVersions = jobs.jobStatus().values().stream()
- .filter(jobStatus -> jobStatus.lastSuccess().isPresent())
- .filter(jobStatus -> jobStatus.type().isProduction())
- .collect(Collectors.groupingBy(jobStatus -> jobStatus.lastSuccess().get().version()));
- for (Version v : succeedingJobsByVersions.keySet()) {
- versionMap.compute(v, (version, statistics) -> emptyIfMissing(version, statistics).withProduction(application.id()));
- }
+ JobList.from(application)
+ .lastSuccess().present()
+ .production()
+ .mapToList(job -> job.lastSuccess().get().version())
+ .forEach(version -> versionMap.put(version, versionMap.getOrDefault(version, DeploymentStatistics.empty(version)).withProduction(application.id())));
+
+ // Deploying versions
+ JobList.from(application)
+ .upgrading()
+ .mapToList(job -> job.lastTriggered().get().version())
+ .forEach(version -> versionMap.put(version, versionMap.getOrDefault(version, DeploymentStatistics.empty(version)).withDeploying(application.id())));
}
return versionMap.values();
}
- private static DeploymentStatistics emptyIfMissing(Version version, DeploymentStatistics statistics) {
- return statistics == null ? DeploymentStatistics.empty(version) : statistics;
- }
-
- private static VespaVersion createVersion(DeploymentStatistics statistics,
+ private static VespaVersion createVersion(DeploymentStatistics statistics,
boolean isSystemVersion,
Collection<String> configServerHostnames,
Controller controller) {
GitSha gitSha = controller.gitHub().getCommit(VESPA_REPO_OWNER, VESPA_REPO, statistics.version().toFullString());
- Instant releasedAt = Instant.ofEpochMilli(gitSha.commit.author.date.getTime());
+ Instant releasedAt = Instant.ofEpochMilli(gitSha.commit.author.date.getTime()); // commitedAt ...
VespaVersion.Confidence confidence;
// Always compute confidence for system version
if (isSystemVersion) {
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VespaVersion.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VespaVersion.java
index de42a9fba4e..4bcee5782ee 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VespaVersion.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VespaVersion.java
@@ -58,7 +58,7 @@ public class VespaVersion implements Comparable<VespaVersion> {
return Confidence.broken;
// 'broken' if 4 non-canary was broken by this, and that is at least 10% of all
- if (nonCanaryApplicationsBroken(failingOnThis, productionOnThis, releasedAt, controller.curator()))
+ if (nonCanaryApplicationsBroken(statistics.version(), failingOnThis, productionOnThis, releasedAt, controller.curator()))
return Confidence.broken;
// 'low' unless all canary applications are upgraded
@@ -142,11 +142,12 @@ public class VespaVersion implements Comparable<VespaVersion> {
}
- private static boolean nonCanaryApplicationsBroken(ApplicationList failingOnThis,
+ private static boolean nonCanaryApplicationsBroken(Version version,
+ ApplicationList failingOnThis,
ApplicationList productionOnThis,
Instant releasedAt,
CuratorDb curator) {
- ApplicationList failingNonCanaries = failingOnThis.without(UpgradePolicy.canary).startedFailingAfter(releasedAt);
+ ApplicationList failingNonCanaries = failingOnThis.without(UpgradePolicy.canary).startedFailingOnVersionAfter(version, releasedAt);
ApplicationList productionNonCanaries = productionOnThis.without(UpgradePolicy.canary);
if (productionNonCanaries.size() + failingNonCanaries.size() == 0 || curator.readIgnoreConfidence()) return false;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/rotation/ControllerRotationRepository.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/rotation/ControllerRotationRepository.java
index 9eef1dac70b..363a2ea19cd 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/rotation/ControllerRotationRepository.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/rotation/ControllerRotationRepository.java
@@ -4,13 +4,12 @@ package com.yahoo.vespa.hosted.rotation;
import com.yahoo.config.application.api.DeploymentSpec;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.Environment;
+import com.yahoo.jdisc.Metric;
import com.yahoo.log.LogLevel;
-import com.yahoo.metrics.simple.Gauge;
-import com.yahoo.metrics.simple.MetricReceiver;
-import com.yahoo.vespa.hosted.controller.api.identifiers.RotationId;
import com.yahoo.vespa.hosted.controller.api.ApplicationAlias;
-import com.yahoo.vespa.hosted.controller.persistence.ControllerDb;
+import com.yahoo.vespa.hosted.controller.api.identifiers.RotationId;
import com.yahoo.vespa.hosted.controller.api.rotation.Rotation;
+import com.yahoo.vespa.hosted.controller.persistence.ControllerDb;
import com.yahoo.vespa.hosted.rotation.config.RotationsConfig;
import org.jetbrains.annotations.NotNull;
@@ -31,17 +30,16 @@ import java.util.stream.Collectors;
public class ControllerRotationRepository implements RotationRepository {
private static final Logger log = Logger.getLogger(ControllerRotationRepository.class.getName());
-
- private static final String REMAINING_ROTATIONS_METRIC_NAME = "remaining_rotations";
- private final Gauge remainingRotations;
+ public static final String REMAINING_ROTATIONS_METRIC_NAME = "remaining_rotations";
private final ControllerDb controllerDb;
private final Map<RotationId, Rotation> rotationsMap;
+ private final Metric metric;
- public ControllerRotationRepository(RotationsConfig rotationConfig, ControllerDb controllerDb, MetricReceiver metricReceiver) {
+ public ControllerRotationRepository(RotationsConfig rotationConfig, ControllerDb controllerDb, Metric metric) {
this.controllerDb = controllerDb;
this.rotationsMap = buildRotationsMap(rotationConfig);
- this.remainingRotations = metricReceiver.declareGauge(REMAINING_ROTATIONS_METRIC_NAME);
+ this.metric = metric;
}
private static Map<RotationId, Rotation> buildRotationsMap(RotationsConfig rotationConfig) {
@@ -73,7 +71,7 @@ public class ControllerRotationRepository implements RotationRepository {
.collect(Collectors.toSet());
}
- if( ! deploymentSpec.globalServiceId().isPresent()) {
+ if (!deploymentSpec.globalServiceId().isPresent()) {
return Collections.emptySet();
}
@@ -84,13 +82,12 @@ public class ControllerRotationRepository implements RotationRepository {
if (productionZoneCount >= 2) {
return assignRotation(applicationId);
- }
- else {
+ } else {
throw new IllegalArgumentException("global-service-id is set but less than 2 prod zones are defined");
}
}
- private boolean isCorp(DeploymentSpec.DeclaredZone zone) {
+ private static boolean isCorp(DeploymentSpec.DeclaredZone zone) {
return zone.region().isPresent() && zone.region().get().value().contains("corp");
}
@@ -139,7 +136,8 @@ public class ControllerRotationRepository implements RotationRepository {
try {
int freeRotationsCount = availableRotations().size();
log.log(LogLevel.INFO, "Rotation: {0} global rotations remaining", freeRotationsCount);
- remainingRotations.sample(freeRotationsCount);
+ metric.set(REMAINING_ROTATIONS_METRIC_NAME, freeRotationsCount,
+ metric.createContext(Collections.emptyMap()));
} catch (Exception e) {
log.log(LogLevel.INFO, "Failed to report rotations metric", e);
}
diff --git a/controller-server/src/main/resources/configdefinitions/athenz.def b/controller-server/src/main/resources/configdefinitions/athenz.def
index 4e27e3ebd07..6d10f3dee28 100644
--- a/controller-server/src/main/resources/configdefinitions/athenz.def
+++ b/controller-server/src/main/resources/configdefinitions/athenz.def
@@ -13,6 +13,10 @@ ztsUrl string
# Athenz domain for controller identity. The domain is also used for Athenz tenancy integration.
domain string
+# Name of the internal user authentication passthru attribute
+userAuthenticationPassThruAttribute string
+# TODO Remove once migrated to Okta
+
# Athenz service name for controller identity
service.name string
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ConfigServerProxyMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ConfigServerProxyMock.java
new file mode 100644
index 00000000000..cc915d4d9a1
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ConfigServerProxyMock.java
@@ -0,0 +1,43 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller;
+
+import com.yahoo.component.AbstractComponent;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.vespa.hosted.controller.proxy.ConfigServerRestExecutor;
+import com.yahoo.vespa.hosted.controller.proxy.ProxyException;
+import com.yahoo.vespa.hosted.controller.proxy.ProxyRequest;
+import com.yahoo.vespa.hosted.controller.restapi.StringResponse;
+
+import java.io.InputStream;
+import java.util.Optional;
+import java.util.Scanner;
+
+/**
+ * @author mpolden
+ */
+public class ConfigServerProxyMock extends AbstractComponent implements ConfigServerRestExecutor {
+
+ private volatile ProxyRequest lastReceived = null;
+ private volatile String requestBody = null;
+
+ @Override
+ public HttpResponse handle(ProxyRequest proxyRequest) throws ProxyException {
+ lastReceived = proxyRequest;
+ // Copy request body as the input stream is drained once the request completes
+ requestBody = asString(proxyRequest.getData());
+ return new StringResponse("ok");
+ }
+
+ public Optional<ProxyRequest> lastReceived() {
+ return Optional.ofNullable(lastReceived);
+ }
+
+ public Optional<String> lastRequestBody() {
+ return Optional.ofNullable(requestBody);
+ }
+
+ private static String asString(InputStream inputStream) {
+ Scanner scanner = new Scanner(inputStream).useDelimiter("\\A");
+ return scanner.hasNext() ? scanner.next() : "";
+ }
+}
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 aea66f3cd67..d0c1fd95427 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
@@ -30,7 +30,6 @@ import com.yahoo.vespa.hosted.controller.application.ApplicationRevision;
import com.yahoo.vespa.hosted.controller.application.Change;
import com.yahoo.vespa.hosted.controller.application.DeploymentJobs;
import com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobError;
-import com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobReport;
import com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobType;
import com.yahoo.vespa.hosted.controller.application.JobStatus;
import com.yahoo.vespa.hosted.controller.athenz.NToken;
@@ -80,6 +79,12 @@ public class ControllerTest {
.region("corp-us-east-1")
.build();
+ private static final ApplicationPackage applicationPackage2 = new ApplicationPackageBuilder()
+ .environment(Environment.prod)
+ .region("corp-us-east-1")
+ .region("us-west-1")
+ .build();
+
@Test
public void testDeployment() {
// Setup system
@@ -94,7 +99,7 @@ public class ControllerTest {
// staging job - succeeding
Version version1 = Version.fromString("6.1"); // Set in config server mock
Application app1 = tester.createApplication("app1", "tenant1", 1, 11L);
- applications.notifyJobCompletion(mockReport(app1, component, true));
+ tester.notifyJobCompletion(component, app1, true);
assertFalse("Revision is currently not known",
((Change.ApplicationChange)tester.controller().applications().require(app1.id()).deploying().get()).revision().isPresent());
tester.deployAndNotify(app1, applicationPackage, true, systemTest);
@@ -104,12 +109,12 @@ public class ControllerTest {
Optional<ApplicationRevision> revision = ((Change.ApplicationChange)tester.controller().applications().require(app1.id()).deploying().get()).revision();
assertTrue("Revision has been set during deployment", revision.isPresent());
assertStatus(JobStatus.initial(stagingTest)
- .withTriggering(-1, version1, revision, false, "", tester.clock().instant())
- .withCompletion(-1, Optional.empty(), tester.clock().instant(), tester.controller()), app1.id(), tester.controller());
+ .withTriggering(version1, revision, false, "", tester.clock().instant().minus(Duration.ofMillis(1)))
+ .withCompletion(42, Optional.empty(), tester.clock().instant(), tester.controller()), app1.id(), tester.controller());
// Causes first deployment job to be triggered
assertStatus(JobStatus.initial(productionCorpUsEast1)
- .withTriggering(-1, version1, revision, false, "", tester.clock().instant()), app1.id(), tester.controller());
+ .withTriggering(version1, revision, false, "", tester.clock().instant()), app1.id(), tester.controller());
tester.clock().advance(Duration.ofSeconds(1));
// production job (failing)
@@ -117,10 +122,10 @@ public class ControllerTest {
assertEquals(4, applications.require(app1.id()).deploymentJobs().jobStatus().size());
JobStatus expectedJobStatus = JobStatus.initial(productionCorpUsEast1)
- .withTriggering(-1, version1, revision, false, "", tester.clock().instant()) // Triggered first without revision info
- .withCompletion(-1, Optional.of(JobError.unknown), tester.clock().instant(), tester.controller())
- .withTriggering(-1, version1, revision, false, "", tester.clock().instant()); // Re-triggering (due to failure) has revision info
-
+ .withTriggering(version1, revision, false, "", tester.clock().instant()) // Triggered first without revision info
+ .withCompletion(42, Optional.of(JobError.unknown), tester.clock().instant(), tester.controller())
+ .withTriggering(version1, revision, false, "", tester.clock().instant()); // Re-triggering (due to failure) has revision info
+
assertStatus(expectedJobStatus, app1.id(), tester.controller());
// Simulate restart
@@ -133,26 +138,29 @@ public class ControllerTest {
InstanceName.from("default"))));
assertEquals(4, applications.require(app1.id()).deploymentJobs().jobStatus().size());
- tester.clock().advance(Duration.ofSeconds(1));
+
+ tester.clock().advance(Duration.ofHours(1));
+
+ tester.notifyJobCompletion(productionCorpUsEast1, app1, false); // Need to complete the job, or new jobs won't start.
// system and staging test job - succeeding
- applications.notifyJobCompletion(mockReport(app1, component, true));
+ tester.notifyJobCompletion(component, app1, true);
tester.deployAndNotify(app1, applicationPackage, true, false, systemTest);
assertStatus(JobStatus.initial(systemTest)
- .withTriggering(-1, version1, revision, false, "", tester.clock().instant())
- .withCompletion(-1, Optional.empty(), tester.clock().instant(), tester.controller()), app1.id(), tester.controller());
+ .withTriggering(version1, revision, false, "", tester.clock().instant().minus(Duration.ofMillis(1)))
+ .withCompletion(42, Optional.empty(), tester.clock().instant(), tester.controller()), app1.id(), tester.controller());
tester.deployAndNotify(app1, applicationPackage, true, stagingTest);
// production job succeeding now
tester.deployAndNotify(app1, applicationPackage, true, productionCorpUsEast1);
expectedJobStatus = expectedJobStatus
- .withTriggering(-1, version1, revision, false, "", tester.clock().instant())
- .withCompletion(-1, Optional.empty(), tester.clock().instant(), tester.controller());
+ .withTriggering(version1, revision, false, "", tester.clock().instant().minus(Duration.ofMillis(1)))
+ .withCompletion(42, Optional.empty(), tester.clock().instant(), tester.controller());
assertStatus(expectedJobStatus, app1.id(), tester.controller());
// causes triggering of next production job
assertStatus(JobStatus.initial(productionUsEast3)
- .withTriggering(-1, version1, revision, false, "", tester.clock().instant()),
+ .withTriggering(version1, revision, false, "", tester.clock().instant()),
app1.id(), tester.controller());
tester.deployAndNotify(app1, applicationPackage, true, productionUsEast3);
@@ -163,7 +171,7 @@ public class ControllerTest {
.environment(Environment.prod)
.region("us-east-3")
.build();
- applications.notifyJobCompletion(mockReport(app1, component, true));
+ tester.notifyJobCompletion(component, app1, true);
try {
tester.deploy(systemTest, app1, applicationPackage);
fail("Expected exception due to unallowed production deployment removal");
@@ -176,7 +184,7 @@ public class ControllerTest {
JobStatus jobStatus = applications.require(app1.id()).deploymentJobs().jobStatus().get(productionCorpUsEast1);
assertNotNull("Deployment job was not removed", jobStatus);
assertEquals(42, jobStatus.lastCompleted().get().id());
- assertEquals("stagingTest completed successfully in build 42", jobStatus.lastCompleted().get().reason());
+ assertEquals("staging-test completed", jobStatus.lastCompleted().get().reason());
// prod zone removal is allowed with override
applicationPackage = new ApplicationPackageBuilder()
@@ -205,13 +213,13 @@ public class ControllerTest {
Application app1 = tester.createApplication("application1", "tenant1", 1, 1L);
// First deployment: An application change
- applications.notifyJobCompletion(mockReport(app1, component, true));
+ tester.notifyJobCompletion(component, app1, true);
tester.deployAndNotify(app1, applicationPackage, true, systemTest);
tester.deployAndNotify(app1, applicationPackage, true, stagingTest);
tester.deployAndNotify(app1, applicationPackage, true, productionUsWest1);
app1 = applications.require(app1.id());
- assertEquals("First deployment gets system version", systemVersion, app1.deployedVersion().get());
+ assertEquals("First deployment gets system version", systemVersion, app1.oldestDeployedVersion().get());
assertEquals(systemVersion, tester.configServer().lastPrepareVersion().get());
// Unexpected deployment
@@ -228,19 +236,19 @@ public class ControllerTest {
.region("us-west-1")
.region("us-east-3")
.build();
- applications.notifyJobCompletion(mockReport(app1, component, true));
+ tester.notifyJobCompletion(component, app1, true);
tester.deployAndNotify(app1, applicationPackage, true, systemTest);
tester.deployAndNotify(app1, applicationPackage, true, stagingTest);
tester.deployAndNotify(app1, applicationPackage, true, productionUsWest1);
app1 = applications.require(app1.id());
- assertEquals("Application change preserves version", systemVersion, app1.deployedVersion().get());
+ assertEquals("Application change preserves version", systemVersion, app1.oldestDeployedVersion().get());
assertEquals(systemVersion, tester.configServer().lastPrepareVersion().get());
// A deployment to the new region gets the same version
tester.deployAndNotify(app1, applicationPackage, true, productionUsEast3);
app1 = applications.require(app1.id());
- assertEquals("Application change preserves version", systemVersion, app1.deployedVersion().get());
+ assertEquals("Application change preserves version", systemVersion, app1.oldestDeployedVersion().get());
assertEquals(systemVersion, tester.configServer().lastPrepareVersion().get());
assertFalse("Change deployed", app1.deploying().isPresent());
@@ -253,7 +261,7 @@ public class ControllerTest {
tester.deployAndNotify(app1, applicationPackage, true, productionUsEast3);
app1 = applications.require(app1.id());
- assertEquals("Version upgrade changes version", newSystemVersion, app1.deployedVersion().get());
+ assertEquals("Version upgrade changes version", newSystemVersion, app1.oldestDeployedVersion().get());
assertEquals(newSystemVersion, tester.configServer().lastPrepareVersion().get());
}
@@ -322,13 +330,13 @@ public class ControllerTest {
tester.notifyJobCompletion(component, app, true);
tester.deployAndNotify(app, applicationPackage, false, systemTest);
assertEquals("Failure age is right at initial failure",
- initialFailure, firstFailing(app, tester).get().at());
+ initialFailure.plus(Duration.ofMillis(2)), firstFailing(app, tester).get().at());
// Failure again -- failingSince should remain the same
tester.clock().advance(Duration.ofMillis(1000));
tester.deployAndNotify(app, applicationPackage, false, systemTest);
assertEquals("Failure age is right at second consecutive failure",
- initialFailure, firstFailing(app, tester).get().at());
+ initialFailure.plus(Duration.ofMillis(2)), firstFailing(app, tester).get().at());
// Success resets failingSince
tester.clock().advance(Duration.ofMillis(1000));
@@ -346,13 +354,13 @@ public class ControllerTest {
tester.notifyJobCompletion(component, app, true);
tester.deployAndNotify(app, applicationPackage, false, systemTest);
assertEquals("Failure age is right at initial failure",
- initialFailure, firstFailing(app, tester).get().at());
+ initialFailure.plus(Duration.ofMillis(2)), firstFailing(app, tester).get().at());
// Failure again -- failingSince should remain the same
tester.clock().advance(Duration.ofMillis(1000));
tester.deployAndNotify(app, applicationPackage, false, systemTest);
assertEquals("Failure age is right at second consecutive failure",
- initialFailure, firstFailing(app, tester).get().at());
+ initialFailure.plus(Duration.ofMillis(2)), firstFailing(app, tester).get().at());
}
private Optional<JobStatus.JobRun> firstFailing(Application application, DeploymentTester tester) {
@@ -425,7 +433,7 @@ public class ControllerTest {
// app1: staging-test job fails with out of capacity and is added to the front of the queue
tester.deploy(stagingTest, app1, applicationPackage);
tester.notifyJobCompletion(stagingTest, app1, Optional.of(JobError.outOfCapacity));
- assertEquals(stagingTest.id(), buildSystem.jobs().get(0).jobName());
+ assertEquals(stagingTest.jobName(), buildSystem.jobs().get(0).jobName());
assertEquals(project1, buildSystem.jobs().get(0).projectId());
// app2 and app3: Completes deployment
@@ -437,6 +445,7 @@ public class ControllerTest {
// app1: 15 minutes pass, staging-test job is still failing due out of capacity, but is no longer re-queued by
// out of capacity retry mechanism
tester.clock().advance(Duration.ofMinutes(15));
+ tester.notifyJobCompletion(stagingTest, app1, Optional.of(JobError.outOfCapacity)); // Clear the previous staging test
tester.notifyJobCompletion(component, app1, true);
tester.deployAndNotify(app1, applicationPackage, true, false, systemTest);
tester.deploy(stagingTest, app1, applicationPackage);
@@ -444,12 +453,13 @@ public class ControllerTest {
tester.notifyJobCompletion(stagingTest, app1, Optional.of(JobError.outOfCapacity));
assertTrue("No jobs queued", buildSystem.jobs().isEmpty());
- // app2 and app3: New change triggers staging-test jobs
+ // app2 and app3: New change triggers system-test jobs
+ // Provide a changed application package, too, or the deployment is a no-op.
tester.notifyJobCompletion(component, app2, true);
- tester.deployAndNotify(app2, applicationPackage, true, systemTest);
+ tester.deployAndNotify(app2, applicationPackage2, true, systemTest);
tester.notifyJobCompletion(component, app3, true);
- tester.deployAndNotify(app3, applicationPackage, true, systemTest);
+ tester.deployAndNotify(app3, applicationPackage2, true, systemTest);
assertEquals(2, buildSystem.jobs().size());
@@ -457,19 +467,19 @@ public class ControllerTest {
// back of the queue
tester.clock().advance(Duration.ofHours(3));
tester.clock().advance(Duration.ofMinutes(50));
- tester.failureRedeployer().maintain();
+ tester.readyJobTrigger().maintain();
List<BuildJob> nextJobs = buildSystem.takeJobsToRun();
assertEquals(2, nextJobs.size());
- assertEquals(stagingTest.id(), nextJobs.get(0).jobName());
+ assertEquals(stagingTest.jobName(), nextJobs.get(0).jobName());
assertEquals(project2, nextJobs.get(0).projectId());
- assertEquals(stagingTest.id(), nextJobs.get(1).jobName());
+ assertEquals(stagingTest.jobName(), nextJobs.get(1).jobName());
assertEquals(project3, nextJobs.get(1).projectId());
// And finally the requeued job for app1
nextJobs = buildSystem.takeJobsToRun();
assertEquals(1, nextJobs.size());
- assertEquals(stagingTest.id(), nextJobs.get(0).jobName());
+ assertEquals(stagingTest.jobName(), nextJobs.get(0).jobName());
assertEquals(project1, nextJobs.get(0).projectId());
}
@@ -480,20 +490,6 @@ public class ControllerTest {
assertEquals(expectedStatus, existingStatus);
}
- private JobReport mockReport(Application application, JobType jobType, Optional<JobError> jobError) {
- return new JobReport(
- application.id(),
- jobType,
- application.deploymentJobs().projectId().get(),
- 42,
- jobError
- );
- }
-
- private JobReport mockReport(Application application, JobType jobType, boolean success) {
- return mockReport(application, jobType, JobError.from(success));
- }
-
@Test
public void testGlobalRotations() throws IOException {
// Setup tester and app def
@@ -527,8 +523,7 @@ public class ControllerTest {
TenantId tenant = tester.createTenant("tenant1", "domain1", 11L);
Application app = tester.createApplication(tenant, "app1", "default", 1);
- try (Lock lock = tester.controller().applications().lock(app.id())) {
- LockedApplication application = tester.controller().applications().require(app.id(), lock);
+ tester.controller().applications().lockedOrThrow(app.id(), application -> {
application = application.withDeploying(Optional.of(new Change.VersionChange(Version.fromString("6.3"))));
applications.store(application);
try {
@@ -537,7 +532,7 @@ public class ControllerTest {
} catch (IllegalArgumentException e) {
assertEquals("Rejecting deployment of application 'tenant1.app1' to zone prod.us-east-3 as version change to 6.3 is not tested", e.getMessage());
}
- }
+ });
}
@Test
@@ -557,11 +552,7 @@ public class ControllerTest {
// Load test data data
ApplicationSerializer serializer = new ApplicationSerializer();
byte[] json = Files.readAllBytes(Paths.get("src/test/java/com/yahoo/vespa/hosted/controller/maintenance/testdata/canary-with-stale-data.json"));
- Slime slime = SlimeUtils.jsonToSlime(json);
- Application application = serializer.fromSlime(slime);
- try (Lock lock = tester.controller().applications().lock(application.id())) {
- tester.controller().applications().store(new LockedApplication(application, lock));
- }
+ Application application = tester.controllerTester().createApplication(SlimeUtils.jsonToSlime(json));
ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
.upgradePolicy("canary")
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java
index 4e3c15ea1a4..8f9c22f8b81 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java
@@ -181,12 +181,9 @@ public final class ControllerTester {
public Application createApplication(TenantId tenant, String applicationName, String instanceName, long projectId) {
ApplicationId applicationId = applicationId(tenant.id(), applicationName, instanceName);
controller().applications().createApplication(applicationId, Optional.of(TestIdentities.userNToken));
- try (Lock lock = controller().applications().lock(applicationId)) {
- LockedApplication lockedApplication = controller().applications().require(applicationId, lock)
- .withProjectId(projectId);
- controller().applications().store(lockedApplication);
- return lockedApplication;
- }
+ controller().applications().lockedOrThrow(applicationId, lockedApplication ->
+ controller().applications().store(lockedApplication.withProjectId(projectId)));
+ return controller().applications().require(applicationId);
}
public void deploy(Application application, Zone zone) {
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ZoneRegistryMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ZoneRegistryMock.java
index bf21467bc8d..18332942c24 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ZoneRegistryMock.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ZoneRegistryMock.java
@@ -1,6 +1,8 @@
// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller;
+import com.google.inject.Inject;
+import com.yahoo.component.AbstractComponent;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.RegionName;
@@ -10,6 +12,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry;
import java.net.URI;
import java.time.Duration;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -19,15 +22,39 @@ import java.util.Optional;
/**
* @author mpolden
*/
-public class ZoneRegistryMock implements ZoneRegistry {
+public class ZoneRegistryMock extends AbstractComponent implements ZoneRegistry {
private final Map<Zone, Duration> deploymentTimeToLive = new HashMap<>();
+ private final Map<Environment, RegionName> defaultRegionForEnvironment = new HashMap<>();
+ private List<Zone> zones = new ArrayList<>();
+ private SystemName system = SystemName.main;
+
+ @Inject
+ public ZoneRegistryMock() {
+ this.zones.add(new Zone(SystemName.main, Environment.from("prod"), RegionName.from("corp-us-east-1")));
+ this.zones.add(new Zone(SystemName.main, Environment.from("prod"), RegionName.from("us-east-3")));
+ this.zones.add(new Zone(SystemName.main, Environment.from("prod"), RegionName.from("us-west-1")));
+ }
- public void setDeploymentTimeToLive(Zone zone, Duration duration) {
+ public ZoneRegistryMock setDeploymentTimeToLive(Zone zone, Duration duration) {
deploymentTimeToLive.put(zone, duration);
+ return this;
}
- private SystemName system = SystemName.main;
+ public ZoneRegistryMock setDefaultRegionForEnvironment(Environment environment, RegionName region) {
+ defaultRegionForEnvironment.put(environment, region);
+ return this;
+ }
+
+ public ZoneRegistryMock setZones(List<Zone> zones) {
+ this.zones = zones;
+ return this;
+ }
+
+ public ZoneRegistryMock setSystem(SystemName system) {
+ this.system = system;
+ return this;
+ }
@Override
public SystemName system() {
@@ -36,12 +63,13 @@ public class ZoneRegistryMock implements ZoneRegistry {
@Override
public List<Zone> zones() {
- return Collections.singletonList(new Zone(SystemName.main, Environment.from("prod"), RegionName.from("corp-us-east-1")));
+ return Collections.unmodifiableList(zones);
}
@Override
public Optional<Zone> getZone(Environment environment, RegionName region) {
- return zones().stream().filter(z -> z.environment().equals(environment) && z.region().equals(region)).findFirst();
+ return zones().stream().filter(z -> z.environment().equals(environment) &&
+ z.region().equals(region)).findFirst();
}
@Override
@@ -64,6 +92,11 @@ public class ZoneRegistryMock implements ZoneRegistry {
}
@Override
+ public Optional<RegionName> getDefaultRegion(Environment environment) {
+ return Optional.ofNullable(defaultRegionForEnvironment.get(environment));
+ }
+
+ @Override
public URI getMonitoringSystemUri(Environment environment, RegionName name, ApplicationId application) {
return URI.create("http://monitoring-system.test/?environment=" + environment.value() + "&region="
+ name.value() + "&application=" + application.toShortString());
@@ -74,7 +107,4 @@ public class ZoneRegistryMock implements ZoneRegistry {
return URI.create("http://dashboard.test");
}
- public void setSystem(SystemName system) {
- this.system = system;
- }
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTester.java
index 79fd717a24f..2b0e953c12c 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTester.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTester.java
@@ -16,17 +16,19 @@ import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
import com.yahoo.vespa.hosted.controller.application.Change;
import com.yahoo.vespa.hosted.controller.application.DeploymentJobs;
import com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobType;
-import com.yahoo.vespa.hosted.controller.maintenance.FailureRedeployer;
+import com.yahoo.vespa.hosted.controller.maintenance.ReadyJobsTrigger;
import com.yahoo.vespa.hosted.controller.maintenance.JobControl;
import com.yahoo.vespa.hosted.controller.maintenance.Upgrader;
import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
import java.time.Duration;
import java.util.List;
+import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
+import static com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobError.unknown;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -44,7 +46,7 @@ public class DeploymentTester {
private final ControllerTester tester;
private final Upgrader upgrader;
- private final FailureRedeployer failureRedeployer;
+ private final ReadyJobsTrigger readyJobTrigger;
public DeploymentTester() {
this(new ControllerTester());
@@ -55,18 +57,20 @@ public class DeploymentTester {
tester.curator().writeUpgradesPerMinute(100);
this.upgrader = new Upgrader(tester.controller(), maintenanceInterval, new JobControl(tester.curator()),
tester.curator());
- this.failureRedeployer = new FailureRedeployer(tester.controller(), maintenanceInterval,
- new JobControl(tester.curator()));
+ this.readyJobTrigger = new ReadyJobsTrigger(tester.controller(), maintenanceInterval,
+ new JobControl(tester.curator()));
}
public Upgrader upgrader() { return upgrader; }
- public FailureRedeployer failureRedeployer() { return failureRedeployer; }
+ public ReadyJobsTrigger readyJobTrigger() { return readyJobTrigger; }
public Controller controller() { return tester.controller(); }
public ApplicationController applications() { return tester.controller().applications(); }
+ // TODO: This thing simulates the wrong thing: the build system won't hold the jobs that are running,
+ // and so these should be consumed immediately upon triggering, and be "somewhere else" while running.
public BuildSystem buildSystem() { return tester.controller().applications().deploymentTrigger().buildSystem(); }
public DeploymentTrigger deploymentTrigger() { return tester.controller().applications().deploymentTrigger(); }
@@ -115,8 +119,13 @@ public class DeploymentTester {
/** Simulate the full lifecycle of an application deployment as declared in given application package */
public Application createAndDeploy(String applicationName, int projectId, ApplicationPackage applicationPackage) {
- tester.createTenant("tenant1", "domain1", 1L);
- Application application = tester.createApplication(new TenantId("tenant1"), applicationName, "default", projectId);
+ TenantId tenantId = tester.createTenant("tenant1", "domain1", 1L);
+ return createAndDeploy(tenantId, applicationName, projectId, applicationPackage);
+ }
+
+ /** Simulate the full lifecycle of an application deployment as declared in given application package */
+ public Application createAndDeploy(TenantId tenantId, String applicationName, int projectId, ApplicationPackage applicationPackage) {
+ Application application = tester.createApplication(tenantId, applicationName, "default", projectId);
deployCompletely(application, applicationPackage);
return applications().require(application.id());
}
@@ -126,6 +135,11 @@ public class DeploymentTester {
return createAndDeploy(applicationName, projectId, applicationPackage(upgradePolicy));
}
+ /** Simulate the full lifecycle of an application deployment to prod.us-west-1 with the given upgrade policy */
+ public Application createAndDeploy(TenantId tenantId, String applicationName, int projectId, String upgradePolicy) {
+ return createAndDeploy(tenantId, applicationName, projectId, applicationPackage(upgradePolicy));
+ }
+
/** Complete an ongoing deployment */
public void deployCompletely(String applicationName) {
deployCompletely(applications().require(ApplicationId.from("tenant1", applicationName, "default")),
@@ -139,6 +153,20 @@ public class DeploymentTester {
completeDeployment(application, applicationPackage, Optional.empty(), true);
}
+ public static DeploymentJobs.JobReport jobReport(Application application, JobType jobType, boolean success) {
+ return jobReport(application, jobType, Optional.ofNullable(success ? null : unknown));
+ }
+
+ public static DeploymentJobs.JobReport jobReport(Application application, JobType jobType, Optional<DeploymentJobs.JobError> jobError) {
+ return new DeploymentJobs.JobReport(
+ application.id(),
+ jobType,
+ application.deploymentJobs().projectId().get(),
+ 42,
+ jobError
+ );
+ }
+
/** Deploy application using the given application package, but expecting to stop after test phases */
public void deployTestOnly(Application application, ApplicationPackage applicationPackage) {
notifyJobCompletion(JobType.component, application, true);
@@ -154,7 +182,7 @@ public class DeploymentTester {
jobs = jobs.stream().filter(job -> ! job.isProduction()).collect(Collectors.toList());
for (JobType job : jobs) {
boolean failJob = failOnJob.map(j -> j.equals(job)).orElse(false);
- deployAndNotify(application, applicationPackage, !failJob, false, job);
+ deployAndNotify(application, applicationPackage, ! failJob, false, job);
if (failJob) {
break;
}
@@ -171,10 +199,11 @@ public class DeploymentTester {
}
public void notifyJobCompletion(JobType jobType, Application application, boolean success) {
- notifyJobCompletion(jobType, application, DeploymentJobs.JobError.from(success));
+ notifyJobCompletion(jobType, application, Optional.ofNullable(success ? null : unknown));
}
public void notifyJobCompletion(JobType jobType, Application application, Optional<DeploymentJobs.JobError> jobError) {
+ clock().advance(Duration.ofMillis(1));
applications().notifyJobCompletion(jobReport(application, jobType, jobError));
}
@@ -211,7 +240,7 @@ public class DeploymentTester {
deployAndNotify(application, applicationPackage, success, true, jobs);
}
- public void deployAndNotify(Application application, ApplicationPackage applicationPackage, boolean success,
+ public void deployAndNotify(Application application, ApplicationPackage applicationPackage, boolean success,
boolean expectOnlyTheseJobs, JobType... jobs) {
consumeJobs(application, expectOnlyTheseJobs, jobs);
for (JobType job : jobs) {
@@ -225,21 +254,20 @@ public class DeploymentTester {
/** Assert that the sceduled jobs of this application are exactly those given, and take them */
private void consumeJobs(Application application, boolean expectOnlyTheseJobs, JobType... jobs) {
for (JobType job : jobs) {
- Optional<BuildService.BuildJob> buildJob = findJob(application, job);
- assertTrue(String.format("Job %s is scheduled for %s", job, application), buildJob.isPresent());
- assertEquals((long) application.deploymentJobs().projectId().get(), buildJob.get().projectId());
- assertEquals(job.id(), buildJob.get().jobName());
+ BuildService.BuildJob buildJob = findJob(application, job);
+ assertEquals((long) application.deploymentJobs().projectId().get(), buildJob.projectId());
+ assertEquals(job.jobName(), buildJob.jobName());
}
if (expectOnlyTheseJobs)
assertEquals(jobs.length, countJobsOf(application));
buildSystem().removeJobs(application.id());
}
- private Optional<BuildService.BuildJob> findJob(Application application, JobType jobType) {
+ private BuildService.BuildJob findJob(Application application, JobType jobType) {
for (BuildService.BuildJob job : buildSystem().jobs())
- if (job.projectId() == application.deploymentJobs().projectId().get() && job.jobName().equals(jobType.id()))
- return Optional.of(job);
- return Optional.empty();
+ if (job.projectId() == application.deploymentJobs().projectId().get() && job.jobName().equals(jobType.jobName()))
+ return job;
+ throw new NoSuchElementException(jobType + " is not scheduled for " + application);
}
private int countJobsOf(Application application) {
@@ -247,17 +275,8 @@ public class DeploymentTester {
.filter(job -> job.projectId() == application.deploymentJobs().projectId().get())
.count();
}
- private DeploymentJobs.JobReport jobReport(Application application, JobType jobType, Optional<DeploymentJobs.JobError> jobError) {
- return new DeploymentJobs.JobReport(
- application.id(),
- jobType,
- application.deploymentJobs().projectId().get(),
- 42,
- jobError
- );
- }
- private static ApplicationPackage applicationPackage(String upgradePolicy) {
+ public static ApplicationPackage applicationPackage(String upgradePolicy) {
return new ApplicationPackageBuilder()
.upgradePolicy(upgradePolicy)
.environment(Environment.prod)
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java
index 022fa705def..10f8e80f318 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java
@@ -13,7 +13,7 @@ import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
import com.yahoo.vespa.hosted.controller.application.Change;
import com.yahoo.vespa.hosted.controller.application.DeploymentJobs;
import com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobType;
-import com.yahoo.vespa.hosted.controller.maintenance.BlockedChangeDeployer;
+import com.yahoo.vespa.hosted.controller.maintenance.ReadyJobsTrigger;
import com.yahoo.vespa.hosted.controller.maintenance.JobControl;
import org.junit.Test;
@@ -59,10 +59,11 @@ public class DeploymentTriggerTest {
// system-test fails and is retried
tester.deployAndNotify(app, applicationPackage, false, JobType.systemTest);
assertEquals("Retried immediately", 1, tester.buildSystem().jobs().size());
- tester.buildSystem().takeJobsToRun();
- assertEquals("Job removed", 0, tester.buildSystem().jobs().size());
- tester.clock().advance(Duration.ofHours(4).plus(Duration.ofSeconds(1)));
- tester.failureRedeployer().maintain(); // Causes retry of systemTests
+ tester.clock().advance(Duration.ofHours(1));
+ tester.deployAndNotify(app, applicationPackage, false, JobType.systemTest);
+ tester.clock().advance(Duration.ofHours(1));
+ assertEquals("Nothing scheduled", 0, tester.buildSystem().jobs().size());
+ tester.readyJobTrigger().maintain(); // Causes retry of systemTests
assertEquals("Scheduled retry", 1, tester.buildSystem().jobs().size());
tester.deployAndNotify(app, applicationPackage, true, JobType.systemTest);
@@ -70,9 +71,9 @@ public class DeploymentTriggerTest {
// staging-test times out and is retried
tester.buildSystem().takeJobsToRun();
tester.clock().advance(Duration.ofHours(12).plus(Duration.ofSeconds(1)));
- tester.failureRedeployer().maintain();
+ tester.readyJobTrigger().maintain();
assertEquals("Retried dead job", 1, tester.buildSystem().jobs().size());
- assertEquals(JobType.stagingTest.id(), tester.buildSystem().jobs().get(0).jobName());
+ assertEquals(JobType.stagingTest.jobName(), tester.buildSystem().jobs().get(0).jobName());
}
@Test
@@ -127,16 +128,16 @@ public class DeploymentTriggerTest {
// 30 seconds pass, us-west-1 is triggered
tester.clock().advance(Duration.ofSeconds(30));
- tester.deploymentTrigger().triggerDelayed();
+ tester.deploymentTrigger().triggerReadyJobs();
// Consume us-west-1 job without reporting completion
assertEquals(1, buildSystem.jobs().size());
- assertEquals(JobType.productionUsWest1.id(), buildSystem.jobs().get(0).jobName());
+ assertEquals(JobType.productionUsWest1.jobName(), buildSystem.jobs().get(0).jobName());
buildSystem.takeJobsToRun();
// 3 minutes pass, delayed trigger does nothing as us-west-1 is still in progress
tester.clock().advance(Duration.ofMinutes(3));
- tester.deploymentTrigger().triggerDelayed();
+ tester.deploymentTrigger().triggerReadyJobs();
assertTrue("No more jobs triggered at this time", buildSystem.jobs().isEmpty());
// us-west-1 completes
@@ -144,18 +145,18 @@ public class DeploymentTriggerTest {
tester.notifyJobCompletion(JobType.productionUsWest1, application, true);
// Delayed trigger does nothing as not enough time has passed after us-west-1 completion
- tester.deploymentTrigger().triggerDelayed();
+ tester.deploymentTrigger().triggerReadyJobs();
assertTrue("No more jobs triggered at this time", buildSystem.jobs().isEmpty());
// 3 minutes pass, us-central-1 is triggered
tester.clock().advance(Duration.ofMinutes(3));
- tester.deploymentTrigger().triggerDelayed();
+ tester.deploymentTrigger().triggerReadyJobs();
tester.deployAndNotify(application, applicationPackage, true, JobType.productionUsCentral1);
assertTrue("All jobs consumed", buildSystem.jobs().isEmpty());
// Delayed trigger job runs again, with nothing to trigger
tester.clock().advance(Duration.ofMinutes(10));
- tester.deploymentTrigger().triggerDelayed();
+ tester.deploymentTrigger().triggerReadyJobs();
assertTrue("All jobs consumed", buildSystem.jobs().isEmpty());
}
@@ -184,8 +185,8 @@ public class DeploymentTriggerTest {
// Deploys in two regions in parallel
assertEquals(2, tester.buildSystem().jobs().size());
- assertEquals(JobType.productionUsEast3.id(), tester.buildSystem().jobs().get(0).jobName());
- assertEquals(JobType.productionUsWest1.id(), tester.buildSystem().jobs().get(1).jobName());
+ assertEquals(JobType.productionUsEast3.jobName(), tester.buildSystem().jobs().get(0).jobName());
+ assertEquals(JobType.productionUsWest1.jobName(), tester.buildSystem().jobs().get(1).jobName());
tester.buildSystem().takeJobsToRun();
tester.deploy(JobType.productionUsWest1, application, applicationPackage, false);
@@ -269,9 +270,9 @@ public class DeploymentTriggerTest {
public void testBlockRevisionChange() {
ManualClock clock = new ManualClock(Instant.parse("2017-09-26T17:30:00.00Z")); // Tuesday, 17:30
DeploymentTester tester = new DeploymentTester(new ControllerTester(clock));
- BlockedChangeDeployer blockedChangeDeployer = new BlockedChangeDeployer(tester.controller(),
- Duration.ofHours(1),
- new JobControl(tester.controllerTester().curator()));
+ ReadyJobsTrigger readyJobsTrigger = new ReadyJobsTrigger(tester.controller(),
+ Duration.ofHours(1),
+ new JobControl(tester.controllerTester().curator()));
Version version = Version.fromString("5.0");
tester.updateVersionStatus(version);
@@ -290,7 +291,7 @@ public class DeploymentTriggerTest {
tester.clock().advance(Duration.ofHours(1)); // --------------- Enter block window: 18:30
- blockedChangeDeployer.run();
+ readyJobsTrigger.run();
assertEquals(0, tester.buildSystem().jobs().size());
String searchDefinition =
@@ -304,7 +305,7 @@ public class DeploymentTriggerTest {
tester.deployTestOnly(app, changedApplication);
- blockedChangeDeployer.run();
+ readyJobsTrigger.run();
assertEquals(0, tester.buildSystem().jobs().size());
tester.clock().advance(Duration.ofHours(2)); // ---------------- Exit block window: 20:30
@@ -317,14 +318,14 @@ public class DeploymentTriggerTest {
@Test
public void testUpgradingButNoJobStarted() {
DeploymentTester tester = new DeploymentTester();
- BlockedChangeDeployer blockedChangeDeployer = new BlockedChangeDeployer(tester.controller(),
- Duration.ofHours(1),
- new JobControl(tester.controllerTester().curator()));
+ ReadyJobsTrigger readyJobsTrigger = new ReadyJobsTrigger(tester.controller(),
+ Duration.ofHours(1),
+ new JobControl(tester.controllerTester().curator()));
LockedApplication app = (LockedApplication)tester.createAndDeploy("default0", 3, "default");
// Store that we are upgrading but don't start the system-tests job
tester.controller().applications().store(app.withDeploying(Optional.of(new Change.VersionChange(Version.fromString("6.2")))));
assertEquals(0, tester.buildSystem().jobs().size());
- blockedChangeDeployer.run();
+ readyJobsTrigger.run();
assertEquals(1, tester.buildSystem().jobs().size());
assertEquals("system-test", tester.buildSystem().jobs().get(0).jobName());
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/MockBuildService.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/MockBuildService.java
index 0293ea08d65..1b1a4feaa4e 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/MockBuildService.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/MockBuildService.java
@@ -13,6 +13,7 @@ import java.time.Duration;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;
+import java.util.Optional;
import java.util.function.Supplier;
import static com.yahoo.vespa.hosted.controller.deployment.MockBuildService.JobStatus.QUEUED;
@@ -161,11 +162,11 @@ public class MockBuildService implements BuildService {
jobType,
projectId,
42,
- JobError.from(success)
+ Optional.ofNullable(success ? null : JobError.unknown)
));
}
- private BuildJob buildJob() { return new BuildJob(projectId, jobType.id()); }
+ private BuildJob buildJob() { return new BuildJob(projectId, jobType.jobName()); }
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmerTest.java
new file mode 100644
index 00000000000..f5be882fcb8
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmerTest.java
@@ -0,0 +1,121 @@
+package com.yahoo.vespa.hosted.controller.maintenance;
+
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Environment;
+import com.yahoo.config.provision.RegionName;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.vespa.hosted.controller.Application;
+import com.yahoo.vespa.hosted.controller.ControllerTester;
+import com.yahoo.vespa.hosted.controller.api.Tenant;
+import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
+import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
+import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
+import com.yahoo.vespa.hosted.controller.api.integration.organization.OwnershipIssues;
+import com.yahoo.vespa.hosted.controller.api.integration.organization.User;
+import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
+import com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.time.Duration;
+import java.util.Optional;
+import java.util.function.Supplier;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author jvenstad
+ */
+public class ApplicationOwnershipConfirmerTest {
+
+ private MockOwnershipIssues issues;
+ private ApplicationOwnershipConfirmer confirmer;
+ private DeploymentTester tester;
+
+ @Before
+ public void setup() {
+ tester = new DeploymentTester();
+ issues = new MockOwnershipIssues();
+ confirmer = new ApplicationOwnershipConfirmer(tester.controller(), Duration.ofDays(1), new JobControl(new MockCuratorDb()), issues);
+ }
+
+ @Test
+ public void testConfirmation() {
+ TenantId property = tester.controllerTester().createTenant("property", "domain", 1L);
+ tester.createAndDeploy(property, "application", 1, "default");
+ Supplier<Application> propertyApp = () -> tester.controller().applications().require(ApplicationId.from("property", "application", "default"));
+
+ TenantId user = new TenantId("by-user");
+ tester.controller().tenants().addTenant(Tenant.createUserTenant(user), Optional.empty());
+ tester.createAndDeploy(user, "application", 2, "default");
+ Supplier<Application> userApp = () -> tester.controller().applications().require(ApplicationId.from("by-user", "application", "default"));
+
+ assertFalse("No issue is initially stored for a new application.", propertyApp.get().ownershipIssueId().isPresent());
+ assertFalse("No issue is initially stored for a new application.", userApp.get().ownershipIssueId().isPresent());
+ assertFalse("No escalation has been attempted for a new application", issues.escalatedForProperty || issues.escalatedForUser);
+
+ // Set response from the issue mock, which will be obtained by the maintainer on issue filing.
+ Optional<IssueId> issueId = Optional.of(IssueId.from("1"));
+ issues.response = issueId;
+ confirmer.maintain();
+ confirmer.maintain();
+
+ assertEquals("Confirmation issue has been filed for property owned application.", issueId, propertyApp.get().ownershipIssueId());
+ assertEquals("Confirmation issue has been filed for user owned application.", issueId, userApp.get().ownershipIssueId());
+ assertTrue("Both applications have had their responses ensured.", issues.escalatedForProperty && issues.escalatedForUser);
+
+ // No new issue is created, so return empty now.
+ issues.response = Optional.empty();
+ confirmer.maintain();
+ confirmer.maintain();
+
+ assertEquals("Confirmation issue reference is not updated when no issue id is returned.", issueId, propertyApp.get().ownershipIssueId());
+ assertEquals("Confirmation issue reference is not updated when no issue id is returned.", issueId, userApp.get().ownershipIssueId());
+
+ // The user deletes its production deployment — see that the issue is forgotten.
+ assertEquals("Confirmation issue for user is sitll open.", issueId, userApp.get().ownershipIssueId());
+ tester.controller().applications().deactivate(userApp.get(), userApp.get().productionDeployments().keySet().stream().findAny().get());
+ assertTrue("No production deployments are listed for user.", userApp.get().productionDeployments().isEmpty());
+ confirmer.maintain();
+ confirmer.maintain();
+
+ assertEquals("Confirmation issue has been forgotten for application without production deployments.", Optional.empty(), userApp.get().ownershipIssueId());
+
+ // Time has passed, and a new confirmation issue is in order for the property which is still in production.
+ Optional<IssueId> issueId2 = Optional.of(IssueId.from("2"));
+ issues.response = issueId2;
+ confirmer.maintain();
+ confirmer.maintain();
+
+ assertEquals("A new confirmation issue id is stored when something is returned to the maintainer.", issueId2, propertyApp.get().ownershipIssueId());
+ assertEquals("Confirmation issue for application without production deployments has not been filed.", Optional.empty(), userApp.get().ownershipIssueId());
+
+ }
+
+ private class MockOwnershipIssues implements OwnershipIssues {
+
+ private Optional<IssueId> response;
+ private boolean escalatedForProperty = false;
+ private boolean escalatedForUser = false;
+
+ @Override
+ public Optional<IssueId> confirmOwnership(Optional<IssueId> issueId, ApplicationId applicationId, PropertyId propertyId) {
+ return response;
+ }
+
+ @Override
+ public Optional<IssueId> confirmOwnership(Optional<IssueId> issueId, ApplicationId applicationId, User owner) {
+ return response;
+ }
+
+ @Override
+ public void ensureResponse(IssueId issueId, Optional<PropertyId> propertyId) {
+ if (propertyId.isPresent()) escalatedForProperty = true;
+ else escalatedForUser = true;
+ }
+
+ }
+
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporterTest.java
index b5ea8e0a36f..e57edcf6da0 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporterTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporterTest.java
@@ -54,7 +54,7 @@ public class DeploymentIssueReporterTest {
public void setup() {
tester = new DeploymentTester();
issues = new MockDeploymentIssues();
- reporter = new DeploymentIssueReporter(tester.controller(), issues, Duration.ofMinutes(5), new JobControl(new MockCuratorDb()));
+ reporter = new DeploymentIssueReporter(tester.controller(), issues, Duration.ofDays(1), new JobControl(new MockCuratorDb()));
}
@Test
@@ -135,9 +135,7 @@ public class DeploymentIssueReporterTest {
// app3 now has a new failure past max failure age; see that a new issue is filed.
tester.notifyJobCompletion(component, app3, true);
- tester.deployAndNotify(app3, applicationPackage, true, systemTest);
- tester.deployAndNotify(app3, applicationPackage, true, stagingTest);
- tester.deployAndNotify(app3, applicationPackage, false, productionCorpUsEast1);
+ tester.deployAndNotify(app3, applicationPackage, false, systemTest);
tester.clock().advance(maxInactivity.plus(maxFailureAge));
reporter.maintain();
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentMetricsMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentMetricsMaintainerTest.java
index d3907e27456..148d11e8b38 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentMetricsMaintainerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentMetricsMaintainerTest.java
@@ -5,14 +5,16 @@ package com.yahoo.vespa.hosted.controller.maintenance;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.Environment;
+import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.ControllerTester;
import com.yahoo.vespa.hosted.controller.application.Deployment;
import com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb;
-import org.junit.Assert;
import org.junit.Test;
import java.time.Duration;
+import static org.junit.Assert.assertEquals;
+
/**
* @author smorgrav
*/
@@ -23,20 +25,24 @@ public class DeploymentMetricsMaintainerTest {
ControllerTester tester = new ControllerTester();
ApplicationId app = tester.createAndDeploy("tenant1", "domain1", "app1", Environment.dev, 123).id();
- // Pre condition: no metric info on the deployment
+ // Pre condition: no metric info on neither application nor deployment
+ assertEquals(0, tester.controller().applications().require(app).metrics().queryServiceQuality(), 0);
Deployment deployment = tester.controller().applications().get(app).get().deployments().values().stream().findAny().get();
- Assert.assertEquals(0, deployment.metrics().documentCount(), Double.MIN_VALUE);
+ assertEquals(0, deployment.metrics().documentCount(), 0);
DeploymentMetricsMaintainer maintainer = new DeploymentMetricsMaintainer(tester.controller(), Duration.ofMinutes(10), new JobControl(new MockCuratorDb()));
maintainer.maintain();
// Post condition:
- deployment = tester.controller().applications().get(app).get().deployments().values().stream().findAny().get();
- Assert.assertEquals(1, deployment.metrics().queriesPerSecond(), Double.MIN_VALUE);
- Assert.assertEquals(2, deployment.metrics().writesPerSecond(), Double.MIN_VALUE);
- Assert.assertEquals(3, deployment.metrics().documentCount(), Double.MIN_VALUE);
- Assert.assertEquals(4, deployment.metrics().queryLatencyMillis(), Double.MIN_VALUE);
- Assert.assertEquals(5, deployment.metrics().writeLatencyMillis(), Double.MIN_VALUE);
+ Application application = tester.controller().applications().require(app);
+ assertEquals(0.5, application.metrics().queryServiceQuality(), Double.MIN_VALUE);
+ assertEquals(0.7, application.metrics().writeServiceQuality(), Double.MIN_VALUE);
+ deployment = application.deployments().values().stream().findAny().get();
+ assertEquals(1, deployment.metrics().queriesPerSecond(), Double.MIN_VALUE);
+ assertEquals(2, deployment.metrics().writesPerSecond(), Double.MIN_VALUE);
+ assertEquals(3, deployment.metrics().documentCount(), Double.MIN_VALUE);
+ assertEquals(4, deployment.metrics().queryLatencyMillis(), Double.MIN_VALUE);
+ assertEquals(5, deployment.metrics().writeLatencyMillis(), Double.MIN_VALUE);
}
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/FailureRedeployerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/FailureRedeployerTest.java
index 2782dd6ec3b..fd00123c697 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/FailureRedeployerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/FailureRedeployerTest.java
@@ -62,15 +62,16 @@ public class FailureRedeployerTest {
// Another version is released, which cancels any pending upgrades to lower versions
version = Version.fromString("5.2");
tester.updateVersionStatus(version);
+ tester.deployAndNotify(app, applicationPackage, true, DeploymentJobs.JobType.productionUsEast3); // Finish previous production job.
tester.upgrader().maintain();
assertEquals("Application starts upgrading to new version", 1, tester.buildSystem().jobs().size());
assertEquals("Application has pending upgrade to " + version, version, tester.versionChange(app.id()).get().version());
// Failure redeployer does not retry failing job for prod.us-east-3 as there's an ongoing deployment
tester.clock().advance(Duration.ofMinutes(1));
- tester.failureRedeployer().maintain();
+ tester.readyJobTrigger().maintain();
assertFalse("Job is not retried", tester.buildSystem().jobs().stream()
- .anyMatch(j -> j.jobName().equals(DeploymentJobs.JobType.productionUsEast3.id())));
+ .anyMatch(j -> j.jobName().equals(DeploymentJobs.JobType.productionUsEast3.jobName())));
// Test environments pass
tester.deployAndNotify(app, applicationPackage, true, DeploymentJobs.JobType.systemTest);
@@ -86,7 +87,7 @@ public class FailureRedeployerTest {
// Failure redeployer retries job
tester.clock().advance(Duration.ofMinutes(5));
- tester.failureRedeployer().maintain();
+ tester.readyJobTrigger().maintain();
assertEquals("Job is retried", 1, tester.buildSystem().jobs().size());
// Production job finally succeeds
@@ -109,14 +110,14 @@ public class FailureRedeployerTest {
tester.deployAndNotify(app, applicationPackage, true, DeploymentJobs.JobType.systemTest);
// staging-test starts, but does not complete
- assertEquals(DeploymentJobs.JobType.stagingTest.id(), tester.buildSystem().takeJobsToRun().get(0).jobName());
- tester.failureRedeployer().maintain();
+ assertEquals(DeploymentJobs.JobType.stagingTest.jobName(), tester.buildSystem().takeJobsToRun().get(0).jobName());
+ tester.readyJobTrigger().maintain();
assertTrue("No jobs retried", tester.buildSystem().jobs().isEmpty());
// Just over 12 hours pass, job is retried
tester.clock().advance(Duration.ofHours(12).plus(Duration.ofSeconds(1)));
- tester.failureRedeployer().maintain();
- assertEquals(DeploymentJobs.JobType.stagingTest.id(), tester.buildSystem().takeJobsToRun().get(0).jobName());
+ tester.readyJobTrigger().maintain();
+ assertEquals(DeploymentJobs.JobType.stagingTest.jobName(), tester.buildSystem().takeJobsToRun().get(0).jobName());
// Deployment completes
tester.deploy(DeploymentJobs.JobType.stagingTest, app, applicationPackage, true);
@@ -168,7 +169,7 @@ public class FailureRedeployerTest {
// Failure re-deployer does not retry failing system-test job as it failed for an older change
tester.clock().advance(Duration.ofMinutes(5));
- tester.failureRedeployer().maintain();
+ tester.readyJobTrigger().maintain();
assertTrue("No jobs retried", tester.buildSystem().jobs().isEmpty());
}
@@ -212,11 +213,11 @@ public class FailureRedeployerTest {
// Production job starts, but does not complete
assertEquals(1, tester.buildSystem().jobs().size());
- assertEquals("Production job triggered", DeploymentJobs.JobType.productionCdUsCentral1.id(), tester.buildSystem().jobs().get(0).jobName());
+ assertEquals("Production job triggered", DeploymentJobs.JobType.productionCdUsCentral1.jobName(), tester.buildSystem().jobs().get(0).jobName());
tester.buildSystem().takeJobsToRun();
// Failure re-deployer runs
- tester.failureRedeployer().maintain();
+ tester.readyJobTrigger().maintain();
assertTrue("No jobs retried", tester.buildSystem().jobs().isEmpty());
// Deployment completes
@@ -241,7 +242,7 @@ public class FailureRedeployerTest {
Application application = tester.controllerTester().createApplication(slime);
// Failure redeployer does not restart deployment
- tester.failureRedeployer().maintain();
+ tester.readyJobTrigger().maintain();
assertTrue("No jobs scheduled", tester.buildSystem().jobs().isEmpty());
}
@@ -261,7 +262,7 @@ public class FailureRedeployerTest {
tester.controllerTester().createApplication(slime);
// Failure redeployer does not restart deployment
- tester.failureRedeployer().maintain();
+ tester.readyJobTrigger().maintain();
assertTrue("No jobs scheduled", tester.buildSystem().jobs().isEmpty());
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporterTest.java
index 621e189ba37..a7458f9f8ed 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporterTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporterTest.java
@@ -71,7 +71,7 @@ public class MetricsReporterTest {
metricsReporter.maintain();
assertEquals(0.0, metricsMock.getMetric(MetricsReporter.deploymentFailMetric));
- // Deploy 3 apps successfully
+ // Deploy all apps successfully
Application app1 = tester.createApplication("app1", "tenant1", 1, 11L);
Application app2 = tester.createApplication("app2", "tenant1", 2, 22L);
Application app3 = tester.createApplication("app3", "tenant1", 3, 33L);
@@ -79,6 +79,7 @@ public class MetricsReporterTest {
tester.deployCompletely(app1, applicationPackage);
tester.deployCompletely(app2, applicationPackage);
tester.deployCompletely(app3, applicationPackage);
+ tester.deployCompletely(app4, applicationPackage);
metricsReporter.maintain();
assertEquals(0.0, metricsMock.getMetric(MetricsReporter.deploymentFailMetric));
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OutstandingChangeDeployerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OutstandingChangeDeployerTest.java
index 4886eba40b6..13636122cfd 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OutstandingChangeDeployerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OutstandingChangeDeployerTest.java
@@ -49,7 +49,7 @@ public class OutstandingChangeDeployerTest {
List<BuildService.BuildJob> jobs = tester.buildSystem().jobs();
assertEquals(1, jobs.size());
assertEquals(11, jobs.get(0).projectId());
- assertEquals(DeploymentJobs.JobType.systemTest.id(), jobs.get(0).jobName());
+ assertEquals(DeploymentJobs.JobType.systemTest.jobName(), jobs.get(0).jobName());
assertFalse(tester.application("app1").hasOutstandingChange());
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/UpgraderTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/UpgraderTest.java
index 0414cda3f55..8839f6a5a18 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/UpgraderTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/UpgraderTest.java
@@ -51,7 +51,7 @@ public class UpgraderTest {
tester.upgrader().maintain();
assertEquals("All already on the right version: Nothing to do", 0, tester.buildSystem().jobs().size());
- // --- A new version is released - everything goes smoothly
+ // --- 5.1 is released - everything goes smoothly
version = Version.fromString("5.1");
tester.updateVersionStatus(version);
assertEquals(version, tester.controller().versionStatus().systemVersion().get().versionNumber());
@@ -86,7 +86,7 @@ public class UpgraderTest {
tester.upgrader().maintain();
assertEquals("Nothing to do", 0, tester.buildSystem().jobs().size());
- // --- A new version is released - which fails a Canary
+ // --- 5.2 is released - which fails a Canary
version = Version.fromString("5.2");
tester.updateVersionStatus(version);
assertEquals(version, tester.controller().versionStatus().systemVersion().get().versionNumber());
@@ -95,12 +95,23 @@ public class UpgraderTest {
assertEquals("New system version: Should upgrade Canaries", 2, tester.buildSystem().jobs().size());
tester.completeUpgradeWithError(canary0, version, "canary", DeploymentJobs.JobType.stagingTest);
assertEquals("Other Canary was cancelled", 2, tester.buildSystem().jobs().size());
+ // TODO: Cancelled would mean it was triggerd, removed from the build system, but never reported in.
+ // Thus, the expected number of jobs should be 1, above: the retrying canary0.
+ // Further, canary1 should be retried after the timeout period of 12 hours, but verifying this is
+ // not possible when jobs are consumed form the build system on notification, rather than on deploy.
tester.updateVersionStatus(version);
assertEquals(VespaVersion.Confidence.broken, tester.controller().versionStatus().systemVersion().get().confidence());
tester.upgrader().maintain();
assertEquals("Version broken, but Canaries should keep trying", 2, tester.buildSystem().jobs().size());
+ // Exhaust canary retries.
+ tester.notifyJobCompletion(DeploymentJobs.JobType.systemTest, canary1, false);
+ tester.clock().advance(Duration.ofHours(1));
+ tester.deployAndNotify(canary0, DeploymentTester.applicationPackage("canary"), false, DeploymentJobs.JobType.stagingTest);
+ tester.notifyJobCompletion(DeploymentJobs.JobType.systemTest, canary1, false);
+ //tester.deployAndNotify(canary1, DeploymentTester.applicationPackage("canary"), false, DeploymentJobs.JobType.stagingTest);
+
// --- A new version is released - which repairs the Canary app and fails a default
version = Version.fromString("5.3");
tester.updateVersionStatus(version);
@@ -128,11 +139,15 @@ public class UpgraderTest {
tester.completeUpgrade(default2, version, "default");
tester.updateVersionStatus(version);
- assertEquals("Not enough evidence to mark this neither broken nor high",
+ assertEquals("Not enough evidence to mark this as neither broken nor high",
VespaVersion.Confidence.normal, tester.controller().versionStatus().systemVersion().get().confidence());
- tester.upgrader().maintain();
+
assertEquals("Upgrade with error should retry", 1, tester.buildSystem().jobs().size());
+ // Finish previous run, with exhausted retry.
+ tester.clock().advance(Duration.ofHours(1));
+ tester.notifyJobCompletion(DeploymentJobs.JobType.stagingTest, default0, false);
+
// --- Failing application is repaired by changing the application, causing confidence to move above 'high' threshold
// Deploy application change
tester.deployCompletely("default0");
@@ -148,51 +163,59 @@ public class UpgraderTest {
assertEquals("Applications are on 5.3 - nothing to do", 0, tester.buildSystem().jobs().size());
// --- Starting upgrading to a new version which breaks, causing upgrades to commence on the previous version
- version = Version.fromString("5.4");
+ Version version54 = Version.fromString("5.4");
Application default3 = tester.createAndDeploy("default3", 5, "default"); // need 4 to break a version
Application default4 = tester.createAndDeploy("default4", 5, "default");
- tester.updateVersionStatus(version);
+ tester.updateVersionStatus(version54);
tester.upgrader().maintain(); // cause canary upgrades to 5.4
- tester.completeUpgrade(canary0, version, "canary");
- tester.completeUpgrade(canary1, version, "canary");
- tester.updateVersionStatus(version);
+ tester.completeUpgrade(canary0, version54, "canary");
+ tester.completeUpgrade(canary1, version54, "canary");
+ tester.updateVersionStatus(version54);
assertEquals(VespaVersion.Confidence.normal, tester.controller().versionStatus().systemVersion().get().confidence());
tester.upgrader().maintain();
assertEquals("Upgrade of defaults are scheduled", 5, tester.buildSystem().jobs().size());
- assertEquals(version, ((Change.VersionChange)tester.application(default0.id()).deploying().get()).version());
- assertEquals(version, ((Change.VersionChange)tester.application(default1.id()).deploying().get()).version());
- assertEquals(version, ((Change.VersionChange)tester.application(default2.id()).deploying().get()).version());
- assertEquals(version, ((Change.VersionChange)tester.application(default3.id()).deploying().get()).version());
- assertEquals(version, ((Change.VersionChange)tester.application(default4.id()).deploying().get()).version());
- tester.completeUpgrade(default0, version, "default");
+ assertEquals(version54, ((Change.VersionChange)tester.application(default0.id()).deploying().get()).version());
+ assertEquals(version54, ((Change.VersionChange)tester.application(default1.id()).deploying().get()).version());
+ assertEquals(version54, ((Change.VersionChange)tester.application(default2.id()).deploying().get()).version());
+ assertEquals(version54, ((Change.VersionChange)tester.application(default3.id()).deploying().get()).version());
+ assertEquals(version54, ((Change.VersionChange)tester.application(default4.id()).deploying().get()).version());
+ tester.completeUpgrade(default0, version54, "default");
// State: Default applications started upgrading to 5.4 (and one completed)
- version = Version.fromString("5.5");
- tester.updateVersionStatus(version);
+ Version version55 = Version.fromString("5.5");
+ tester.updateVersionStatus(version55);
tester.upgrader().maintain(); // cause canary upgrades to 5.5
- tester.completeUpgrade(canary0, version, "canary");
- tester.completeUpgrade(canary1, version, "canary");
- tester.updateVersionStatus(version);
+ tester.completeUpgrade(canary0, version55, "canary");
+ tester.completeUpgrade(canary1, version55, "canary");
+ tester.updateVersionStatus(version55);
assertEquals(VespaVersion.Confidence.normal, tester.controller().versionStatus().systemVersion().get().confidence());
tester.upgrader().maintain();
assertEquals("Upgrade of defaults are scheduled", 5, tester.buildSystem().jobs().size());
- assertEquals(version, ((Change.VersionChange)tester.application(default0.id()).deploying().get()).version());
- assertEquals(version, ((Change.VersionChange)tester.application(default1.id()).deploying().get()).version());
- assertEquals(version, ((Change.VersionChange)tester.application(default2.id()).deploying().get()).version());
- assertEquals(version, ((Change.VersionChange)tester.application(default3.id()).deploying().get()).version());
- assertEquals(version, ((Change.VersionChange)tester.application(default4.id()).deploying().get()).version());
+ assertEquals(version55, ((Change.VersionChange)tester.application(default0.id()).deploying().get()).version());
+ assertEquals(version54, ((Change.VersionChange)tester.application(default1.id()).deploying().get()).version());
+ assertEquals(version54, ((Change.VersionChange)tester.application(default2.id()).deploying().get()).version());
+ assertEquals(version54, ((Change.VersionChange)tester.application(default3.id()).deploying().get()).version());
+ assertEquals(version54, ((Change.VersionChange)tester.application(default4.id()).deploying().get()).version());
+ tester.completeUpgrade(default1, version54, "default");
+ tester.completeUpgrade(default2, version54, "default");
+ tester.completeUpgradeWithError(default3, version54, "default", DeploymentJobs.JobType.stagingTest);
+ tester.completeUpgradeWithError(default4, version54, "default", DeploymentJobs.JobType.productionUsWest1);
// State: Default applications started upgrading to 5.5
- tester.completeUpgradeWithError(default0, version, "default", DeploymentJobs.JobType.stagingTest);
- tester.completeUpgradeWithError(default1, version, "default", DeploymentJobs.JobType.stagingTest);
- tester.completeUpgradeWithError(default2, version, "default", DeploymentJobs.JobType.stagingTest);
- tester.completeUpgradeWithError(default3, version, "default", DeploymentJobs.JobType.productionUsWest1);
- tester.completeUpgrade(default4, version, "default");
- tester.updateVersionStatus(version);
+ tester.upgrader().maintain();
+ tester.completeUpgradeWithError(default0, version55, "default", DeploymentJobs.JobType.stagingTest);
+ tester.completeUpgradeWithError(default1, version55, "default", DeploymentJobs.JobType.stagingTest);
+ tester.completeUpgradeWithError(default2, version55, "default", DeploymentJobs.JobType.stagingTest);
+ tester.completeUpgradeWithError(default3, version55, "default", DeploymentJobs.JobType.productionUsWest1);
+ tester.updateVersionStatus(version55);
assertEquals(VespaVersion.Confidence.broken, tester.controller().versionStatus().systemVersion().get().confidence());
+
+ // Finish running job, without retry.
+ tester.clock().advance(Duration.ofHours(1));
+ tester.notifyJobCompletion(DeploymentJobs.JobType.productionUsWest1, default3, false);
+
tester.upgrader().maintain();
- assertEquals("Upgrade of defaults are scheduled on 5.4 instead, since 5.5 broken",
- 3, tester.buildSystem().jobs().size());
- assertEquals("5.4", ((Change.VersionChange)tester.application(default1.id()).deploying().get()).version().toString());
- assertEquals("5.4", ((Change.VersionChange)tester.application(default2.id()).deploying().get()).version().toString());
+ assertEquals("Upgrade of defaults are scheduled on 5.4 instead, since 5.5 broken: " +
+ "This is default3 since it failed upgrade on both 5.4 and 5.5",
+ 1, tester.buildSystem().jobs().size());
assertEquals("5.4", ((Change.VersionChange)tester.application(default3.id()).deploying().get()).version().toString());
}
@@ -451,9 +474,9 @@ public class UpgraderTest {
public void testBlockVersionChangeHalfwayThough() {
ManualClock clock = new ManualClock(Instant.parse("2017-09-26T17:00:00.00Z")); // Tuesday, 17:00
DeploymentTester tester = new DeploymentTester(new ControllerTester(clock));
- BlockedChangeDeployer blockedChangeDeployer = new BlockedChangeDeployer(tester.controller(),
- Duration.ofHours(1),
- new JobControl(tester.controllerTester().curator()));
+ ReadyJobsTrigger readyJobsTrigger = new ReadyJobsTrigger(tester.controller(),
+ Duration.ofHours(1),
+ new JobControl(tester.controllerTester().curator()));
Version version = Version.fromString("5.0");
tester.updateVersionStatus(version);
@@ -483,12 +506,12 @@ public class UpgraderTest {
// One hour passes, time is 19:00, still no upgrade
tester.clock().advance(Duration.ofHours(1));
- blockedChangeDeployer.maintain();
+ readyJobsTrigger.maintain();
assertTrue("No jobs scheduled", tester.buildSystem().jobs().isEmpty());
// Another hour pass, time is 20:00 and application upgrades
tester.clock().advance(Duration.ofHours(1));
- blockedChangeDeployer.maintain();
+ readyJobsTrigger.maintain();
tester.deployAndNotify(app, applicationPackage, true, DeploymentJobs.JobType.productionUsCentral1);
tester.deployAndNotify(app, applicationPackage, true, DeploymentJobs.JobType.productionUsEast3);
assertTrue("All jobs consumed", tester.buildSystem().jobs().isEmpty());
@@ -505,9 +528,9 @@ public class UpgraderTest {
public void testBlockVersionChangeHalfwayThoughThenNewVersion() {
ManualClock clock = new ManualClock(Instant.parse("2017-09-29T16:00:00.00Z")); // Friday, 16:00
DeploymentTester tester = new DeploymentTester(new ControllerTester(clock));
- BlockedChangeDeployer blockedChangeDeployer = new BlockedChangeDeployer(tester.controller(),
- Duration.ofHours(1),
- new JobControl(tester.controllerTester().curator()));
+ ReadyJobsTrigger readyJobsTrigger = new ReadyJobsTrigger(tester.controller(),
+ Duration.ofHours(1),
+ new JobControl(tester.controllerTester().curator()));
Version version = Version.fromString("5.0");
tester.updateVersionStatus(version);
@@ -542,14 +565,14 @@ public class UpgraderTest {
version = Version.fromString("5.2");
tester.updateVersionStatus(version);
tester.upgrader().maintain();
- blockedChangeDeployer.maintain();
+ readyJobsTrigger.maintain();
assertTrue("Nothing is scheduled", tester.buildSystem().jobs().isEmpty());
// Monday morning: We are not blocked
tester.clock().advance(Duration.ofDays(1)); // Sunday, 17:00
tester.clock().advance(Duration.ofHours(17)); // Monday, 10:00
tester.upgrader().maintain();
- blockedChangeDeployer.maintain();
+ readyJobsTrigger.maintain();
// We proceed with the new version in the expected order, not starting with the previously blocked version:
// Test jobs are run with the new version, but not production as we are in the block window
tester.deployAndNotify(app, applicationPackage, true, DeploymentJobs.JobType.systemTest);
@@ -584,7 +607,7 @@ public class UpgraderTest {
Application default3 = tester.createAndDeploy("default3", 6, "default");
Application default4 = tester.createAndDeploy("default4", 7, "default");
- assertEquals(version, default0.deployedVersion().get());
+ assertEquals(version, default0.oldestDeployedVersion().get());
// New version is released
version = Version.fromString("5.1");
@@ -610,8 +633,16 @@ public class UpgraderTest {
tester.completeUpgradeWithError(default3, version, "default", DeploymentJobs.JobType.systemTest);
tester.updateVersionStatus(version);
assertEquals(VespaVersion.Confidence.broken, tester.controller().versionStatus().systemVersion().get().confidence());
+
tester.upgrader().maintain();
+ // Exhaust retries and finish runs
+ tester.clock().advance(Duration.ofHours(1));
+ tester.notifyJobCompletion(DeploymentJobs.JobType.systemTest, default0, false);
+ tester.notifyJobCompletion(DeploymentJobs.JobType.systemTest, default1, false);
+ tester.notifyJobCompletion(DeploymentJobs.JobType.systemTest, default2, false);
+ tester.notifyJobCompletion(DeploymentJobs.JobType.systemTest, default3, false);
+
// 5th app never reports back and has a dead job, but no ongoing change
Application deadLocked = tester.applications().require(default4.id());
assertTrue("Jobs in progress", deadLocked.deploymentJobs().isRunning(tester.controller().applications().deploymentTrigger().jobTimeoutLimit()));
@@ -633,20 +664,10 @@ public class UpgraderTest {
tester.completeUpgrade(default2, version, "default");
tester.completeUpgrade(default3, version, "default");
- assertEquals(version, tester.application(default0.id()).deployedVersion().get());
- assertEquals(version, tester.application(default1.id()).deployedVersion().get());
- assertEquals(version, tester.application(default2.id()).deployedVersion().get());
- assertEquals(version, tester.application(default3.id()).deployedVersion().get());
-
- // Over 12 hours pass and upgrade is rescheduled for 5th app
- assertEquals(0, tester.buildSystem().jobs().size());
- tester.clock().advance(Duration.ofHours(12).plus(Duration.ofSeconds(1)));
- tester.upgrader().maintain();
- assertEquals(1, tester.buildSystem().jobs().size());
- assertEquals("Upgrade is rescheduled", DeploymentJobs.JobType.systemTest.id(),
- tester.buildSystem().jobs().get(0).jobName());
- tester.deployCompletely(default4, applicationPackage);
- assertEquals(version, tester.application(default4.id()).deployedVersion().get());
+ assertEquals(version, tester.application(default0.id()).oldestDeployedVersion().get());
+ assertEquals(version, tester.application(default1.id()).oldestDeployedVersion().get());
+ assertEquals(version, tester.application(default2.id()).oldestDeployedVersion().get());
+ assertEquals(version, tester.application(default3.id()).oldestDeployedVersion().get());
}
@Test
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/testdata/pr-instance-with-dead-locked-job.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/testdata/pr-instance-with-dead-locked-job.json
index 32d34edd576..425b9d4512d 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/testdata/pr-instance-with-dead-locked-job.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/testdata/pr-instance-with-dead-locked-job.json
@@ -4,7 +4,7 @@
"validationOverrides": "<deployment version='1.0'/>",
"deployments": [],
"deploymentJobs": {
- "projectId": 0,
+ "projectId": 42,
"jobStatus": [
{
"jobType": "system-test",
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java
index b38a38c3120..2c1471b29b6 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java
@@ -13,6 +13,8 @@ import com.yahoo.slime.Slime;
import com.yahoo.vespa.config.SlimeUtils;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.ControllerTester;
+import com.yahoo.vespa.hosted.controller.api.integration.MetricsService;
+import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
import com.yahoo.vespa.hosted.controller.application.ApplicationRevision;
import com.yahoo.vespa.hosted.controller.application.Change;
import com.yahoo.vespa.hosted.controller.application.ClusterInfo;
@@ -69,10 +71,10 @@ public class ApplicationSerializerTest {
List<JobStatus> statusList = new ArrayList<>();
statusList.add(JobStatus.initial(DeploymentJobs.JobType.systemTest)
- .withTriggering(37, Version.fromString("5.6.7"), Optional.empty(), true, "Test", Instant.ofEpochMilli(7))
+ .withTriggering(Version.fromString("5.6.7"), Optional.empty(), true, "Test", Instant.ofEpochMilli(7))
.withCompletion(30, Optional.empty(), Instant.ofEpochMilli(8), tester.controller()));
statusList.add(JobStatus.initial(DeploymentJobs.JobType.stagingTest)
- .withTriggering(12, Version.fromString("5.6.6"), Optional.empty(), true, "Test 2", Instant.ofEpochMilli(5))
+ .withTriggering(Version.fromString("5.6.6"), Optional.empty(), true, "Test 2", Instant.ofEpochMilli(5))
.withCompletion(11, Optional.of(JobError.unknown), Instant.ofEpochMilli(6), tester.controller()));
DeploymentJobs deploymentJobs = new DeploymentJobs(projectId, statusList, Optional.empty());
@@ -82,7 +84,9 @@ public class ApplicationSerializerTest {
validationOverrides,
deployments, deploymentJobs,
Optional.of(new Change.VersionChange(Version.fromString("6.7"))),
- true);
+ true,
+ Optional.of(IssueId.from("1234")),
+ new MetricsService.ApplicationMetrics(0.5, 0.9));
Application serialized = applicationSerializer.fromSlime(applicationSerializer.toSlime(original));
@@ -105,10 +109,11 @@ public class ApplicationSerializerTest {
serialized.deploymentJobs().jobStatus().get(DeploymentJobs.JobType.systemTest));
assertEquals( original.deploymentJobs().jobStatus().get(DeploymentJobs.JobType.stagingTest),
serialized.deploymentJobs().jobStatus().get(DeploymentJobs.JobType.stagingTest));
- assertEquals(original.deploymentJobs().failingSince(), serialized.deploymentJobs().failingSince());
assertEquals(original.hasOutstandingChange(), serialized.hasOutstandingChange());
+ assertEquals(original.ownershipIssueId(), serialized.ownershipIssueId());
+
assertEquals(original.deploying(), serialized.deploying());
// Test cluster utilization
@@ -129,6 +134,9 @@ public class ApplicationSerializerTest {
assertEquals(50, serialized.deployments().get(zone2).clusterInfo().get(ClusterSpec.Id.from("id2")).getFlavorDisk(), Double.MIN_VALUE);
// Test metrics
+ assertEquals(original.metrics().queryServiceQuality(), serialized.metrics().queryServiceQuality(), Double.MIN_VALUE);
+ assertEquals(original.metrics().writeServiceQuality(), serialized.metrics().writeServiceQuality(), Double.MIN_VALUE);
+
assertEquals(2, serialized.deployments().get(zone2).metrics().queriesPerSecond(), Double.MIN_VALUE);
assertEquals(3, serialized.deployments().get(zone2).metrics().writesPerSecond(), Double.MIN_VALUE);
assertEquals(4, serialized.deployments().get(zone2).metrics().documentCount(), Double.MIN_VALUE);
@@ -199,14 +207,11 @@ public class ApplicationSerializerTest {
Application application = applicationSerializer.fromSlime(applicationSlime(false));
assertFalse(application.deploymentJobs().jobStatus().get(DeploymentJobs.JobType.systemTest).lastCompleted().get().upgrade());
}
-
- // TODO: Remove after October 2017
+
@Test
- public void testLegacySerializationWithZeroProjectId() {
- Application original = applicationSerializer.fromSlime(applicationSlime(0, false));
- assertFalse(original.deploymentJobs().projectId().isPresent());
- Application serialized = applicationSerializer.fromSlime(applicationSerializer.toSlime(original));
- assertFalse(serialized.deploymentJobs().projectId().isPresent());
+ public void testCompleteApplicationDeserialization() {
+ Application application = applicationSerializer.fromSlime(SlimeUtils.jsonToSlime(longApplicationJson.getBytes(StandardCharsets.UTF_8)));
+ // ok if no error
}
private Slime applicationSlime(boolean error) {
@@ -245,4 +250,6 @@ public class ApplicationSerializerTest {
" }\n" +
"}\n";
}
+
+ private final String longApplicationJson = "{\"id\":\"tripod:service-aggregation-vespa:default\",\"deploymentSpecField\":\"<deployment version='1.0'>\\n <test />\\n <!--<staging />-->\\n <prod global-service-id=\\\"tripod\\\">\\n <region active=\\\"true\\\">us-east-3</region>\\n <region active=\\\"true\\\">us-west-1</region>\\n </prod>\\n</deployment>\\n\",\"validationOverrides\":\"<validation-overrides>\\n <allow until=\\\"2016-04-28\\\" comment=\\\"Renaming content cluster\\\">content-cluster-removal</allow>\\n <allow until=\\\"2016-08-22\\\" comment=\\\"Migrating us-east-3 to C-2E\\\">cluster-size-reduction</allow>\\n <allow until=\\\"2017-06-30\\\" comment=\\\"Test Vespa upgrade tests\\\">force-automatic-tenant-upgrade-test</allow>\\n</validation-overrides>\\n\",\"deployments\":[{\"zone\":{\"environment\":\"prod\",\"region\":\"us-west-1\"},\"version\":\"6.173.62\",\"deployTime\":1510837817704,\"applicationPackageRevision\":{\"applicationPackageHash\":\"9db423e1021d7b452d37ec6372bc757d9c1bda87\",\"sourceRevision\":{\"repositoryField\":\"git@git.ouroath.com:Tripod/service-aggregation-vespa.git\",\"branchField\":\"origin/master\",\"commitField\":\"49cd7bbb1ed9f4b922083cb042590b0885ffe22b\"}},\"clusterInfo\":{\"tripod\":{\"flavor\":\"d-3-16-100\",\"cost\":9,\"flavorCpu\":0,\"flavorMem\":0,\"flavorDisk\":0,\"clusterType\":\"container\",\"hostnames\":[\"oxy-oxygen-2001-4998-c-2942--10d1.gq1.yahoo.com\",\"oxy-oxygen-2001-4998-c-2942--10e2.gq1.yahoo.com\"]},\"tripodaggregation\":{\"flavor\":\"d-12-64-400\",\"cost\":38,\"flavorCpu\":0,\"flavorMem\":0,\"flavorDisk\":0,\"clusterType\":\"content\",\"hostnames\":[\"oxy-oxygen-2001-4998-c-2941--106a.gq1.yahoo.com\",\"zt74700-v6-23.ostk.bm2.prod.gq1.yahoo.com\",\"zt74714-v6-28.ostk.bm2.prod.gq1.yahoo.com\",\"zt74730-v6-13.ostk.bm2.prod.gq1.yahoo.com\",\"zt74717-v6-7.ostk.bm2.prod.gq1.yahoo.com\",\"2080260-v6-12.ostk.bm2.prod.gq1.yahoo.com\",\"zt74719-v6-23.ostk.bm2.prod.gq1.yahoo.com\",\"zt74722-v6-26.ostk.bm2.prod.gq1.yahoo.com\",\"zt74704-v6-9.ostk.bm2.prod.gq1.yahoo.com\",\"oxy-oxygen-2001-4998-c-2942--107d.gq1.yahoo.com\"]},\"tripodaggregationstream\":{\"flavor\":\"d-12-64-400\",\"cost\":38,\"flavorCpu\":0,\"flavorMem\":0,\"flavorDisk\":0,\"clusterType\":\"content\",\"hostnames\":[\"zt74727-v6-21.ostk.bm2.prod.gq1.yahoo.com\",\"zt74773-v6-8.ostk.bm2.prod.gq1.yahoo.com\",\"zt74699-v6-25.ostk.bm2.prod.gq1.yahoo.com\",\"zt74766-v6-27.ostk.bm2.prod.gq1.yahoo.com\"]}},\"clusterUtils\":{\"tripod\":{\"cpu\":0.1720353499228221,\"mem\":0.4986146831512451,\"disk\":0.0617671330041831,\"diskbusy\":0},\"tripodaggregation\":{\"cpu\":0.07505730001866318,\"mem\":0.7936344432830811,\"disk\":0.2260549694485994,\"diskbusy\":0},\"tripodaggregationstream\":{\"cpu\":0.01712671480989384,\"mem\":0.0225852754983035,\"disk\":0.006084436856721915,\"diskbusy\":0}},\"metrics\":{\"queriesPerSecond\":1.25,\"writesPerSecond\":43.83199977874756,\"documentCount\":525880277.9999999,\"queryLatencyMillis\":5.607503938674927,\"writeLatencyMillis\":20.57866265104621}},{\"zone\":{\"environment\":\"test\",\"region\":\"us-east-1\"},\"version\":\"6.173.62\",\"deployTime\":1511256872316,\"applicationPackageRevision\":{\"applicationPackageHash\":\"ec548fa61cbfab7a270a51d46b1263ec1be5d9a8\",\"sourceRevision\":{\"repositoryField\":\"git@git.ouroath.com:Tripod/service-aggregation-vespa.git\",\"branchField\":\"origin/master\",\"commitField\":\"234f3e4e77049d0b9538c9e1b356d17eb1dedb6a\"}},\"clusterInfo\":{},\"clusterUtils\":{},\"metrics\":{\"queriesPerSecond\":0,\"writesPerSecond\":0,\"documentCount\":0,\"queryLatencyMillis\":0,\"writeLatencyMillis\":0}},{\"zone\":{\"environment\":\"dev\",\"region\":\"us-east-1\"},\"version\":\"6.173.62\",\"deployTime\":1510597489464,\"applicationPackageRevision\":{\"applicationPackageHash\":\"59b883f263c2a3c23dfab249730097d7e0e1ed32\"},\"clusterInfo\":{\"tripod\":{\"flavor\":\"d-2-8-50\",\"cost\":5,\"flavorCpu\":0,\"flavorMem\":0,\"flavorDisk\":0,\"clusterType\":\"container\",\"hostnames\":[\"zt40807-v6-29.ostk.bm2.prod.bf1.yahoo.com\"]},\"tripodaggregation\":{\"flavor\":\"d-2-8-50\",\"cost\":5,\"flavorCpu\":0,\"flavorMem\":0,\"flavorDisk\":0,\"clusterType\":\"content\",\"hostnames\":[\"zt40807-v6-24.ostk.bm2.prod.bf1.yahoo.com\"]},\"tripodaggregationstream\":{\"flavor\":\"d-2-8-50\",\"cost\":5,\"flavorCpu\":0,\"flavorMem\":0,\"flavorDisk\":0,\"clusterType\":\"content\",\"hostnames\":[\"zt40694-v6-21.ostk.bm2.prod.bf1.yahoo.com\"]}},\"clusterUtils\":{\"tripod\":{\"cpu\":0.191833330678661,\"mem\":0.4625738318415235,\"disk\":0.05582004563850269,\"diskbusy\":0},\"tripodaggregation\":{\"cpu\":0.2227037978608054,\"mem\":0.2051752598416401,\"disk\":0.05471533698695047,\"diskbusy\":0},\"tripodaggregationstream\":{\"cpu\":0.1869410834020498,\"mem\":0.1691722576000564,\"disk\":0.04977374774258153,\"diskbusy\":0}},\"metrics\":{\"queriesPerSecond\":0,\"writesPerSecond\":0,\"documentCount\":30916,\"queryLatencyMillis\":0,\"writeLatencyMillis\":0}},{\"zone\":{\"environment\":\"prod\",\"region\":\"us-east-3\"},\"version\":\"6.173.62\",\"deployTime\":1510817190016,\"applicationPackageRevision\":{\"applicationPackageHash\":\"9db423e1021d7b452d37ec6372bc757d9c1bda87\",\"sourceRevision\":{\"repositoryField\":\"git@git.ouroath.com:Tripod/service-aggregation-vespa.git\",\"branchField\":\"origin/master\",\"commitField\":\"49cd7bbb1ed9f4b922083cb042590b0885ffe22b\"}},\"clusterInfo\":{\"tripod\":{\"flavor\":\"d-3-16-100\",\"cost\":9,\"flavorCpu\":0,\"flavorMem\":0,\"flavorDisk\":0,\"clusterType\":\"container\",\"hostnames\":[\"zt40738-v6-13.ostk.bm2.prod.bf1.yahoo.com\",\"zt40783-v6-31.ostk.bm2.prod.bf1.yahoo.com\"]},\"tripodaggregation\":{\"flavor\":\"d-12-64-400\",\"cost\":38,\"flavorCpu\":0,\"flavorMem\":0,\"flavorDisk\":0,\"clusterType\":\"content\",\"hostnames\":[\"zt40819-v6-7.ostk.bm2.prod.bf1.yahoo.com\",\"zt40661-v6-3.ostk.bm2.prod.bf1.yahoo.com\",\"zt40805-v6-30.ostk.bm2.prod.bf1.yahoo.com\",\"zt40702-v6-32.ostk.bm2.prod.bf1.yahoo.com\",\"zt40706-v6-3.ostk.bm2.prod.bf1.yahoo.com\",\"zt40691-v6-27.ostk.bm2.prod.bf1.yahoo.com\",\"zt40676-v6-15.ostk.bm2.prod.bf1.yahoo.com\",\"zt40788-v6-23.ostk.bm2.prod.bf1.yahoo.com\",\"zt40782-v6-30.ostk.bm2.prod.bf1.yahoo.com\",\"zt40802-v6-32.ostk.bm2.prod.bf1.yahoo.com\"]},\"tripodaggregationstream\":{\"flavor\":\"d-12-64-400\",\"cost\":38,\"flavorCpu\":0,\"flavorMem\":0,\"flavorDisk\":0,\"clusterType\":\"content\",\"hostnames\":[\"zt40779-v6-27.ostk.bm2.prod.bf1.yahoo.com\",\"zt40791-v6-15.ostk.bm2.prod.bf1.yahoo.com\",\"zt40733-v6-31.ostk.bm2.prod.bf1.yahoo.com\",\"zt40724-v6-30.ostk.bm2.prod.bf1.yahoo.com\"]}},\"clusterUtils\":{\"tripod\":{\"cpu\":0.2295038983007097,\"mem\":0.4627357390237263,\"disk\":0.05559941525894966,\"diskbusy\":0},\"tripodaggregation\":{\"cpu\":0.05340429087579549,\"mem\":0.8107630891552372,\"disk\":0.226444914138854,\"diskbusy\":0},\"tripodaggregationstream\":{\"cpu\":0.02148227413975218,\"mem\":0.02162174219104161,\"disk\":0.006057760545243265,\"diskbusy\":0}},\"metrics\":{\"queriesPerSecond\":1.734000012278557,\"writesPerSecond\":44.59999895095825,\"documentCount\":525868193.9999999,\"queryLatencyMillis\":5.65284947195106,\"writeLatencyMillis\":17.34593812832452}}],\"deploymentJobs\":{\"projectId\":102889,\"jobStatus\":[{\"jobType\":\"staging-test\",\"lastTriggered\":{\"id\":-1,\"version\":\"6.173.62\",\"revision\":{\"applicationPackageHash\":\"9db423e1021d7b452d37ec6372bc757d9c1bda87\",\"sourceRevision\":{\"repositoryField\":\"git@git.ouroath.com:Tripod/service-aggregation-vespa.git\",\"branchField\":\"origin/master\",\"commitField\":\"49cd7bbb1ed9f4b922083cb042590b0885ffe22b\"}},\"upgrade\":true,\"reason\":\"system-test completed\",\"at\":1510830134259},\"lastCompleted\":{\"id\":1184,\"version\":\"6.173.62\",\"revision\":{\"applicationPackageHash\":\"9db423e1021d7b452d37ec6372bc757d9c1bda87\",\"sourceRevision\":{\"repositoryField\":\"git@git.ouroath.com:Tripod/service-aggregation-vespa.git\",\"branchField\":\"origin/master\",\"commitField\":\"49cd7bbb1ed9f4b922083cb042590b0885ffe22b\"}},\"upgrade\":true,\"reason\":\"system-test completed\",\"at\":1510830684960},\"lastSuccess\":{\"id\":1184,\"version\":\"6.173.62\",\"revision\":{\"applicationPackageHash\":\"9db423e1021d7b452d37ec6372bc757d9c1bda87\",\"sourceRevision\":{\"repositoryField\":\"git@git.ouroath.com:Tripod/service-aggregation-vespa.git\",\"branchField\":\"origin/master\",\"commitField\":\"49cd7bbb1ed9f4b922083cb042590b0885ffe22b\"}},\"upgrade\":true,\"reason\":\"system-test completed\",\"at\":1510830684960}},{\"jobType\":\"component\",\"lastCompleted\":{\"id\":849,\"version\":\"6.174.156\",\"upgrade\":false,\"reason\":\"Application commit\",\"at\":1511217733555},\"lastSuccess\":{\"id\":849,\"version\":\"6.174.156\",\"upgrade\":false,\"reason\":\"Application commit\",\"at\":1511217733555}},{\"jobType\":\"production-us-east-3\",\"lastTriggered\":{\"id\":-1,\"version\":\"6.173.62\",\"revision\":{\"applicationPackageHash\":\"9db423e1021d7b452d37ec6372bc757d9c1bda87\",\"sourceRevision\":{\"repositoryField\":\"git@git.ouroath.com:Tripod/service-aggregation-vespa.git\",\"branchField\":\"origin/master\",\"commitField\":\"49cd7bbb1ed9f4b922083cb042590b0885ffe22b\"}},\"upgrade\":true,\"reason\":\"staging-test completed\",\"at\":1510830685127},\"lastCompleted\":{\"id\":923,\"version\":\"6.173.62\",\"revision\":{\"applicationPackageHash\":\"9db423e1021d7b452d37ec6372bc757d9c1bda87\",\"sourceRevision\":{\"repositoryField\":\"git@git.ouroath.com:Tripod/service-aggregation-vespa.git\",\"branchField\":\"origin/master\",\"commitField\":\"49cd7bbb1ed9f4b922083cb042590b0885ffe22b\"}},\"upgrade\":true,\"reason\":\"staging-test completed\",\"at\":1510837650046},\"lastSuccess\":{\"id\":923,\"version\":\"6.173.62\",\"revision\":{\"applicationPackageHash\":\"9db423e1021d7b452d37ec6372bc757d9c1bda87\",\"sourceRevision\":{\"repositoryField\":\"git@git.ouroath.com:Tripod/service-aggregation-vespa.git\",\"branchField\":\"origin/master\",\"commitField\":\"49cd7bbb1ed9f4b922083cb042590b0885ffe22b\"}},\"upgrade\":true,\"reason\":\"staging-test completed\",\"at\":1510837650046}},{\"jobType\":\"production-us-west-1\",\"lastTriggered\":{\"id\":-1,\"version\":\"6.173.62\",\"revision\":{\"applicationPackageHash\":\"9db423e1021d7b452d37ec6372bc757d9c1bda87\",\"sourceRevision\":{\"repositoryField\":\"git@git.ouroath.com:Tripod/service-aggregation-vespa.git\",\"branchField\":\"origin/master\",\"commitField\":\"49cd7bbb1ed9f4b922083cb042590b0885ffe22b\"}},\"upgrade\":true,\"reason\":\"production-us-east-3 completed\",\"at\":1510837650139},\"lastCompleted\":{\"id\":646,\"version\":\"6.173.62\",\"revision\":{\"applicationPackageHash\":\"9db423e1021d7b452d37ec6372bc757d9c1bda87\",\"sourceRevision\":{\"repositoryField\":\"git@git.ouroath.com:Tripod/service-aggregation-vespa.git\",\"branchField\":\"origin/master\",\"commitField\":\"49cd7bbb1ed9f4b922083cb042590b0885ffe22b\"}},\"upgrade\":true,\"reason\":\"production-us-east-3 completed\",\"at\":1510843559162},\"lastSuccess\":{\"id\":646,\"version\":\"6.173.62\",\"revision\":{\"applicationPackageHash\":\"9db423e1021d7b452d37ec6372bc757d9c1bda87\",\"sourceRevision\":{\"repositoryField\":\"git@git.ouroath.com:Tripod/service-aggregation-vespa.git\",\"branchField\":\"origin/master\",\"commitField\":\"49cd7bbb1ed9f4b922083cb042590b0885ffe22b\"}},\"upgrade\":true,\"reason\":\"production-us-east-3 completed\",\"at\":1510843559162}},{\"jobType\":\"system-test\",\"jobError\":\"unknown\",\"lastTriggered\":{\"id\":-1,\"version\":\"6.173.62\",\"revision\":{\"applicationPackageHash\":\"ec548fa61cbfab7a270a51d46b1263ec1be5d9a8\",\"sourceRevision\":{\"repositoryField\":\"git@git.ouroath.com:Tripod/service-aggregation-vespa.git\",\"branchField\":\"origin/master\",\"commitField\":\"234f3e4e77049d0b9538c9e1b356d17eb1dedb6a\"}},\"upgrade\":false,\"reason\":\"Available change in component\",\"at\":1511256608649},\"lastCompleted\":{\"id\":1686,\"version\":\"6.173.62\",\"revision\":{\"applicationPackageHash\":\"ec548fa61cbfab7a270a51d46b1263ec1be5d9a8\",\"sourceRevision\":{\"repositoryField\":\"git@git.ouroath.com:Tripod/service-aggregation-vespa.git\",\"branchField\":\"origin/master\",\"commitField\":\"234f3e4e77049d0b9538c9e1b356d17eb1dedb6a\"}},\"upgrade\":false,\"reason\":\"Available change in component\",\"at\":1511256603353},\"firstFailing\":{\"id\":1659,\"version\":\"6.173.62\",\"revision\":{\"applicationPackageHash\":\"ec548fa61cbfab7a270a51d46b1263ec1be5d9a8\",\"sourceRevision\":{\"repositoryField\":\"git@git.ouroath.com:Tripod/service-aggregation-vespa.git\",\"branchField\":\"origin/master\",\"commitField\":\"234f3e4e77049d0b9538c9e1b356d17eb1dedb6a\"}},\"upgrade\":false,\"reason\":\"component completed\",\"at\":1511219070725},\"lastSuccess\":{\"id\":1658,\"version\":\"6.173.62\",\"revision\":{\"applicationPackageHash\":\"9db423e1021d7b452d37ec6372bc757d9c1bda87\",\"sourceRevision\":{\"repositoryField\":\"git@git.ouroath.com:Tripod/service-aggregation-vespa.git\",\"branchField\":\"origin/master\",\"commitField\":\"49cd7bbb1ed9f4b922083cb042590b0885ffe22b\"}},\"upgrade\":true,\"reason\":\"Upgrading to 6.173.62\",\"at\":1511175754163}}]},\"deployingField\":{\"applicationPackageHash\":\"ec548fa61cbfab7a270a51d46b1263ec1be5d9a8\",\"sourceRevision\":{\"repositoryField\":\"git@git.ouroath.com:Tripod/service-aggregation-vespa.git\",\"branchField\":\"origin/master\",\"commitField\":\"234f3e4e77049d0b9538c9e1b356d17eb1dedb6a\"}},\"outstandingChangeField\":false,\"queryQuality\":100,\"writeQuality\":99.99894341115082}";
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializerTest.java
index f5f43265cb8..189b3a97a80 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializerTest.java
@@ -27,6 +27,8 @@ public class VersionStatusSerializerTest {
Version.fromString("5.0"),
Arrays.asList(ApplicationId.from("tenant1", "failing1", "default")),
Arrays.asList(ApplicationId.from("tenant2", "success1", "default"),
+ ApplicationId.from("tenant2", "success2", "default")),
+ Arrays.asList(ApplicationId.from("tenant1", "failing1", "default"),
ApplicationId.from("tenant2", "success2", "default"))
);
vespaVersions.add(new VespaVersion(statistics, "dead", Instant.now(), false,
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/proxy/ProxyRequestTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/proxy/ProxyRequestTest.java
new file mode 100644
index 00000000000..04a987d98d1
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/proxy/ProxyRequestTest.java
@@ -0,0 +1,83 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.proxy;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author Haakon Dybdahl
+ */
+public class ProxyRequestTest {
+
+ @Rule
+ public final ExpectedException exception = ExpectedException.none();
+
+ @Test
+ public void testEmpty() throws Exception {
+ exception.expectMessage("Request not set.");
+ testRequest(null, "/zone/v2/");
+ }
+
+ @Test
+ public void testBadUri() throws Exception {
+ exception.expectMessage("Request not starting with /zone/v2/");
+ testRequest(URI.create("http://foo"), "/zone/v2/");
+ }
+
+ @Test
+ public void testConfigRequestEmpty() throws Exception {
+ ProxyRequest proxyRequest = testRequest(URI.create("http://foo/zone/v2/foo/bar"), "/zone/v2/");
+ assertEquals("foo", proxyRequest.getEnvironment());
+ assertEquals("bar", proxyRequest.getRegion());
+ assertFalse(proxyRequest.isDiscoveryRequest());
+ assertTrue(proxyRequest.getConfigServerRequest().isEmpty());
+
+ }
+
+ @Test
+ public void testDiscoveryRequest() throws Exception {
+ ProxyRequest proxyRequest = testRequest(URI.create("http://foo/zone/v2/foo"), "/zone/v2/");
+ assertEquals("foo", proxyRequest.getEnvironment());
+ assertTrue(proxyRequest.isDiscoveryRequest());
+
+ }
+
+ @Test
+ public void testProxyRequest() throws Exception {
+ ProxyRequest proxyRequest = testRequest(URI.create("http://foo/zone/v2/foo/bar/bla/bla/v1/something"),
+ "/zone/v2/");
+ assertEquals("foo", proxyRequest.getEnvironment());
+ assertEquals("/bla/bla/v1/something", proxyRequest.getConfigServerRequest());
+ }
+
+ @Test
+ public void testProxyRequestWithParameters() throws Exception {
+ ProxyRequest proxyRequest = testRequest(URI.create("http://foo/zone/v2/foo/bar/something?p=v&q=y"),
+ "/zone/v2/");
+ assertEquals("foo", proxyRequest.getEnvironment());
+ assertEquals("/something?p=v&q=y", proxyRequest.getConfigServerRequest());
+ }
+
+ private static ProxyRequest testRequest(URI url, String pathPrefix) throws IOException, ProxyException {
+ return new ProxyRequest(url, headers("controller:49152"), null, "GET", pathPrefix);
+ }
+
+ private static Map<String, List<String>> headers(String hostPort) {
+ Map<String, List<String>> headers = new HashMap<>();
+ headers.put("host", Collections.singletonList(hostPort));
+ return Collections.unmodifiableMap(headers);
+ }
+
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/proxy/ProxyResponseTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/proxy/ProxyResponseTest.java
new file mode 100644
index 00000000000..8dbd1c4ef61
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/proxy/ProxyResponseTest.java
@@ -0,0 +1,69 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.proxy;
+
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.net.URI;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author Haakon Dybdahl
+ */
+public class ProxyResponseTest {
+
+ @Test
+ public void testRewriteUrl() throws Exception {
+ String controllerPrefix = "/zone/v2/";
+ URI configServer = URI.create("http://configserver:1234");
+ ProxyRequest request = new ProxyRequest(URI.create("http://foo/zone/v2/env/region/configserver"),
+ headers("controller:49152"), null, "GET",
+ controllerPrefix);
+ ProxyResponse proxyResponse = new ProxyResponse(
+ request,
+ "response link is http://configserver:1234/bla/bla/",
+ 200,
+ Optional.of(configServer),
+ "application/json");
+
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+
+ proxyResponse.render(outputStream);
+ String document = new String(outputStream.toByteArray(),"UTF-8");
+ assertEquals("response link is http://controller:49152/zone/v2/env/region/bla/bla/", document);
+ }
+
+ @Test
+ public void testRewriteSecureUrl() throws Exception {
+ String controllerPrefix = "/zone/v2/";
+ URI configServer = URI.create("http://configserver:1234");
+ ProxyRequest request = new ProxyRequest(URI.create("https://foo/zone/v2/env/region/configserver"),
+ headers("controller:49152"), null, "GET",
+ controllerPrefix);
+ ProxyResponse proxyResponse = new ProxyResponse(
+ request,
+ "response link is http://configserver:1234/bla/bla/",
+ 200,
+ Optional.of(configServer),
+ "application/json");
+
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+
+ proxyResponse.render(outputStream);
+ String document = new String(outputStream.toByteArray(),"UTF-8");
+ assertEquals("response link is https://controller:49152/zone/v2/env/region/bla/bla/", document);
+ }
+
+ private static Map<String, List<String>> headers(String hostPort) {
+ Map<String, List<String>> headers = new HashMap<>();
+ headers.put("host", Collections.singletonList(hostPort));
+ return Collections.unmodifiableMap(headers);
+ }
+
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerControllerTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerControllerTester.java
index 45a8972eafe..6c5120df515 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerControllerTester.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerControllerTester.java
@@ -88,6 +88,12 @@ public class ContainerControllerTester {
}
public void notifyJobCompletion(ApplicationId applicationId, long projectId, boolean success, DeploymentJobs.JobType job) {
+ try {
+ Thread.sleep(1);
+ }
+ catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
controller().applications().notifyJobCompletion(new DeploymentJobs.JobReport(applicationId, job, projectId,
42,
success ? Optional.empty() : Optional.of(DeploymentJobs.JobError.unknown)
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerTester.java
index b55ee9a195f..c0e8b48f821 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerTester.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerTester.java
@@ -22,6 +22,7 @@ import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
+import java.util.function.Supplier;
import static org.junit.Assert.assertEquals;
@@ -52,10 +53,18 @@ public class ContainerTester {
controller.updateVersionStatus(VersionStatus.compute(controller, version));
}
+ public void assertResponse(Supplier<Request> request, File responseFile) throws IOException {
+ assertResponse(request.get(), responseFile);
+ }
+
public void assertResponse(Request request, File responseFile) throws IOException {
assertResponse(request, responseFile, 200);
}
+ public void assertResponse(Supplier<Request> request, File responseFile, int expectedStatusCode) throws IOException {
+ assertResponse(request.get(), responseFile, expectedStatusCode);
+ }
+
public void assertResponse(Request request, File responseFile, int expectedStatusCode) throws IOException {
String expectedResponse = IOUtils.readFile(new File(responseFilePath + responseFile.toString()));
expectedResponse = include(expectedResponse);
@@ -72,10 +81,18 @@ public class ContainerTester {
replace(new String(SlimeUtils.toJsonBytes(responseSlime), StandardCharsets.UTF_8), replaceStrings));
}
+ public void assertResponse(Supplier<Request> request, String expectedResponse) throws IOException {
+ assertResponse(request.get(), expectedResponse, 200);
+ }
+
public void assertResponse(Request request, String expectedResponse) throws IOException {
assertResponse(request, expectedResponse, 200);
}
+ public void assertResponse(Supplier<Request> request, String expectedResponse, int expectedStatusCode) throws IOException {
+ assertResponse(request.get(), expectedResponse, expectedStatusCode);
+ }
+
public void assertResponse(Request request, String expectedResponse, int expectedStatusCode) throws IOException {
Response response = container.handleRequest(request);
assertEquals(expectedResponse, response.getBodyAsString());
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java
index e6c0ce9027d..044c5d75d12 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java
@@ -43,10 +43,12 @@ public class ControllerContainerTest {
" <component id='com.yahoo.vespa.hosted.controller.api.integration.github.GitHubMock'/>" +
" <component id='com.yahoo.vespa.hosted.controller.api.integration.routing.MemoryGlobalRoutingService'/>" +
" <component id='com.yahoo.vespa.hosted.controller.api.integration.stubs.LoggingDeploymentIssues'/>" +
+ " <component id='com.yahoo.vespa.hosted.controller.api.integration.stubs.DummyOwnershipIssues'/>" +
" <component id='com.yahoo.vespa.hosted.controller.api.integration.organization.MockOrganization'/>" +
" <component id='com.yahoo.vespa.hosted.controller.ConfigServerClientMock'/>" +
" <component id='com.yahoo.vespa.hosted.controller.ZoneRegistryMock'/>" +
" <component id='com.yahoo.vespa.hosted.controller.Controller'/>" +
+ " <component id='com.yahoo.vespa.hosted.controller.ConfigServerProxyMock'/>" +
" <component id='com.yahoo.vespa.hosted.controller.integration.MockMetricsService'/>" +
" <component id='com.yahoo.vespa.hosted.controller.maintenance.ControllerMaintenance'/>" +
" <component id='com.yahoo.vespa.hosted.controller.maintenance.JobControl'/>" +
@@ -69,6 +71,14 @@ public class ControllerContainerTest {
" <handler id='com.yahoo.vespa.hosted.controller.restapi.screwdriver.ScrewdriverApiHandler'>" +
" <binding>http://*/screwdriver/v1/*</binding>" +
" </handler>" +
+ " <handler id='com.yahoo.vespa.hosted.controller.restapi.zone.v1.ZoneApiHandler'>" +
+ " <binding>http://*/zone/v1</binding>" +
+ " <binding>http://*/zone/v1/*</binding>" +
+ " </handler>" +
+ " <handler id='com.yahoo.vespa.hosted.controller.restapi.zone.v2.ZoneApiHandler'>" +
+ " <binding>http://*/zone/v2</binding>" +
+ " <binding>http://*/zone/v2/*</binding>" +
+ " </handler>" +
"</jdisc>";
protected void assertResponse(Request request, int responseStatus, String responseMessage) throws IOException {
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java
index e3443d6c014..bf4586f9fd0 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java
@@ -5,14 +5,14 @@ import com.yahoo.application.container.handler.Request;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.Environment;
-import com.yahoo.vespa.curator.Lock;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.ConfigServerClientMock;
-import com.yahoo.vespa.hosted.controller.LockedApplication;
import com.yahoo.vespa.hosted.controller.api.identifiers.AthenzDomain;
import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
import com.yahoo.vespa.hosted.controller.api.identifiers.UserId;
+import com.yahoo.vespa.hosted.controller.api.integration.MetricsService.ApplicationMetrics;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServerException;
+import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
import com.yahoo.vespa.hosted.controller.api.integration.organization.MockOrganization;
import com.yahoo.vespa.hosted.controller.api.integration.organization.User;
import com.yahoo.vespa.hosted.controller.application.ApplicationPackage;
@@ -47,6 +47,12 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
+import java.util.function.Supplier;
+
+import static com.yahoo.application.container.handler.Request.Method.DELETE;
+import static com.yahoo.application.container.handler.Request.Method.GET;
+import static com.yahoo.application.container.handler.Request.Method.POST;
+import static com.yahoo.application.container.handler.Request.Method.PUT;
/**
* @author bratseth
@@ -72,63 +78,87 @@ public class ApplicationApiTest extends ControllerContainerTest {
addTenantAthenzDomain(athenzUserDomain, "mytenant"); // (Necessary but not provided in this API)
// GET API root
- tester.assertResponse(request("/application/v4/", "", Request.Method.GET),
+ tester.assertResponse(request("/application/v4/", GET),
new File("root.json"));
// GET athens domains
- tester.assertResponse(request("/application/v4/athensDomain/", "", Request.Method.GET),
+ tester.assertResponse(request("/application/v4/athensDomain/", GET),
new File("athensDomain-list.json"));
// GET OpsDB properties
- tester.assertResponse(request("/application/v4/property/", "", Request.Method.GET),
+ tester.assertResponse(request("/application/v4/property/", GET),
new File("property-list.json"));
// GET cookie freshness
- tester.assertResponse(request("/application/v4/cookiefreshness/", "", Request.Method.GET),
+ tester.assertResponse(request("/application/v4/cookiefreshness/", GET),
new File("cookiefreshness.json"));
// POST (add) a tenant without property ID
- tester.assertResponse(request("/application/v4/tenant/tenant1",
- "{\"athensDomain\":\"domain1\", \"property\":\"property1\"}",
- Request.Method.POST),
+ tester.assertResponse(request("/application/v4/tenant/tenant1", POST)
+ .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}"),
new File("tenant-without-applications.json"));
// PUT (modify) a tenant
- tester.assertResponse(request("/application/v4/tenant/tenant1",
- "{\"athensDomain\":\"domain1\", \"property\":\"property1\"}",
- Request.Method.PUT),
+ tester.assertResponse(request("/application/v4/tenant/tenant1", PUT)
+ .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}"),
new File("tenant-without-applications.json"));
// GET the authenticated user (with associated tenants)
- tester.assertResponse(request("/application/v4/user", "", Request.Method.GET),
+ tester.assertResponse(request("/application/v4/user", GET),
new File("user.json"));
// GET all tenants
- tester.assertResponse(request("/application/v4/tenant/", "", Request.Method.GET),
+ tester.assertResponse(request("/application/v4/tenant/", GET),
new File("tenant-list.json"));
+
+
+ // Add another Athens domain, so we can try to create more tenants
+ addTenantAthenzDomain("domain2", "mytenant"); // New domain to test tenant w/property ID
+ // Add property info for that property id, as well, in the mock organization.
+ addPropertyData((MockOrganization) controllerTester.controller().organization(), "1234");
+ // POST (add) a tenant with property ID
+ tester.assertResponse(request("/application/v4/tenant/tenant2", POST)
+ .data("{\"athensDomain\":\"domain2\", \"property\":\"property2\", \"propertyId\":\"1234\"}"),
+ new File("tenant-without-applications-with-id.json"));
+ // PUT (modify) a tenant with property ID
+ tester.assertResponse(request("/application/v4/tenant/tenant2", PUT)
+ .data("{\"athensDomain\":\"domain2\", \"property\":\"property2\", \"propertyId\":\"1234\"}"),
+ new File("tenant-without-applications-with-id.json"));
+ // GET a tenant with property ID
+ tester.assertResponse(request("/application/v4/tenant/tenant2", GET),
+ new File("tenant-without-applications-with-id.json"));
+
+ // Test legacy OpsDB tenants
+ // POST (add) an OpsDB tenant with property ID
+ tester.assertResponse(request("/application/v4/tenant/tenant3", POST)
+ .data("{\"userGroup\":\"group1\",\"property\":\"property1\",\"propertyId\":\"1234\"}"),
+ new File("opsdb-tenant-with-id-without-applications.json"));
+ // PUT (modify) the OpsDB tenant to set another property
+ tester.assertResponse(request("/application/v4/tenant/tenant3", PUT)
+ .data("{\"userGroup\":\"group1\",\"property\":\"property2\",\"propertyId\":\"4321\"}"),
+ new File("opsdb-tenant-with-new-id-without-applications.json"));
+
// POST (create) an application
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1",
- "",
- Request.Method.POST),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", POST),
new File("application-reference.json"));
// GET a tenant
- tester.assertResponse(request("/application/v4/tenant/tenant1", "", Request.Method.GET),
+ tester.assertResponse(request("/application/v4/tenant/tenant1", GET),
new File("tenant-with-application.json"));
// GET tenant applications
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/", "", Request.Method.GET),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/", GET),
new File("application-list.json"));
// POST triggering of a full deployment to an application (if version is omitted, current system version is used)
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/deploying", "6.1.0", Request.Method.POST),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/deploying", POST)
+ .data("6.1.0"),
new File("application-deployment.json"));
// DELETE (cancel) ongoing change
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/deploying", "", Request.Method.DELETE),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/deploying", DELETE),
new File("application-deployment-cancelled.json"));
// DELETE (cancel) again is a no-op
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/deploying", "", Request.Method.DELETE),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/deploying", DELETE),
new File("application-deployment-cancelled-no-op.json"));
// POST (deploy) an application to a zone - manual user deployment
HttpEntity entity = createApplicationDeployData(applicationPackage, Optional.empty());
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/dev/region/us-west-1/instance/default/deploy",
- entity,
- Request.Method.POST,
- athenzUserDomain, "mytenant"),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/dev/region/us-west-1/instance/default/deploy", POST)
+ .data(entity)
+ .domain(athenzUserDomain).user("mytenant"),
new File("deploy-result.json"));
// POST (deploy) an application to a zone. This simulates calls done by our tenant pipeline.
@@ -138,168 +168,146 @@ public class ApplicationApiTest extends ControllerContainerTest {
addScrewdriverUserToDomain("screwdriveruser1", "domain1"); // (Necessary but not provided in this API)
// Trigger deployment
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/deploying", "6.1.0", Request.Method.POST),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/deploying", POST)
+ .data("6.1.0"),
new File("application-deployment.json"));
// ... systemtest
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/test/region/us-east-1/instance/default/",
- createApplicationDeployData(applicationPackage, Optional.of(screwdriverProjectId)),
- Request.Method.POST,
- athenzScrewdriverDomain, "screwdriveruser1"),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/test/region/us-east-1/instance/default/", POST)
+ .data(createApplicationDeployData(applicationPackage, Optional.of(screwdriverProjectId)))
+ .domain(athenzScrewdriverDomain).user("screwdriveruser1"),
new File("deploy-result.json"));
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/test/region/us-east-1/instance/default",
- "",
- Request.Method.DELETE),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/test/region/us-east-1/instance/default", DELETE),
"Deactivated tenant/tenant1/application/application1/environment/test/region/us-east-1/instance/default");
controllerTester.notifyJobCompletion(id, screwdriverProjectId, true, DeploymentJobs.JobType.systemTest); // Called through the separate screwdriver/v1 API
// ... staging
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/staging/region/us-east-3/instance/default/",
- createApplicationDeployData(applicationPackage, Optional.of(screwdriverProjectId)),
- Request.Method.POST,
- athenzScrewdriverDomain, "screwdriveruser1"),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/staging/region/us-east-3/instance/default/", POST)
+ .data(createApplicationDeployData(applicationPackage, Optional.of(screwdriverProjectId)))
+ .domain(athenzScrewdriverDomain).user("screwdriveruser1"),
new File("deploy-result.json"));
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/staging/region/us-east-3/instance/default",
- "",
- Request.Method.DELETE),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/staging/region/us-east-3/instance/default", DELETE),
"Deactivated tenant/tenant1/application/application1/environment/staging/region/us-east-3/instance/default");
controllerTester.notifyJobCompletion(id, screwdriverProjectId, true, DeploymentJobs.JobType.stagingTest);
// ... prod zone
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default/",
- createApplicationDeployData(applicationPackage, Optional.of(screwdriverProjectId)),
- Request.Method.POST,
- athenzScrewdriverDomain, "screwdriveruser1"),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default/", POST)
+ .data(createApplicationDeployData(applicationPackage, Optional.of(screwdriverProjectId)))
+ .domain(athenzScrewdriverDomain).user("screwdriveruser1"),
new File("deploy-result.json"));
controllerTester.notifyJobCompletion(id, screwdriverProjectId, false, DeploymentJobs.JobType.productionCorpUsEast1);
// GET tenant screwdriver projects
- tester.assertResponse(request("/application/v4/tenant-pipeline/", "", Request.Method.GET),
+ tester.assertResponse(request("/application/v4/tenant-pipeline/", GET),
new File("tenant-pipelines.json"));
+ setDeploymentMaintainedInfo(controllerTester);
// GET tenant application deployments
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", "", Request.Method.GET),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", GET),
new File("application.json"));
// GET an application deployment
- setDeploymentMaintainedInfo(controllerTester);
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default", "", Request.Method.GET),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default", GET),
new File("deployment.json"));
+
+ addIssues(controllerTester, ApplicationId.from("tenant1", "application1", "default"));
+ // GET at root, with "&recursive=deployment", returns info about all tenants, their applications and their deployments
+ tester.assertResponse(request("/application/v4/", GET)
+ .domain("domain1").user("mytenant")
+ .recursive("deployment"),
+ new File("recursive-root.json"));
+ // GET at root, with "&recursive=tenant", returns info about all tenants, with limmited info about their applications.
+ tester.assertResponse(request("/application/v4/", GET)
+ .domain("domain1").user("mytenant")
+ .recursive("tenant"),
+ new File("recursive-until-tenant-root.json"));
+ // GET at a tenant, with "&recursive=true", returns full info about their applications and their deployments
+ tester.assertResponse(request("/application/v4/tenant/tenant1/", GET)
+ .domain("domain1").user("mytenant")
+ .recursive("true"),
+ new File("tenant1-recursive.json"));
+ // GET at an application, with "&recursive=true", returns full info about its deployments
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/", GET)
+ .domain("domain1").user("mytenant")
+ .recursive("true"),
+ new File("application1-recursive.json"));
+
+
// POST a 'restart application' command
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default/restart",
- "",
- Request.Method.POST),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default/restart", POST),
"Requested restart of tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default");
// POST a 'restart application' command with a host filter (other filters not supported yet)
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default/restart?hostname=host1",
- "",
- Request.Method.POST),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default/restart?hostname=host1", POST),
"Requested restart of tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default");
// POST a 'log' command
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default/log",
- "",
- Request.Method.POST),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default/log", POST),
new File("log-response.json")); // Proxied to config server, not sure about the expected return format
// GET (wait for) convergence
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default/converge", "", Request.Method.GET),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default/converge", GET),
new File("convergence.json"));
// GET services
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default/service", "", Request.Method.GET),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default/service", GET),
new File("services.json"));
// GET service
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default/service/storagenode-awe3slno6mmq2fye191y324jl/state/v1/", "", Request.Method.GET),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default/service/storagenode-awe3slno6mmq2fye191y324jl/state/v1/", GET),
new File("service.json"));
// DELETE (deactivate) a deployment - dev
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/dev/region/us-west-1/instance/default",
- "",
- Request.Method.DELETE),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/dev/region/us-west-1/instance/default", DELETE),
"Deactivated tenant/tenant1/application/application1/environment/dev/region/us-west-1/instance/default");
// DELETE (deactivate) a deployment - prod
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default",
- "",
- Request.Method.DELETE),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default", DELETE),
"Deactivated tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default");
// DELETE (deactivate) a deployment is idempotent
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default",
- "",
- Request.Method.DELETE),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default", DELETE),
"Deactivated tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default");
// DELETE an application
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", "", Request.Method.DELETE),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", DELETE),
"");
// DELETE a tenant
- tester.assertResponse(request("/application/v4/tenant/tenant1", "", Request.Method.DELETE),
+ tester.assertResponse(request("/application/v4/tenant/tenant1", DELETE),
new File("tenant-without-applications.json"));
// PUT (create) the authenticated user
- tester.assertResponse(request("/application/v4/user?user=newuser&domain=by",
- new byte[0],
- Request.Method.PUT,
- athenzUserDomain, "newuser", "application/json"),
+ byte[] data = new byte[0];
+ tester.assertResponse(request("/application/v4/user?user=newuser&domain=by", PUT)
+ .data(data)
+ .domain(athenzUserDomain).user("newuser"),
new File("create-user-response.json"));
// OPTIONS return 200 OK
- tester.assertResponse(request("/application/v4/", "", Request.Method.OPTIONS),
+ tester.assertResponse(request("/application/v4/", Request.Method.OPTIONS),
"");
- // Add another Athens domain, so we can try to create more tenants
- addTenantAthenzDomain("domain2", "mytenant"); // New domain to test tenant w/property ID
- // Add property info for that property id, as well, in the mock organization.
- addPropertyData((MockOrganization) controllerTester.controller().organization(), "1234");
- // POST (add) a tenant with property ID
- tester.assertResponse(request("/application/v4/tenant/tenant2",
- "{\"athensDomain\":\"domain2\", \"property\":\"property2\", \"propertyId\":\"1234\"}",
- Request.Method.POST),
- new File("tenant-without-applications-with-id.json"));
- // PUT (modify) a tenant with property ID
- tester.assertResponse(request("/application/v4/tenant/tenant2",
- "{\"athensDomain\":\"domain2\", \"property\":\"property2\", \"propertyId\":\"1234\"}",
- Request.Method.PUT),
- new File("tenant-without-applications-with-id.json"));
- // GET a tenant with property ID
- tester.assertResponse(request("/application/v4/tenant/tenant2", "", Request.Method.GET),
- new File("tenant-without-applications-with-id.json"));
-
- // Test legacy OpsDB tenants
- // POST (add) an OpsDB tenant with property ID
- tester.assertResponse(request("/application/v4/tenant/tenant3",
- "{\"userGroup\":\"group1\",\"property\":\"property1\",\"propertyId\":\"1234\"}",
- Request.Method.POST),
- new File("opsdb-tenant-with-id-without-applications.json"));
- // PUT (modify) the OpsDB tenant to set another property
- tester.assertResponse(request("/application/v4/tenant/tenant3",
- "{\"userGroup\":\"group1\",\"property\":\"property2\",\"propertyId\":\"4321\"}",
- Request.Method.PUT),
- new File("opsdb-tenant-with-new-id-without-applications.json"));
-
// GET global rotation status
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-west-1/instance/default/global-rotation", "", Request.Method.GET),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-west-1/instance/default/global-rotation", GET),
new File("global-rotation.json"));
// GET global rotation override status
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-west-1/instance/default/global-rotation/override", "", Request.Method.GET),
- new File("global-rotation-get.json"));
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-west-1/instance/default/global-rotation/override", GET),
+ new File("global-rotation-get.json"));
// SET global rotation override status
- tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2/environment/prod/region/us-west-1/instance/default/global-rotation/override", "{\"reason\":\"because i can\"}", Request.Method.PUT),
- new File("global-rotation-put.json"));
+ tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2/environment/prod/region/us-west-1/instance/default/global-rotation/override", PUT)
+ .data("{\"reason\":\"because i can\"}"),
+ new File("global-rotation-put.json"));
// DELETE global rotation override status
- tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2/environment/prod/region/us-west-1/instance/default/global-rotation/override", "{\"reason\":\"because i can\"}", Request.Method.DELETE),
- new File("global-rotation-delete.json"));
+ tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2/environment/prod/region/us-west-1/instance/default/global-rotation/override", DELETE)
+ .data("{\"reason\":\"because i can\"}"),
+ new File("global-rotation-delete.json"));
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/promote", "", Request.Method.POST),
- "{\"message\":\"Successfully copied environment hosted-verified-prod to hosted-instance_tenant1_application1_placeholder_component_default\"}");
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-west-1/instance/default/promote", "", Request.Method.POST),
- "{\"message\":\"Successfully copied environment hosted-instance_tenant1_application1_placeholder_component_default to hosted-instance_tenant1_application1_us-west-1_prod_default\"}");
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/promote", POST),
+ "{\"message\":\"Successfully copied environment hosted-verified-prod to hosted-instance_tenant1_application1_placeholder_component_default\"}");
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-west-1/instance/default/promote", POST),
+ "{\"message\":\"Successfully copied environment hosted-instance_tenant1_application1_placeholder_component_default to hosted-instance_tenant1_application1_us-west-1_prod_default\"}");
controllerTester.controller().deconstruct();
}
- private void addPropertyData(MockOrganization organization, String propertyIdValue) {
- PropertyId propertyId = new PropertyId(propertyIdValue);
- organization.addProperty(propertyId);
- organization.setContactsFor(propertyId, Arrays.asList(Collections.singletonList(User.from("alice")),
- Collections.singletonList(User.from("bob"))));
+ private void addIssues(ContainerControllerTester tester, ApplicationId id) {
+ tester.controller().applications().lockedOrThrow(id, application ->
+ tester.controller().applications().store(application
+ .withDeploymentIssueId(IssueId.from("123"))
+ .withOwnershipIssueId(IssueId.from("321"))));
}
@Test
@@ -312,23 +320,19 @@ public class ApplicationApiTest extends ControllerContainerTest {
addScrewdriverUserToDomain("screwdriveruser1", "domain1");
// Create tenant
- tester.assertResponse(request("/application/v4/tenant/tenant1",
- "{\"athensDomain\":\"domain1\", \"property\":\"property1\"}",
- Request.Method.POST),
+ tester.assertResponse(request("/application/v4/tenant/tenant1", POST)
+ .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}"),
new File("tenant-without-applications.json"));
// Create application
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1",
- "",
- Request.Method.POST),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", POST),
new File("application-reference.json"));
// POST (deploy) an application to a prod zone - allowed when project ID is not specified
HttpEntity entity = createApplicationDeployData(applicationPackage, Optional.empty());
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default/deploy",
- entity,
- Request.Method.POST,
- athenzScrewdriverDomain, "screwdriveruser1"),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/corp-us-east-1/instance/default/deploy", POST)
+ .data(entity)
+ .domain(athenzScrewdriverDomain).user("screwdriveruser1"),
new File("deploy-result.json"));
}
@@ -342,15 +346,12 @@ public class ApplicationApiTest extends ControllerContainerTest {
addScrewdriverUserToDomain("screwdriveruser1", "domain1");
// Create tenant
- tester.assertResponse(request("/application/v4/tenant/tenant1",
- "{\"athensDomain\":\"domain1\", \"property\":\"property1\"}",
- Request.Method.POST),
+ tester.assertResponse(request("/application/v4/tenant/tenant1", POST)
+ .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}"),
new File("tenant-without-applications.json"));
// Create application
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1",
- "",
- Request.Method.POST),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", POST),
new File("application-reference.json"));
// Deploy
@@ -364,10 +365,9 @@ public class ApplicationApiTest extends ControllerContainerTest {
startAndTestChange(controllerTester, id, projectId, deployData);
// us-east-3
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-east-3/instance/default/deploy",
- deployData,
- Request.Method.POST,
- athenzScrewdriverDomain, "screwdriveruser1"),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-east-3/instance/default/deploy", POST)
+ .data(deployData)
+ .domain(athenzScrewdriverDomain).user("screwdriveruser1"),
new File("deploy-result.json"));
controllerTester.notifyJobCompletion(id, projectId, true, DeploymentJobs.JobType.productionUsEast3);
@@ -381,22 +381,20 @@ public class ApplicationApiTest extends ControllerContainerTest {
startAndTestChange(controllerTester, id, projectId, deployData);
// us-west-1
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-west-1/instance/default/deploy",
- deployData,
- Request.Method.POST,
- athenzScrewdriverDomain, "screwdriveruser1"),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-west-1/instance/default/deploy", POST)
+ .data(deployData)
+ .domain(athenzScrewdriverDomain).user("screwdriveruser1"),
new File("deploy-result.json"));
controllerTester.notifyJobCompletion(id, projectId, true, DeploymentJobs.JobType.productionUsWest1);
// us-east-3
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-east-3/instance/default/deploy",
- deployData,
- Request.Method.POST,
- athenzScrewdriverDomain, "screwdriveruser1"),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-east-3/instance/default/deploy", POST)
+ .data(deployData).domain(athenzScrewdriverDomain).user("screwdriveruser1"),
new File("deploy-result.json"));
controllerTester.notifyJobCompletion(id, projectId, true, DeploymentJobs.JobType.productionUsEast3);
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", "", Request.Method.GET),
+ setDeploymentMaintainedInfo(controllerTester);
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", GET),
new File("application-without-change-multiple-deployments.json"));
}
@@ -407,63 +405,49 @@ public class ApplicationApiTest extends ControllerContainerTest {
addTenantAthenzDomain("domain1", "mytenant");
// PUT (update) non-existing tenant
- tester.assertResponse(request("/application/v4/tenant/tenant1",
- "{\"athensDomain\":\"domain1\", \"property\":\"property1\"}",
- Request.Method.PUT),
+ tester.assertResponse(request("/application/v4/tenant/tenant1", PUT)
+ .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}"),
"{\"error-code\":\"NOT_FOUND\",\"message\":\"Tenant 'tenant1' does not exist\"}",
404);
// GET non-existing tenant
- tester.assertResponse(request("/application/v4/tenant/tenant1",
- "",
- Request.Method.GET),
+ tester.assertResponse(request("/application/v4/tenant/tenant1", GET),
"{\"error-code\":\"NOT_FOUND\",\"message\":\"Tenant 'tenant1' does not exist\"}",
404);
// GET non-existing application
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1",
- "",
- Request.Method.GET),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", GET),
"{\"error-code\":\"NOT_FOUND\",\"message\":\"tenant1.application1 not found\"}",
404);
// GET non-existing deployment
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-east/instance/default",
- "",
- Request.Method.GET),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-east/instance/default", GET),
"{\"error-code\":\"NOT_FOUND\",\"message\":\"tenant1.application1 not found\"}",
404);
// POST (add) a tenant
- tester.assertResponse(request("/application/v4/tenant/tenant1",
- "{\"athensDomain\":\"domain1\", \"property\":\"property1\"}",
- Request.Method.POST),
+ tester.assertResponse(request("/application/v4/tenant/tenant1", POST)
+ .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}"),
new File("tenant-without-applications.json"));
// POST (add) another tenant under the same domain
- tester.assertResponse(request("/application/v4/tenant/tenant2",
- "{\"athensDomain\":\"domain1\", \"property\":\"property1\"}",
- Request.Method.POST),
+ tester.assertResponse(request("/application/v4/tenant/tenant2", POST)
+ .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}"),
"{\"error-code\":\"BAD_REQUEST\",\"message\":\"Could not create tenant 'tenant2': The Athens domain 'domain1' is already connected to tenant 'tenant1'\"}",
400);
// Add the same tenant again
- tester.assertResponse(request("/application/v4/tenant/tenant1",
- "{\"athensDomain\":\"domain1\", \"property\":\"property1\"}",
- Request.Method.POST),
+ tester.assertResponse(request("/application/v4/tenant/tenant1", POST)
+ .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}"),
"{\"error-code\":\"BAD_REQUEST\",\"message\":\"Tenant 'tenant1' already exists\"}",
400);
// POST (create) an (empty) application
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1",
- "",
- Request.Method.POST),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", POST),
new File("application-reference.json"));
// Create the same application again
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1",
- "",
- Request.Method.POST),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", POST),
"{\"error-code\":\"BAD_REQUEST\",\"message\":\"An application with id 'tenant1.application1' already exists\"}",
400);
@@ -472,64 +456,56 @@ public class ApplicationApiTest extends ControllerContainerTest {
// POST (deploy) an application with an invalid application package
HttpEntity entity = createApplicationDeployData(applicationPackage, Optional.empty());
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/dev/region/us-west-1/instance/default/deploy",
- entity,
- Request.Method.POST,
- athenzUserDomain, "mytenant"),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/dev/region/us-west-1/instance/default/deploy", POST)
+ .data(entity)
+ .domain(athenzUserDomain).user("mytenant"),
new File("deploy-failure.json"), 400);
// POST (deploy) an application without available capacity
configServer.throwOnNextPrepare(new ConfigServerException(new URI("server-url"), "Failed to prepare application", ConfigServerException.ErrorCode.OUT_OF_CAPACITY, null));
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/dev/region/us-west-1/instance/default/deploy",
- entity,
- Request.Method.POST,
- athenzUserDomain, "mytenant"),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/dev/region/us-west-1/instance/default/deploy", POST)
+ .data(entity)
+ .domain(athenzUserDomain).user("mytenant"),
new File("deploy-out-of-capacity.json"), 400);
// POST (deploy) an application where activation fails
configServer.throwOnNextPrepare(new ConfigServerException(new URI("server-url"), "Failed to activate application", ConfigServerException.ErrorCode.ACTIVATION_CONFLICT, null));
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/dev/region/us-west-1/instance/default/deploy",
- entity,
- Request.Method.POST,
- athenzUserDomain, "mytenant"),
- new File("deploy-activation-conflict.json"), 409);
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/dev/region/us-west-1/instance/default/deploy", POST)
+ .data(entity)
+ .domain(athenzUserDomain).user("mytenant"),
+ new File("deploy-activation-conflict.json"), 409);
// POST (deploy) an application where we get an internal server error
configServer.throwOnNextPrepare(new ConfigServerException(new URI("server-url"), "Internal server error", ConfigServerException.ErrorCode.INTERNAL_SERVER_ERROR, null));
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/dev/region/us-west-1/instance/default/deploy",
- entity,
- Request.Method.POST,
- athenzUserDomain, "mytenant"),
- new File("deploy-internal-server-error.json"), 500);
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/dev/region/us-west-1/instance/default/deploy", POST)
+ .data(entity)
+ .domain(athenzUserDomain).user("mytenant"),
+ new File("deploy-internal-server-error.json"), 500);
// DELETE tenant which has an application
- tester.assertResponse(request("/application/v4/tenant/tenant1", "", Request.Method.DELETE),
+ tester.assertResponse(request("/application/v4/tenant/tenant1", DELETE),
"{\"error-code\":\"BAD_REQUEST\",\"message\":\"Could not delete tenant 'tenant1': This tenant has active applications\"}",
400);
// DELETE application
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1",
- "",
- Request.Method.DELETE),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", DELETE),
"");
// DELETE application again - should produce 404
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1",
- "",
- Request.Method.DELETE),
- "{\"error-code\":\"NOT_FOUND\",\"message\":\"Could not delete application 'tenant1.application1': Application not found\"}",
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", DELETE),
+ "{\"error-code\":\"NOT_FOUND\",\"message\":\"Could not delete application 'tenant1.application1': Application not found\"}",
404);
// DELETE tenant
- tester.assertResponse(request("/application/v4/tenant/tenant1", "", Request.Method.DELETE),
+ tester.assertResponse(request("/application/v4/tenant/tenant1", DELETE),
new File("tenant-without-applications.json"));
// DELETE tenant again - should produce 404
- tester.assertResponse(request("/application/v4/tenant/tenant1", "", Request.Method.DELETE),
- "{\"error-code\":\"NOT_FOUND\",\"message\":\"Could not delete tenant 'tenant1': Tenant not found\"}",
+ tester.assertResponse(request("/application/v4/tenant/tenant1", DELETE),
+ "{\"error-code\":\"NOT_FOUND\",\"message\":\"Could not delete tenant 'tenant1': Tenant not found\"}",
404);
// Promote application chef env for nonexistent tenant/application
- tester.assertResponse(request("/application/v4/tenant/dontexist/application/dontexist/environment/prod/region/us-west-1/instance/default/promote", "", Request.Method.POST),
- "{\"error-code\":\"INTERNAL_SERVER_ERROR\",\"message\":\"Unable to promote Chef environments for application\"}",
- 500);
+ tester.assertResponse(request("/application/v4/tenant/dontexist/application/dontexist/environment/prod/region/us-west-1/instance/default/promote", POST),
+ "{\"error-code\":\"INTERNAL_SERVER_ERROR\",\"message\":\"Unable to promote Chef environments for application\"}",
+ 500);
}
@Test
@@ -539,102 +515,85 @@ public class ApplicationApiTest extends ControllerContainerTest {
String unauthorizedUser = "othertenant";
// Mutation without an authorized user is disallowed
- tester.assertResponse(request("/application/v4/tenant/tenant1",
- "{\"athensDomain\":\"domain1\", \"property\":\"property1\"}",
- Request.Method.POST,
- "domain1", null),
+ tester.assertResponse(request("/application/v4/tenant/tenant1", POST)
+ .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}")
+ .domain("domain1").user(null),
"{\"error-code\":\"FORBIDDEN\",\"message\":\"User is not authenticated\"}",
403);
// ... but read methods are allowed
- tester.assertResponse(request("/application/v4/tenant/",
- "{\"athensDomain\":\"domain1\", \"property\":\"property1\"}",
- Request.Method.GET,
- "domain1", null),
+ tester.assertResponse(request("/application/v4/tenant/", GET)
+ .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}")
+ .domain("domain1").user(null),
"[]",
200);
addTenantAthenzDomain("domain1", "mytenant");
// Creating a tenant for an Athens domain the user is not admin for is disallowed
- tester.assertResponse(request("/application/v4/tenant/tenant1",
- "{\"athensDomain\":\"domain1\", \"property\":\"property1\"}",
- Request.Method.POST,
- "domain1", unauthorizedUser),
+ tester.assertResponse(request("/application/v4/tenant/tenant1", POST)
+ .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}")
+ .domain("domain1").user(unauthorizedUser),
"{\"error-code\":\"FORBIDDEN\",\"message\":\"The user 'othertenant' is not admin in Athenz domain 'domain1'\"}",
403);
// (Create it with the right tenant id)
- tester.assertResponse(request("/application/v4/tenant/tenant1",
- "{\"athensDomain\":\"domain1\", \"property\":\"property1\"}",
- Request.Method.POST,
- "domain1", authorizedUser),
+ tester.assertResponse(request("/application/v4/tenant/tenant1", POST)
+ .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}")
+ .domain("domain1").user(authorizedUser),
new File("tenant-without-applications.json"),
200);
// Creating an application for an Athens domain the user is not admin for is disallowed
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1",
- "",
- Request.Method.POST,
- "domain1", unauthorizedUser),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", POST)
+ .domain("domain1").user(unauthorizedUser),
"{\"error-code\":\"FORBIDDEN\",\"message\":\"User othertenant does not have write access to tenant tenant1\"}",
403);
// (Create it with the right tenant id)
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1",
- "",
- Request.Method.POST,
- "domain1", authorizedUser),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", POST)
+ .domain("domain1").user(authorizedUser),
new File("application-reference.json"),
200);
// Deploy to an authorized zone by a user tenant is disallowed
HttpEntity entity = createApplicationDeployData(applicationPackage, Optional.empty());
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-west-1/instance/default/deploy",
- entity,
- Request.Method.POST,
- athenzUserDomain, "mytenant"),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-west-1/instance/default/deploy", POST)
+ .data(entity)
+ .domain(athenzUserDomain).user("mytenant"),
"{\"error-code\":\"FORBIDDEN\",\"message\":\"Principal 'mytenant' is not a Screwdriver principal. Excepted principal with Athenz domain 'cd.screwdriver.project', got 'domain1'.\"}",
403);
// Deleting an application for an Athens domain the user is not admin for is disallowed
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1",
- "",
- Request.Method.DELETE,
- "domain1", unauthorizedUser),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", DELETE)
+ .domain("domain1").user(unauthorizedUser),
"{\"error-code\":\"FORBIDDEN\",\"message\":\"User othertenant does not have write access to tenant tenant1\"}",
403);
// (Deleting it with the right tenant id)
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1",
- "",
- Request.Method.DELETE,
- "domain1", authorizedUser),
+ tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", DELETE)
+ .domain("domain1").user(authorizedUser),
"",
200);
// Updating a tenant for an Athens domain the user is not admin for is disallowed
- tester.assertResponse(request("/application/v4/tenant/tenant1",
- "{\"athensDomain\":\"domain1\", \"property\":\"property1\"}",
- Request.Method.PUT,
- "domain1", unauthorizedUser),
+ tester.assertResponse(request("/application/v4/tenant/tenant1", PUT)
+ .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}")
+ .domain("domain1").user(unauthorizedUser),
"{\"error-code\":\"FORBIDDEN\",\"message\":\"User othertenant does not have write access to tenant tenant1\"}",
403);
// Change Athens domain
addTenantAthenzDomain("domain2", "mytenant");
- tester.assertResponse(request("/application/v4/tenant/tenant1",
- "{\"athensDomain\":\"domain2\", \"property\":\"property1\"}",
- Request.Method.PUT,
- "domain1", authorizedUser),
- "{\"type\":\"ATHENS\",\"athensDomain\":\"domain2\",\"property\":\"property1\",\"applications\":[]}",
+ tester.assertResponse(request("/application/v4/tenant/tenant1", PUT)
+ .data("{\"athensDomain\":\"domain2\", \"property\":\"property1\"}")
+ .domain("domain1").user(authorizedUser),
+ "{\"tenant\":\"tenant1\",\"type\":\"ATHENS\",\"athensDomain\":\"domain2\",\"property\":\"property1\",\"applications\":[]}",
200);
// Deleting a tenant for an Athens domain the user is not admin for is disallowed
- tester.assertResponse(request("/application/v4/tenant/tenant1",
- "",
- Request.Method.DELETE,
- "domain1", unauthorizedUser),
+ tester.assertResponse(request("/application/v4/tenant/tenant1", DELETE)
+ .domain("domain1").user(unauthorizedUser),
"{\"error-code\":\"FORBIDDEN\",\"message\":\"User othertenant does not have write access to tenant tenant1\"}",
403);
}
@@ -669,33 +628,53 @@ public class ApplicationApiTest extends ControllerContainerTest {
"}";
}
-
- /** Make a request with (athens) user domain1.mytenant1 */
- private Request request(String path, String data, Request.Method method) {
- return request(path, data.getBytes(StandardCharsets.UTF_8), method, "domain1", "mytenant", "application/json");
- }
- private Request request(String path, String data, Request.Method method, String domain, String user) {
- return request(path, data.getBytes(StandardCharsets.UTF_8), method, domain, user, "application/json");
- }
+ private static class RequestBuilder implements Supplier<Request> {
- private Request request(String path, byte[] data, Request.Method method, String domain, String user, String contentType) {
- // user and domain parameters are translated to a Principal by MockAuthorizer as we do not run HTTP filters
- Request request = new Request("http://localhost:8080" + path + "?domain=" + domain +
- (user != null ? "&user=" + user : ""),
- data, method);
- request.getHeaders().put("Content-Type", contentType);
- return request;
- }
+ private final String path;
+ private final Request.Method method;
+ private byte[] data = new byte[0];
+ private String domain = "domain1";
+ private String user = "mytenant";
+ private String contentType = "application/json";
+ private String recursive;
- private Request request(String path, HttpEntity data, Request.Method method, String domain, String user) {
- ByteArrayOutputStream out = new ByteArrayOutputStream();
- try {
- data.writeTo(out);
- } catch (IOException e) {
- throw new UncheckedIOException(e);
+ private RequestBuilder(String path, Request.Method method) {
+ this.path = path;
+ this.method = method;
}
- return request(path, out.toByteArray(), method, domain, user, data.getContentType().getValue());
+
+ private RequestBuilder data(byte[] data) { this.data = data; return this; }
+ private RequestBuilder data(String data) { return data(data.getBytes(StandardCharsets.UTF_8)); }
+ private RequestBuilder data(HttpEntity data) {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ try {
+ data.writeTo(out);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ return data(out.toByteArray()).contentType(data.getContentType().getValue());
+ }
+ private RequestBuilder domain(String domain) { this.domain = domain; return this; }
+ private RequestBuilder user(String user) { this.user = user; return this; }
+ private RequestBuilder contentType(String contentType) { this.contentType = contentType; return this; }
+ private RequestBuilder recursive(String recursive) { this.recursive = recursive; return this; }
+
+ @Override
+ public Request get() {
+ Request request = new Request("http://localhost:8080" + path +
+ // user and domain parameters are translated to a Principal by MockAuthorizer as we do not run HTTP filters
+ "?domain=" + domain + (user == null ? "" : "&user=" + user) +
+ (recursive == null ? "" : "&recursive=" + recursive),
+ data, method);
+ request.getHeaders().put("Content-Type", contentType);
+ return request;
+ }
+ }
+
+ /** Make a request with (athens) user domain1.mytenant */
+ private RequestBuilder request(String path, Request.Method method) {
+ return new RequestBuilder(path, method);
}
/**
@@ -734,34 +713,28 @@ public class ApplicationApiTest extends ControllerContainerTest {
// system-test
String testPath = String.format("/application/v4/tenant/%s/application/%s/environment/test/region/us-east-1/instance/default",
application.tenant().value(), application.application().value());
- tester.assertResponse(request(testPath,
- deployData,
- Request.Method.POST,
- athenzScrewdriverDomain, "screwdriveruser1"),
- new File("deploy-result.json"));
- tester.assertResponse(request(testPath,
- "",
- Request.Method.DELETE),
+ tester.assertResponse(request(testPath, POST)
+ .data(deployData)
+ .domain(athenzScrewdriverDomain).user("screwdriveruser1"),
+ new File("deploy-result.json"));
+ tester.assertResponse(request(testPath, DELETE),
"Deactivated " + testPath.replaceFirst("/application/v4/", ""));
controllerTester.notifyJobCompletion(application, projectId, true, DeploymentJobs.JobType.systemTest);
// staging
String stagingPath = String.format("/application/v4/tenant/%s/application/%s/environment/staging/region/us-east-3/instance/default",
application.tenant().value(), application.application().value());
- tester.assertResponse(request(stagingPath,
- deployData,
- Request.Method.POST,
- athenzScrewdriverDomain, "screwdriveruser1"),
- new File("deploy-result.json"));
- tester.assertResponse(request(stagingPath,
- "",
- Request.Method.DELETE),
+ tester.assertResponse(request(stagingPath, POST)
+ .data(deployData)
+ .domain(athenzScrewdriverDomain).user("screwdriveruser1"),
+ new File("deploy-result.json"));
+ tester.assertResponse(request(stagingPath, DELETE),
"Deactivated " + stagingPath.replaceFirst("/application/v4/", ""));
controllerTester.notifyJobCompletion(application, projectId, true, DeploymentJobs.JobType.stagingTest);
}
/**
- * Cluster info, utilization and deployment metrics are maintained async by maintainers.
+ * Cluster info, utilization and application and deployment metrics are maintained async by maintainers.
*
* This sets these values as if the maintainers has been ran.
*
@@ -769,9 +742,9 @@ public class ApplicationApiTest extends ControllerContainerTest {
*/
private void setDeploymentMaintainedInfo(ContainerControllerTester controllerTester) {
for (Application application : controllerTester.controller().applications().asList()) {
- try (Lock lock = controllerTester.controller().applications().lock(application.id())) {
- LockedApplication lockedApplication = controllerTester.controller().applications()
- .require(application.id(), lock);
+ controllerTester.controller().applications().lockedOrThrow(application.id(), lockedApplication -> {
+ lockedApplication = lockedApplication.with(new ApplicationMetrics(0.5, 0.7));
+
for (Deployment deployment : application.deployments().values()) {
Map<ClusterSpec.Id, ClusterInfo> clusterInfo = new HashMap<>();
List<String> hostnames = new ArrayList<>();
@@ -780,13 +753,23 @@ public class ApplicationApiTest extends ControllerContainerTest {
clusterInfo.put(ClusterSpec.Id.from("cluster1"), new ClusterInfo("flavor1", 37, 2, 4, 50, ClusterSpec.Type.content, hostnames));
Map<ClusterSpec.Id, ClusterUtilization> clusterUtils = new HashMap<>();
clusterUtils.put(ClusterSpec.Id.from("cluster1"), new ClusterUtilization(0.3, 0.6, 0.4, 0.3));
- deployment = deployment.withClusterInfo(clusterInfo);
- deployment = deployment.withClusterUtils(clusterUtils);
- deployment = deployment.withMetrics(new DeploymentMetrics(1,2,3,4,5));
- controllerTester.controller().applications().store(lockedApplication.with(deployment));
+ DeploymentMetrics metrics = new DeploymentMetrics(1,2,3,4,5);
+
+ lockedApplication = lockedApplication
+ .withClusterInfo(deployment.zone(), clusterInfo)
+ .withClusterUtilization(deployment.zone(), clusterUtils)
+ .with(deployment.zone(), metrics);
}
- }
+ controllerTester.controller().applications().store(lockedApplication);
+ });
}
}
+ private void addPropertyData(MockOrganization organization, String propertyIdValue) {
+ PropertyId propertyId = new PropertyId(propertyIdValue);
+ organization.addProperty(propertyId);
+ organization.setContactsFor(propertyId, Arrays.asList(Collections.singletonList(User.from("alice")),
+ Collections.singletonList(User.from("bob"))));
+ }
+
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-without-change-multiple-deployments.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-without-change-multiple-deployments.json
index fe9c373b7d5..6442ddf5c02 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-without-change-multiple-deployments.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-without-change-multiple-deployments.json
@@ -1,4 +1,6 @@
{
+ "application": "application1",
+ "instance": "default",
"deploymentJobs": [
{
"type": "component",
@@ -30,7 +32,7 @@
"gitCommit": "commit1"
}
},
- "reason": "component completed successfully in build 42",
+ "reason": "component completed",
"at": "(ignore)"
},
"lastCompleted": {
@@ -44,7 +46,7 @@
"gitCommit": "commit1"
}
},
- "reason": "component completed successfully in build 42",
+ "reason": "component completed",
"at": "(ignore)"
},
"lastSuccess": {
@@ -58,7 +60,7 @@
"gitCommit": "commit1"
}
},
- "reason": "component completed successfully in build 42",
+ "reason": "component completed",
"at": "(ignore)"
}
},
@@ -76,7 +78,7 @@
"gitCommit": "commit1"
}
},
- "reason":"systemTest completed successfully in build 42",
+ "reason":"system-test completed",
"at": "(ignore)"
},
"lastCompleted": {
@@ -90,7 +92,7 @@
"gitCommit": "commit1"
}
},
- "reason":"systemTest completed successfully in build 42",
+ "reason":"system-test completed",
"at": "(ignore)"
},
"lastSuccess": {
@@ -104,7 +106,7 @@
"gitCommit": "commit1"
}
},
- "reason":"systemTest completed successfully in build 42",
+ "reason":"system-test completed",
"at": "(ignore)"
}
},
@@ -122,7 +124,7 @@
"gitCommit": "commit1"
}
},
- "reason":"stagingTest completed successfully in build 42",
+ "reason":"staging-test completed",
"at": "(ignore)"
},
"lastCompleted": {
@@ -136,7 +138,7 @@
"gitCommit": "commit1"
}
},
- "reason":"stagingTest completed successfully in build 42",
+ "reason":"staging-test completed",
"at": "(ignore)"
},
"lastSuccess": {
@@ -150,7 +152,7 @@
"gitCommit": "commit1"
}
},
- "reason":"stagingTest completed successfully in build 42",
+ "reason":"staging-test completed",
"at": "(ignore)"
}
},
@@ -168,7 +170,7 @@
"gitCommit": "commit1"
}
},
- "reason":"productionUsWest1 completed successfully in build 42",
+ "reason":"production-us-west-1 completed",
"at": "(ignore)"
},
"lastCompleted": {
@@ -182,7 +184,7 @@
"gitCommit": "commit1"
}
},
- "reason":"productionUsWest1 completed successfully in build 42",
+ "reason":"production-us-west-1 completed",
"at": "(ignore)"
},
"lastSuccess": {
@@ -196,7 +198,7 @@
"gitCommit": "commit1"
}
},
- "reason":"productionUsWest1 completed successfully in build 42",
+ "reason":"production-us-west-1 completed",
"at": "(ignore)"
}
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application.json
index 3dca8103ed7..fdd3dcc4d5c 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application.json
@@ -1,4 +1,6 @@
{
+ "application": "application1",
+ "instance": "default",
"deploying": {
"version": "(ignore)"
},
@@ -63,7 +65,7 @@
"gitCommit": "commit1"
}
},
- "reason": "systemTest completed successfully in build 42",
+ "reason": "system-test completed",
"at": "(ignore)"
},
"lastCompleted": {
@@ -77,7 +79,7 @@
"gitCommit": "commit1"
}
},
- "reason": "systemTest completed successfully in build 42",
+ "reason": "system-test completed",
"at": "(ignore)"
},
"lastSuccess": {
@@ -91,7 +93,7 @@
"gitCommit": "commit1"
}
},
- "reason": "systemTest completed successfully in build 42",
+ "reason": "system-test completed",
"at": "(ignore)"
}
},
@@ -109,7 +111,7 @@
"gitCommit": "commit1"
}
},
- "reason": "Retrying as build 42 just started failing",
+ "reason": "Immediate retry on failure",
"at": "(ignore)"
},
"lastCompleted": {
@@ -123,7 +125,7 @@
"gitCommit": "commit1"
}
},
- "reason": "stagingTest completed successfully in build 42",
+ "reason": "staging-test completed",
"at": "(ignore)"
},
"firstFailing": {
@@ -137,7 +139,7 @@
"gitCommit": "commit1"
}
},
- "reason": "stagingTest completed successfully in build 42",
+ "reason": "staging-test completed",
"at": "(ignore)"
}
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application1-recursive.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application1-recursive.json
new file mode 100644
index 00000000000..41556c04209
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application1-recursive.json
@@ -0,0 +1,161 @@
+{
+ "application": "application1",
+ "instance": "default",
+ "deploying": {
+ "version": "6.1"
+ },
+ "deploymentJobs": [
+ {
+ "type": "system-test",
+ "success": true,
+ "lastTriggered": {
+ "id": -1,
+ "version": "6.1.0",
+ "revision": {
+ "hash": "(ignore)",
+ "source": {
+ "gitRepository": "repository1",
+ "gitBranch": "master",
+ "gitCommit": "commit1"
+ }
+ },
+ "reason": "",
+ "at": "(ignore)"
+ },
+ "lastCompleted": {
+ "id": 42,
+ "version": "6.1.0",
+ "revision": {
+ "hash": "(ignore)",
+ "source": {
+ "gitRepository": "repository1",
+ "gitBranch": "master",
+ "gitCommit": "commit1"
+ }
+ },
+ "reason": "",
+ "at": "(ignore)"
+ },
+ "lastSuccess": {
+ "id": 42,
+ "version": "6.1.0",
+ "revision": {
+ "hash": "(ignore)",
+ "source": {
+ "gitRepository": "repository1",
+ "gitBranch": "master",
+ "gitCommit": "commit1"
+ }
+ },
+ "reason": "",
+ "at": "(ignore)"
+ }
+ },
+ {
+ "type": "staging-test",
+ "success": true,
+ "lastTriggered": {
+ "id": -1,
+ "version": "6.1.0",
+ "revision": {
+ "hash": "(ignore)",
+ "source": {
+ "gitRepository": "repository1",
+ "gitBranch": "master",
+ "gitCommit": "commit1"
+ }
+ },
+ "reason": "system-test completed",
+ "at": "(ignore)"
+ },
+ "lastCompleted": {
+ "id": 42,
+ "version": "6.1.0",
+ "revision": {
+ "hash": "(ignore)",
+ "source": {
+ "gitRepository": "repository1",
+ "gitBranch": "master",
+ "gitCommit": "commit1"
+ }
+ },
+ "reason": "system-test completed",
+ "at": "(ignore)"
+ },
+ "lastSuccess": {
+ "id": 42,
+ "version": "6.1.0",
+ "revision": {
+ "hash": "(ignore)",
+ "source": {
+ "gitRepository": "repository1",
+ "gitBranch": "master",
+ "gitCommit": "commit1"
+ }
+ },
+ "reason": "system-test completed",
+ "at": "(ignore)"
+ }
+ },
+ {
+ "type": "production-corp-us-east-1",
+ "success": false,
+ "lastTriggered": {
+ "id": -1,
+ "version": "6.1.0",
+ "revision": {
+ "hash": "(ignore)",
+ "source": {
+ "gitRepository": "repository1",
+ "gitBranch": "master",
+ "gitCommit": "commit1"
+ }
+ },
+ "reason": "Immediate retry on failure",
+ "at": "(ignore)"
+ },
+ "lastCompleted": {
+ "id": 42,
+ "version": "6.1.0",
+ "revision": {
+ "hash": "(ignore)",
+ "source": {
+ "gitRepository": "repository1",
+ "gitBranch": "master",
+ "gitCommit": "commit1"
+ }
+ },
+ "reason": "staging-test completed",
+ "at": "(ignore)"
+ },
+ "firstFailing": {
+ "id": 42,
+ "version": "6.1.0",
+ "revision": {
+ "hash": "(ignore)",
+ "source": {
+ "gitRepository": "repository1",
+ "gitBranch": "master",
+ "gitCommit": "commit1"
+ }
+ },
+ "reason": "staging-test completed",
+ "at": "(ignore)"
+ }
+ }
+ ],
+ "compileVersion": "6.1.0",
+ "globalRotations": [
+ "http://fake-global-rotation-tenant1.application1"
+ ],
+ "instances": [
+ @include(dev-us-west-1.json),
+ @include(prod-corp-us-east-1.json)
+ ],
+ "metrics": {
+ "queryServiceQuality": 0.5,
+ "writeServiceQuality": 0.7
+ },
+ "ownershipIssueId": "321",
+ "deploymentIssueId": "123"
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/dev-us-west-1.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/dev-us-west-1.json
new file mode 100644
index 00000000000..062f4408518
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/dev-us-west-1.json
@@ -0,0 +1,63 @@
+{
+ "environment": "dev",
+ "region": "us-west-1",
+ "instance": "default",
+ "serviceUrls": [
+ "http://old-endpoint.vespa.yahooapis.com:4080",
+ "http://qrs-endpoint.vespa.yahooapis.com:4080",
+ "http://feeding-endpoint.vespa.yahooapis.com:4080",
+ "http://global-endpoint.vespa.yahooapis.com:4080",
+ "http://alias-endpoint.vespa.yahooapis.com:4080"
+ ],
+ "nodes": "http://localhost:8080/zone/v2/dev/us-west-1/nodes/v2/node/%3F&recursive=true&application=tenant1.application1.default",
+ "yamasUrl": "http://monitoring-system.test/?environment=dev&region=us-west-1&application=tenant1.application1",
+ "version": "6.1.0",
+ "revision": "(ignore)",
+ "deployTimeEpochMs": "(ignore)",
+ "screwdriverId": "123",
+
+
+ "cost": {
+ "tco": 74,
+ "waste": 0,
+ "utilization": 2.999999999999999,
+ "cluster": {
+ "cluster1": {
+ "count": 2,
+ "resource": "cpu",
+ "utilization": 2.999999999999999,
+ "tco": 74,
+ "waste": 0,
+ "flavor": "flavor1",
+ "flavorCost": 37.0,
+ "flavorCpu": 2.0,
+ "flavorMem": 4.0,
+ "flavorDisk": 50.0,
+ "type": "content",
+ "util": {
+ "cpu": 2.999999999999999,
+ "mem": 0.4285714285714286,
+ "disk": 0.5714285714285715,
+ "diskBusy": 1.0
+ },
+ "usage": {
+ "cpu": 0.6,
+ "mem": 0.3,
+ "disk": 0.4,
+ "diskBusy": 0.3
+ },
+ "hostnames": [
+ "host1",
+ "host2"
+ ]
+ }
+ }
+ },
+ "metrics": {
+ "queriesPerSecond": 1.0,
+ "writesPerSecond": 2.0,
+ "documentCount": 3.0,
+ "queryLatencyMillis": 4.0,
+ "writeLatencyMillis": 5.0
+ }
+} \ No newline at end of file
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/opsdb-tenant-with-id-without-applications.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/opsdb-tenant-with-id-without-applications.json
index 8acb4a045f3..a2e70d9c1eb 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/opsdb-tenant-with-id-without-applications.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/opsdb-tenant-with-id-without-applications.json
@@ -1,4 +1,5 @@
{
+ "tenant": "tenant3",
"type": "OPSDB",
"property": "property1",
"propertyId": "1234",
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/opsdb-tenant-with-new-id-without-applications.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/opsdb-tenant-with-new-id-without-applications.json
index 3f4b6017971..f9161ea49b1 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/opsdb-tenant-with-new-id-without-applications.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/opsdb-tenant-with-new-id-without-applications.json
@@ -1,4 +1,5 @@
{
+ "tenant": "tenant3",
"type": "OPSDB",
"property": "property2",
"propertyId": "4321",
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/prod-corp-us-east-1.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/prod-corp-us-east-1.json
new file mode 100644
index 00000000000..75b257da0ed
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/prod-corp-us-east-1.json
@@ -0,0 +1,68 @@
+{
+ "environment": "prod",
+ "region": "corp-us-east-1",
+ "instance": "default",
+ "bcpStatus": {
+ "rotationStatus": "UNKNOWN"
+ },
+ "serviceUrls": [
+ "http://old-endpoint.vespa.yahooapis.com:4080",
+ "http://qrs-endpoint.vespa.yahooapis.com:4080",
+ "http://feeding-endpoint.vespa.yahooapis.com:4080",
+ "http://global-endpoint.vespa.yahooapis.com:4080",
+ "http://alias-endpoint.vespa.yahooapis.com:4080"
+ ],
+ "nodes": "http://localhost:8080/zone/v2/prod/corp-us-east-1/nodes/v2/node/%3F&recursive=true&application=tenant1.application1.default",
+ "elkUrl": "http://log.prod.corp-us-east-1.test/#/discover?_g=()&_a=(columns:!(_source),index:'logstash-*',interval:auto,query:(query_string:(analyze_wildcard:!t,query:'HV-tenant:%22tenant1%22%20AND%20HV-application:%22application1%22%20AND%20HV-region:%22corp-us-east-1%22%20AND%20HV-instance:%22default%22%20AND%20HV-environment:%22prod%22')),sort:!('@timestamp',desc))",
+ "yamasUrl": "http://monitoring-system.test/?environment=prod&region=corp-us-east-1&application=tenant1.application1",
+ "version": "6.1.0",
+ "revision": "(ignore)",
+ "deployTimeEpochMs": "(ignore)",
+ "screwdriverId": "123",
+ "gitRepository": "repository1",
+ "gitBranch": "master",
+ "gitCommit": "commit1",
+ "cost": {
+ "tco": 74,
+ "waste": 0,
+ "utilization": 2.999999999999999,
+ "cluster": {
+ "cluster1": {
+ "count": 2,
+ "resource": "cpu",
+ "utilization": 2.999999999999999,
+ "tco": 74,
+ "waste": 0,
+ "flavor": "flavor1",
+ "flavorCost": 37.0,
+ "flavorCpu": 2.0,
+ "flavorMem": 4.0,
+ "flavorDisk": 50.0,
+ "type": "content",
+ "util": {
+ "cpu": 2.999999999999999,
+ "mem": 0.4285714285714286,
+ "disk": 0.5714285714285715,
+ "diskBusy": 1.0
+ },
+ "usage": {
+ "cpu": 0.6,
+ "mem": 0.3,
+ "disk": 0.4,
+ "diskBusy": 0.3
+ },
+ "hostnames": [
+ "host1",
+ "host2"
+ ]
+ }
+ }
+ },
+ "metrics": {
+ "queriesPerSecond": 1.0,
+ "writesPerSecond": 2.0,
+ "documentCount": 3.0,
+ "queryLatencyMillis": 4.0,
+ "writeLatencyMillis": 5.0
+ }
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/recursive-root.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/recursive-root.json
new file mode 100644
index 00000000000..a4395faede4
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/recursive-root.json
@@ -0,0 +1,5 @@
+[
+ @include(tenant2.json),
+ @include(tenant3.json),
+ @include(tenant1-recursive.json)
+]
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/recursive-until-tenant-root.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/recursive-until-tenant-root.json
new file mode 100644
index 00000000000..35ed8181fac
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/recursive-until-tenant-root.json
@@ -0,0 +1,6 @@
+[
+ @include(tenant2.json),
+ @include(tenant3.json),
+ @include(tenant-with-application.json)
+]
+
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-with-application.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-with-application.json
index 87901218c2e..ad8e65692b4 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-with-application.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-with-application.json
@@ -1,4 +1,5 @@
{
+ "tenant": "tenant1",
"type": "ATHENS",
"athensDomain": "domain1",
"property": "property1",
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-without-applications-with-id.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-without-applications-with-id.json
index ede2413218d..69949c47d8c 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-without-applications-with-id.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-without-applications-with-id.json
@@ -1,4 +1,5 @@
{
+ "tenant": "tenant2",
"type": "ATHENS",
"athensDomain": "domain2",
"property": "property2",
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-without-applications.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-without-applications.json
index 69669b5dfb8..3ad5a307348 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-without-applications.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-without-applications.json
@@ -1,4 +1,5 @@
{
+ "tenant": "tenant1",
"type": "ATHENS",
"athensDomain": "domain1",
"property": "property1",
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant1-recursive.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant1-recursive.json
new file mode 100644
index 00000000000..309177e6285
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant1-recursive.json
@@ -0,0 +1,9 @@
+{
+ "tenant": "tenant1",
+ "type": "ATHENS",
+ "athensDomain": "domain1",
+ "property": "property1",
+ "applications": [
+ @include(application1-recursive.json)
+ ]
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant2.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant2.json
new file mode 100644
index 00000000000..6e66202b70d
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant2.json
@@ -0,0 +1,19 @@
+{
+ "tenant": "tenant2",
+ "type": "ATHENS",
+ "athensDomain": "domain2",
+ "property": "property2",
+ "propertyId": "1234",
+ "applications": [],
+ "propertyUrl": "www.properties.tld/1234",
+ "contactsUrl": "www.contacts.tld/1234",
+ "issueCreationUrl": "www.issues.tld/1234",
+ "contacts": [
+ [
+ "alice"
+ ],
+ [
+ "bob"
+ ]
+ ]
+} \ No newline at end of file
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant3.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant3.json
new file mode 100644
index 00000000000..fdf3ca490f4
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant3.json
@@ -0,0 +1,12 @@
+{
+ "tenant": "tenant3",
+ "type": "OPSDB",
+ "property": "property2",
+ "propertyId": "4321",
+ "userGroup": "group1",
+ "applications": [],
+ "propertyUrl": "www.properties.tld/4321",
+ "contactsUrl": "www.contacts.tld/4321",
+ "issueCreationUrl": "www.issues.tld/4321",
+ "contacts": []
+} \ No newline at end of file
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/maintenance.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/maintenance.json
index 3633860772b..354bab4379c 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/maintenance.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/maintenance.json
@@ -1,19 +1,16 @@
{
"jobs": [
{
- "name": "DelayedDeployer"
+ "name": "ApplicationOwnershipConfirmer"
},
{
- "name": "BlockedChangeDeployer"
- },
- {
- "name": "Upgrader"
+ "name": "ClusterInfoMaintainer"
},
{
- "name": "FailureRedeployer"
+ "name": "ClusterUtilizationMaintainer"
},
{
- "name": "VersionStatusUpdater"
+ "name": "DeploymentExpirer"
},
{
"name": "DeploymentIssueReporter"
@@ -22,22 +19,22 @@
"name": "DeploymentMetricsMaintainer"
},
{
- "name": "OutstandingChangeDeployer"
+ "name": "MetricsReporter"
},
{
- "name": "ClusterUtilizationMaintainer"
+ "name": "OutstandingChangeDeployer"
},
{
- "name": "ClusterInfoMaintainer"
+ "name": "ReadyJobsTrigger"
},
{
- "name": "DeploymentExpirer"
+ "name": "Upgrader"
},
{
- "name": "MetricsReporter"
+ "name": "VersionStatusUpdater"
}
],
"inactive": [
"DeploymentExpirer"
]
-} \ No newline at end of file
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/root.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/root.json
index 7fd000b82c5..5f7fedfd75f 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/root.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/root.json
@@ -1,27 +1,26 @@
{
"versions":[
{
- "version":"(ignore)",
- "confidence":"high",
- "commit":"(ignore)",
- "date":0,
- "controllerVersion":false,
- "systemVersion":false,
- "configServers":[
-
- ],
- "failingApplications":[
-
- ],
- "productionApplications":[
+ "version": "(ignore)",
+ "confidence": "high",
+ "commit": "(ignore)",
+ "date": 0,
+ "controllerVersion": false,
+ "systemVersion": false,
+ "configServers": [ ],
+ "failingApplications": [ ],
+ "productionApplications": [
{
- "tenant":"tenant1",
- "application":"application1",
- "instance":"default",
- "url":"http://localhost:8080/application/v4/tenant/tenant1/application/application1",
- "upgradePolicy":"default"
+ "tenant": "tenant1",
+ "application": "application1",
+ "instance": "default",
+ "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1",
+ "upgradePolicy": "default",
+ "productionJobs": 1,
+ "productionSuccesses": 1
}
- ]
+ ],
+ "deployingApplications": [ ]
},
{
"version":"(ignore)",
@@ -40,40 +39,47 @@
],
"failingApplications":[
{
- "tenant":"tenant1",
- "application":"application1",
- "instance":"default",
- "url":"http://localhost:8080/application/v4/tenant/tenant1/application/application1",
- "upgradePolicy":"default",
- "failingSince":"(ignore)"
+ "tenant": "tenant1",
+ "application": "application1",
+ "instance": "default",
+ "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1",
+ "upgradePolicy": "default",
+ "failing": "staging-test"
}
],
"productionApplications":[
{
- "tenant":"tenant2",
- "application":"application2",
- "instance":"default",
- "url":"http://localhost:8080/application/v4/tenant/tenant2/application/application2",
- "upgradePolicy":"default"
+ "tenant": "tenant2",
+ "application": "application2",
+ "instance": "default",
+ "url": "http://localhost:8080/application/v4/tenant/tenant2/application/application2",
+ "upgradePolicy": "default",
+ "productionJobs": 1,
+ "productionSuccesses": 1
+ }
+ ],
+ "deployingApplications": [
+ {
+ "tenant": "tenant1",
+ "application": "application1",
+ "instance": "default",
+ "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1",
+ "upgradePolicy": "default",
+ "running": "staging-test"
}
]
},
{
- "version":"(ignore)",
- "confidence":"normal",
- "commit":"(ignore)",
- "date":0,
- "controllerVersion":true,
- "systemVersion":false,
- "configServers":[
-
- ],
- "failingApplications":[
-
- ],
- "productionApplications":[
-
- ]
+ "version": "(ignore)",
+ "confidence": "normal",
+ "commit": "(ignore)",
+ "date": 0,
+ "controllerVersion": true,
+ "systemVersion": false,
+ "configServers": [ ],
+ "failingApplications": [ ],
+ "productionApplications": [ ],
+ "deployingApplications": [ ]
}
]
} \ No newline at end of file
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/ScrewdriverApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/ScrewdriverApiTest.java
index 1638a2845ed..e6b3eacd44e 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/ScrewdriverApiTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/ScrewdriverApiTest.java
@@ -96,14 +96,14 @@ public class ScrewdriverApiTest extends ControllerContainerTest {
Response response;
response = container.handleRequest(new Request("http://localhost:8080/screwdriver/v1/jobsToRun", "", Request.Method.GET));
- assertTrue("Response contains system-test", response.getBodyAsString().contains(JobType.systemTest.id()));
- assertTrue("Response contains staging-test", response.getBodyAsString().contains(JobType.stagingTest.id()));
+ assertTrue("Response contains system-test", response.getBodyAsString().contains(JobType.systemTest.jobName()));
+ assertTrue("Response contains staging-test", response.getBodyAsString().contains(JobType.stagingTest.jobName()));
assertEquals("Response contains only two items", 2, SlimeUtils.jsonToSlime(response.getBody()).get().entries());
// Check that GET didn't affect the enqueued jobs.
response = container.handleRequest(new Request("http://localhost:8080/screwdriver/v1/jobsToRun", "", Request.Method.DELETE));
- assertTrue("Response contains system-test", response.getBodyAsString().contains(JobType.systemTest.id()));
- assertTrue("Response contains staging-test", response.getBodyAsString().contains(JobType.stagingTest.id()));
+ assertTrue("Response contains system-test", response.getBodyAsString().contains(JobType.systemTest.jobName()));
+ assertTrue("Response contains staging-test", response.getBodyAsString().contains(JobType.stagingTest.jobName()));
assertEquals("Response contains only two items", 2, SlimeUtils.jsonToSlime(response.getBody()).get().entries());
Thread.sleep(50);
@@ -148,11 +148,8 @@ public class ScrewdriverApiTest extends ControllerContainerTest {
tester.containerTester().updateSystemVersion();
Application app = tester.createApplication();
- try (Lock lock = tester.controller().applications().lock(app.id())) {
- tester.controller().applications().store(
- tester.controller().applications().require(app.id(), lock).withProjectId(1)
- );
- }
+ tester.controller().applications().lockedOrThrow(app.id(), application ->
+ tester.controller().applications().store(application.withProjectId(1)));
// Unknown application
assertResponse(new Request("http://localhost:8080/screwdriver/v1/trigger/tenant/foo/application/bar",
@@ -163,7 +160,7 @@ public class ScrewdriverApiTest extends ControllerContainerTest {
assertResponse(new Request("http://localhost:8080/screwdriver/v1/trigger/tenant/" +
app.id().tenant().value() + "/application/" + app.id().application().value(),
"invalid".getBytes(StandardCharsets.UTF_8), Request.Method.POST),
- 400, "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Unknown job id 'invalid'\"}");
+ 400, "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Unknown job name 'invalid'\"}");
// component is triggered if no job is specified in request body
assertResponse(new Request("http://localhost:8080/screwdriver/v1/trigger/tenant/" +
@@ -172,7 +169,7 @@ public class ScrewdriverApiTest extends ControllerContainerTest {
200, "{\"message\":\"Triggered component for tenant1.application1\"}");
assertFalse(buildSystem.jobs().isEmpty());
- assertEquals(JobType.component.id(), buildSystem.jobs().get(0).jobName());
+ assertEquals(JobType.component.jobName(), buildSystem.jobs().get(0).jobName());
assertEquals(1L, buildSystem.jobs().get(0).projectId());
buildSystem.takeJobsToRun();
@@ -182,7 +179,7 @@ public class ScrewdriverApiTest extends ControllerContainerTest {
"staging-test".getBytes(StandardCharsets.UTF_8), Request.Method.POST),
200, "{\"message\":\"Triggered staging-test for tenant1.application1\"}");
assertFalse(buildSystem.jobs().isEmpty());
- assertEquals(JobType.stagingTest.id(), buildSystem.jobs().get(0).jobName());
+ assertEquals(JobType.stagingTest.jobName(), buildSystem.jobs().get(0).jobName());
assertEquals(1L, buildSystem.jobs().get(0).projectId());
}
@@ -197,14 +194,14 @@ public class ScrewdriverApiTest extends ControllerContainerTest {
Optional<JobError> jobError) {
return
"{\n" +
- " \"projectId\" : " + projectId + ",\n" +
- " \"jobName\" :\"" + jobType.id() + "\",\n" +
- " \"buildNumber\" : " + buildNumber + ",\n" +
- jobError.map(message -> " \"jobError\" : \"" + message + "\",\n").orElse("") +
- " \"tenant\" :\"" + applicationId.tenant().value() + "\",\n" +
- " \"application\" :\"" + applicationId.application().value() + "\",\n" +
- " \"instance\" :\"" + applicationId.instance().value() + "\"\n" +
- "}";
+ " \"projectId\" : " + projectId + ",\n" +
+ " \"jobName\" :\"" + jobType.jobName() + "\",\n" +
+ " \"buildNumber\" : " + buildNumber + ",\n" +
+ jobError.map(message -> " \"jobError\" : \"" + message + "\",\n").orElse("") +
+ " \"tenant\" :\"" + applicationId.tenant().value() + "\",\n" +
+ " \"application\" :\"" + applicationId.application().value() + "\",\n" +
+ " \"instance\" :\"" + applicationId.instance().value() + "\"\n" +
+ "}";
}
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/responses/unexpected-completion.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/responses/unexpected-completion.json
index e293d85b594..8ffd9511a96 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/responses/unexpected-completion.json
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/responses/unexpected-completion.json
@@ -1,4 +1,4 @@
{
"error-code": "BAD_REQUEST",
- "message": "Got notified about completion of job status of productionUsEast3[ last triggered: (never), last completed: (never), first failing: (not failing), lastSuccess: (never)], but that has not been triggered nor deployed"
+ "message": "Got notified about completion of job status of productionUsEast3[ last triggered: (never), last completed: (never), first failing: (not failing), lastSuccess: (never)], but that has neither been triggered nor deployed"
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiTest.java
new file mode 100644
index 00000000000..a00665b77cb
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiTest.java
@@ -0,0 +1,65 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.restapi.zone.v1;
+
+import com.yahoo.application.container.handler.Request;
+import com.yahoo.config.provision.Environment;
+import com.yahoo.config.provision.RegionName;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.vespa.hosted.controller.ZoneRegistryMock;
+import com.yahoo.vespa.hosted.controller.restapi.ContainerControllerTester;
+import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * @author mpolden
+ */
+public class ZoneApiTest extends ControllerContainerTest {
+
+ private static final String responseFiles = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/responses/";
+ private static final List<Zone> zones = Arrays.asList(
+ new Zone(Environment.prod, RegionName.from("us-north-1")),
+ new Zone(Environment.dev, RegionName.from("us-north-2")),
+ new Zone(Environment.test, RegionName.from("us-north-3")),
+ new Zone(Environment.staging, RegionName.from("us-north-4"))
+ );
+
+ private ContainerControllerTester tester;
+
+ @Before
+ public void before() {
+ ZoneRegistryMock zoneRegistry = (ZoneRegistryMock) container.components()
+ .getComponent(ZoneRegistryMock.class.getName());
+ zoneRegistry.setDefaultRegionForEnvironment(Environment.dev, RegionName.from("us-north-2"))
+ .setZones(zones);
+ this.tester = new ContainerControllerTester(container, responseFiles);
+ }
+
+ @Test
+ public void test_requests() throws Exception {
+ // GET /zone/v1
+ tester.containerTester().assertResponse(new Request("http://localhost:8080/zone/v1"),
+ new File("root.json"));
+
+ // GET /zone/v1/environment/prod
+ tester.containerTester().assertResponse(new Request("http://localhost:8080/zone/v1/environment/prod"),
+ new File("prod.json"));
+
+ // GET /zone/v1/environment/dev/default
+ tester.containerTester().assertResponse(new Request("http://localhost:8080/zone/v1/environment/dev/default"),
+ new File("default-for-region.json"));
+ }
+
+ @Test
+ public void test_invalid_requests() throws Exception {
+ // GET /zone/v1/environment/prod/default: No default region
+ tester.containerTester().assertResponse(new Request("http://localhost:8080/zone/v1/environment/prod/default"),
+ new File("no-default-region.json"),
+ 400);
+ }
+
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/responses/default-for-region.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/responses/default-for-region.json
new file mode 100644
index 00000000000..7c4a7e2b4a5
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/responses/default-for-region.json
@@ -0,0 +1,4 @@
+{
+ "name": "us-north-2",
+ "url": "http://localhost:8080/zone/v2/environment/dev/region/us-north-2"
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/responses/no-default-region.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/responses/no-default-region.json
new file mode 100644
index 00000000000..bdc6601a2e9
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/responses/no-default-region.json
@@ -0,0 +1,4 @@
+{
+ "error-code": "BAD_REQUEST",
+ "message": "No default region for environment: prod"
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/responses/prod.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/responses/prod.json
new file mode 100644
index 00000000000..cebf48e6428
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/responses/prod.json
@@ -0,0 +1,6 @@
+[
+ {
+ "name": "us-north-1",
+ "url": "http://localhost:8080/zone/v2/environment/prod/region/us-north-1"
+ }
+]
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/responses/root.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/responses/root.json
new file mode 100644
index 00000000000..b3bd5247414
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/responses/root.json
@@ -0,0 +1,18 @@
+[
+ {
+ "name": "dev",
+ "url": "http://localhost:8080/zone/v2/environment/dev"
+ },
+ {
+ "name": "prod",
+ "url": "http://localhost:8080/zone/v2/environment/prod"
+ },
+ {
+ "name": "staging",
+ "url": "http://localhost:8080/zone/v2/environment/staging"
+ },
+ {
+ "name": "test",
+ "url": "http://localhost:8080/zone/v2/environment/test"
+ }
+]
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiTest.java
new file mode 100644
index 00000000000..63899d808f9
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiTest.java
@@ -0,0 +1,117 @@
+package com.yahoo.vespa.hosted.controller.restapi.zone.v2;
+
+import com.yahoo.application.container.handler.Request;
+import com.yahoo.application.container.handler.Request.Method;
+import com.yahoo.config.provision.Environment;
+import com.yahoo.config.provision.RegionName;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.text.Utf8;
+import com.yahoo.vespa.hosted.controller.ConfigServerProxyMock;
+import com.yahoo.vespa.hosted.controller.ZoneRegistryMock;
+import com.yahoo.vespa.hosted.controller.restapi.ContainerControllerTester;
+import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+/**
+ * @author mpolden
+ */
+public class ZoneApiTest extends ControllerContainerTest {
+
+ private static final String responseFiles = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/responses/";
+ private static final List<Zone> zones = Arrays.asList(
+ new Zone(Environment.prod, RegionName.from("us-north-1")),
+ new Zone(Environment.dev, RegionName.from("us-north-2")),
+ new Zone(Environment.test, RegionName.from("us-north-3")),
+ new Zone(Environment.staging, RegionName.from("us-north-4"))
+ );
+
+ private ContainerControllerTester tester;
+ private ConfigServerProxyMock proxy;
+
+ @Before
+ public void before() {
+ ZoneRegistryMock zoneRegistry = (ZoneRegistryMock) container.components()
+ .getComponent(ZoneRegistryMock.class.getName());
+ zoneRegistry.setDefaultRegionForEnvironment(Environment.dev, RegionName.from("us-north-2"))
+ .setZones(zones);
+ this.tester = new ContainerControllerTester(container, responseFiles);
+ this.proxy = (ConfigServerProxyMock) container.components().getComponent(ConfigServerProxyMock.class.getName());
+ }
+
+ @Test
+ public void test_requests() throws Exception {
+ // GET /zone/v2
+ tester.containerTester().assertResponse(new Request("http://localhost:8080/zone/v2"),
+ new File("root.json"));
+
+ // GET /zone/v2/prod/us-north-1
+ tester.containerTester().assertResponse(new Request("http://localhost:8080/zone/v2/prod/us-north-1"),
+ "ok");
+ assertEquals("prod", proxy.lastReceived().get().getEnvironment());
+ assertEquals("us-north-1", proxy.lastReceived().get().getRegion());
+ assertEquals("", proxy.lastReceived().get().getConfigServerRequest());
+ assertEquals("GET", proxy.lastReceived().get().getMethod());
+
+ // GET /zone/v2/nodes/v2/node/?recursive=true
+ tester.containerTester().assertResponse(new Request("http://localhost:8080/zone/v2/prod/us-north-1/nodes/v2/node/?recursive=true"),
+ "ok");
+
+ assertEquals("prod", proxy.lastReceived().get().getEnvironment());
+ assertEquals("us-north-1", proxy.lastReceived().get().getRegion());
+ assertEquals("/nodes/v2/node/?recursive=true", proxy.lastReceived().get().getConfigServerRequest());
+ assertEquals("GET", proxy.lastReceived().get().getMethod());
+
+ // POST /zone/v2/dev/us-north-2/nodes/v2/command/restart?hostname=node1
+ tester.containerTester().assertResponse(new Request("http://localhost:8080/zone/v2/dev/us-north-2/nodes/v2/command/restart?hostname=node1",
+ new byte[0], Method.POST),
+ "ok");
+ assertEquals("dev", proxy.lastReceived().get().getEnvironment());
+ assertEquals("us-north-2", proxy.lastReceived().get().getRegion());
+ assertEquals("/nodes/v2/command/restart?hostname=node1", proxy.lastReceived().get().getConfigServerRequest());
+ assertEquals("POST", proxy.lastReceived().get().getMethod());
+
+ // PUT /zone/v2/prod/us-north-1/nodes/v2/state/dirty/node1
+ tester.containerTester().assertResponse(new Request("http://localhost:8080/zone/v2/prod/us-north-1/nodes/v2/state/dirty/node1",
+ new byte[0], Method.PUT), "ok");
+ assertEquals("prod", proxy.lastReceived().get().getEnvironment());
+ assertEquals("us-north-1", proxy.lastReceived().get().getRegion());
+ assertEquals("/nodes/v2/state/dirty/node1", proxy.lastReceived().get().getConfigServerRequest());
+ assertEquals("PUT", proxy.lastReceived().get().getMethod());
+
+ // DELETE /zone/v2/prod/us-north-1/nodes/v2/node/node1
+ tester.containerTester().assertResponse(new Request("http://localhost:8080/zone/v2/prod/us-north-1/nodes/v2/node/node1",
+ new byte[0], Method.DELETE), "ok");
+ assertEquals("prod", proxy.lastReceived().get().getEnvironment());
+ assertEquals("us-north-1", proxy.lastReceived().get().getRegion());
+ assertEquals("/nodes/v2/node/node1", proxy.lastReceived().get().getConfigServerRequest());
+ assertEquals("DELETE", proxy.lastReceived().get().getMethod());
+
+ // PATCH /zone/v2/prod/us-north-1/nodes/v2/node/node1
+ tester.containerTester().assertResponse(new Request("http://localhost:8080/zone/v2/prod/us-north-1/nodes/v2/node/node1",
+ Utf8.toBytes("{\"currentRestartGeneration\": 1}"),
+ Method.PATCH), "ok");
+ assertEquals("prod", proxy.lastReceived().get().getEnvironment());
+ assertEquals("us-north-1", proxy.lastReceived().get().getRegion());
+ assertEquals("/nodes/v2/node/node1", proxy.lastReceived().get().getConfigServerRequest());
+ assertEquals("PATCH", proxy.lastReceived().get().getMethod());
+ assertEquals("{\"currentRestartGeneration\": 1}", proxy.lastRequestBody().get());
+ }
+
+ @Test
+ public void test_invalid_requests() throws Exception {
+ // GET /zone/v2/prod/us-north-34/nodes/v2
+ tester.containerTester().assertResponse(new Request("http://localhost:8080/zone/v2/prod/us-north-42/nodes/v2",
+ new byte[0], Method.POST),
+ new File("unknown-zone.json"), 400);
+ assertFalse(proxy.lastReceived().isPresent());
+ }
+
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/responses/root.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/responses/root.json
new file mode 100644
index 00000000000..ab168854267
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/responses/root.json
@@ -0,0 +1,26 @@
+{
+ "uris": [
+ "http://localhost:8080/zone/v2/prod/us-north-1",
+ "http://localhost:8080/zone/v2/dev/us-north-2",
+ "http://localhost:8080/zone/v2/test/us-north-3",
+ "http://localhost:8080/zone/v2/staging/us-north-4"
+ ],
+ "zones": [
+ {
+ "environment": "prod",
+ "region": "us-north-1"
+ },
+ {
+ "environment": "dev",
+ "region": "us-north-2"
+ },
+ {
+ "environment": "test",
+ "region": "us-north-3"
+ },
+ {
+ "environment": "staging",
+ "region": "us-north-4"
+ }
+ ]
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/responses/unknown-zone.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/responses/unknown-zone.json
new file mode 100644
index 00000000000..c7d6e4b8400
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/responses/unknown-zone.json
@@ -0,0 +1,4 @@
+{
+ "error-code": "BAD_REQUEST",
+ "message": "No such zone: prod.us-north-42"
+}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/versions/VersionStatusTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/versions/VersionStatusTest.java
index 519c457e73b..4f97c078c9b 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/versions/VersionStatusTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/versions/VersionStatusTest.java
@@ -1,6 +1,7 @@
// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.versions;
+import com.google.common.collect.ImmutableSet;
import com.yahoo.component.Version;
import com.yahoo.component.Vtag;
import com.yahoo.config.provision.Environment;
@@ -19,6 +20,8 @@ import org.junit.Test;
import java.net.URI;
import java.net.URISyntaxException;
+import java.time.Duration;
+import java.util.Collections;
import java.util.List;
import static com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobType.component;
@@ -28,6 +31,7 @@ import static com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobTy
import static com.yahoo.vespa.hosted.controller.application.DeploymentJobs.JobType.systemTest;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
/**
@@ -95,23 +99,20 @@ public class VersionStatusTest {
List<VespaVersion> versions = tester.controller().versionStatus().versions();
assertEquals("The two versions above exist", 2, versions.size());
+ System.err.println(tester.controller().applications().deploymentTrigger().jobTimeoutLimit());
+
VespaVersion v1 = versions.get(0);
assertEquals(version1, v1.versionNumber());
- assertEquals(0, v1.statistics().failing().size());
- // All applications are on v1 in at least one zone
- assertEquals(3, v1.statistics().production().size());
- assertTrue(v1.statistics().production().contains(app2.id()));
- assertTrue(v1.statistics().production().contains(app1.id()));
+ assertEquals("No applications are failing on version1.", ImmutableSet.of(), v1.statistics().failing());
+ assertEquals("All applications have at least one active production deployment on version 1.", ImmutableSet.of(app1.id(), app2.id(), app3.id()), v1.statistics().production());
+ assertEquals("No applications have active deployment jobs on version1.", ImmutableSet.of(), v1.statistics().deploying());
VespaVersion v2 = versions.get(1);
assertEquals(version2, v2.versionNumber());
- // All applications have failed on v2 in at least one zone
- assertEquals(3, v2.statistics().failing().size());
- assertTrue(v2.statistics().failing().contains(app1.id()));
- assertTrue(v2.statistics().failing().contains(app3.id()));
- // Only one application is on v2 in at least one zone
- assertEquals(1, v2.statistics().production().size());
- assertTrue(v2.statistics().production().contains(app2.id()));
+ assertEquals("All applications have failed on version2 in at least one zone.", ImmutableSet.of(app1.id(), app2.id(), app3.id()), v2.statistics().failing());
+ assertEquals("Only app2 has successfully deployed to production on version2.", ImmutableSet.of(app2.id()), v2.statistics().production());
+ // Should test the below, but can't easily be done with current test framework. This test passes in DeploymentApiTest.
+ // assertEquals("All applications are being retried on version2.", ImmutableSet.of(app1.id(), app2.id(), app3.id()), v2.statistics().deploying());
}
@Test
@@ -161,6 +162,12 @@ public class VersionStatusTest {
assertEquals("One canary failed: Broken",
Confidence.broken, confidence(tester.controller(), version1));
+ // Finish running jobs
+ tester.deployAndNotify(canary2, DeploymentTester.applicationPackage("canary"), false, systemTest);
+ tester.clock().advance(Duration.ofHours(1));
+ tester.deployAndNotify(canary1, DeploymentTester.applicationPackage("canary"), false, productionUsWest1);
+ tester.deployAndNotify(canary2, DeploymentTester.applicationPackage("canary"), false, systemTest);
+
// New version is released
Version version2 = new Version("5.2");
tester.upgradeSystem(version2);
@@ -204,6 +211,7 @@ public class VersionStatusTest {
// Another default application upgrades, raising confidence to high
tester.completeUpgrade(default8, version2, "default");
+ tester.completeUpgrade(default9, version2, "default");
tester.updateVersionStatus();
assertEquals("Confidence remains unchanged for version0: High",
@@ -241,7 +249,7 @@ public class VersionStatusTest {
}
@Test
- public void testIgnoreConfigdeince() {
+ public void testIgnoreConfidence() {
DeploymentTester tester = new DeploymentTester();
Version version0 = new Version("5.0");
@@ -270,7 +278,6 @@ public class VersionStatusTest {
tester.completeUpgradeWithError(default3, version1, "default", stagingTest);
tester.completeUpgradeWithError(default4, version1, "default", stagingTest);
tester.updateVersionStatus();
-
assertEquals("Canaries have upgraded, 1 of 4 default apps failing: Broken",
Confidence.broken, confidence(tester.controller(), version1));
@@ -295,8 +302,9 @@ public class VersionStatusTest {
Version versionWithUnknownTag = new Version("6.1.2");
Application app = tester.createAndDeploy("tenant1", "domain1","application1", Environment.test, 11);
- applications.notifyJobCompletion(mockReport(app, component, true));
- applications.notifyJobCompletion(mockReport(app, systemTest, true));
+ tester.clock().advance(Duration.ofMillis(1));
+ applications.notifyJobCompletion(DeploymentTester.jobReport(app, component, true));
+ applications.notifyJobCompletion(DeploymentTester.jobReport(app, systemTest, true));
List<VespaVersion> vespaVersions = VersionStatus.compute(tester.controller()).versions();
@@ -313,14 +321,4 @@ public class VersionStatusTest {
.orElseThrow(() -> new IllegalArgumentException("Expected to find version: " + version));
}
- private DeploymentJobs.JobReport mockReport(Application application, DeploymentJobs.JobType jobType, boolean success) {
- return new DeploymentJobs.JobReport(
- application.id(),
- jobType,
- application.deploymentJobs().projectId().get(),
- 42,
- JobError.from(success)
- );
- }
-
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/rotation/ControllerRotationRepositoryTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/rotation/ControllerRotationRepositoryTest.java
index 561799529f9..b4074fc1944 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/rotation/ControllerRotationRepositoryTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/rotation/ControllerRotationRepositoryTest.java
@@ -3,7 +3,7 @@ package com.yahoo.vespa.hosted.rotation;
import com.yahoo.config.application.api.DeploymentSpec;
import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.metrics.simple.MetricReceiver;
+import com.yahoo.jdisc.Metric;
import com.yahoo.vespa.hosted.controller.api.identifiers.RotationId;
import com.yahoo.vespa.hosted.controller.api.rotation.Rotation;
import com.yahoo.vespa.hosted.controller.persistence.ControllerDb;
@@ -22,6 +22,10 @@ import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
/**
* @author Oyvind Gronnesby
@@ -100,12 +104,13 @@ public class ControllerRotationRepositoryTest {
private ControllerRotationRepository repository;
private ControllerRotationRepository repositoryWhitespaces;
-
+ private Metric metric;
@Before
public void setup_repository() {
- repository = new ControllerRotationRepository(rotationsConfig, controllerDb, MetricReceiver.nullImplementation);
- repositoryWhitespaces = new ControllerRotationRepository(rotationsConfigWhitespaces, controllerDb, MetricReceiver.nullImplementation);
+ metric = mock(Metric.class);
+ repository = new ControllerRotationRepository(rotationsConfig, controllerDb, metric);
+ repositoryWhitespaces = new ControllerRotationRepository(rotationsConfigWhitespaces, controllerDb, metric);
controllerDb.assignRotation(new RotationId("foo-1"), applicationId);
}
@@ -129,6 +134,7 @@ public class ControllerRotationRepositoryTest {
Set<Rotation> rotations = repository.getOrAssignRotation(other, deploymentSpec);
Rotation assignedRotation = new Rotation(new RotationId("foo-2"), "foo-2.com");
assertContainsOnly(assignedRotation, rotations);
+ verify(metric).set(eq(ControllerRotationRepository.REMAINING_ROTATIONS_METRIC_NAME), eq(1), any());
}
@Test
@@ -140,6 +146,7 @@ public class ControllerRotationRepositoryTest {
thrown.expectMessage("no rotations available");
repository.getOrAssignRotation(third, deploymentSpec);
+ verify(metric).set(eq(ControllerRotationRepository.REMAINING_ROTATIONS_METRIC_NAME), eq(0), any());
}
@Test