summaryrefslogtreecommitdiffstats
path: root/controller-server/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'controller-server/src/main/java')
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java63
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java89
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java32
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java34
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java66
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java3
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/TenantController.java16
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationList.java50
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Change.java55
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Deployment.java28
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Endpoint.java6
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/InstanceList.java9
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackage.java17
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageValidator.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/TestPackage.java318
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ZipEntries.java23
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzFacade.java1
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/auditlog/AuditLogger.java3
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentStatus.java243
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentSteps.java19
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java67
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java230
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java297
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobList.java9
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobMetrics.java4
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobStatus.java15
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/LockedStep.java3
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RetriggerEntrySerializer.java20
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RevisionHistory.java149
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Run.java14
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RunList.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RunStatus.java3
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Step.java3
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Submission.java57
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/TestConfigSerializer.java4
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Versions.java77
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/NameServiceForwarder.java3
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmer.java3
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java20
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java5
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirer.java11
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporter.java1
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentUpgrader.java10
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/EndpointCertificateMaintainer.java11
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporter.java23
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/Upgrader.java7
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/UserManagementMaintainer.java1
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/VcmrMaintainer.java15
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/VersionStatusUpdater.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notification.java10
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDb.java9
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notify/Notifier.java51
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java126
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/BufferedLogStore.java6
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ControllerVersionSerializer.java4
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java163
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/MockCuratorDb.java26
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NodeVersionSerializer.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java16
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RoutingPolicySerializer.java4
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializer.java82
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/SupportAccessSerializer.java1
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ProxyRequest.java4
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ProxyResponse.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java244
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.java105
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ServiceApiResponse.java197
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandler.java4
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2.java13
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/ChangeManagementApiHandler.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/ConfigServerApiHandler.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/AuditLogResponse.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/UpgraderResponse.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/BadgeApiHandler.java8
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/Badges.java45
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiHandler.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilter.java6
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilter.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java4
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiHandler.java6
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiHandler.java2
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicies.java17
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicy.java8
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/rotation/Rotation.java1
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/rotation/RotationLock.java5
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/support/access/SupportAccessControl.java9
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/ControllerVersion.java69
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/DeploymentStatistics.java1
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VersionStatus.java3
90 files changed, 1749 insertions, 1661 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 30f416747e0..af8965bdeff 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
@@ -7,23 +7,22 @@ import com.yahoo.config.application.api.ValidationOverrides;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.InstanceName;
import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
+import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
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.application.ApplicationActivity;
import com.yahoo.vespa.hosted.controller.application.Deployment;
import com.yahoo.vespa.hosted.controller.application.QuotaUsage;
import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
+import com.yahoo.vespa.hosted.controller.deployment.RevisionHistory;
import com.yahoo.vespa.hosted.controller.metric.ApplicationMetrics;
import com.yahoo.vespa.hosted.controller.tenant.Tenant;
import java.security.PublicKey;
import java.time.Instant;
-import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
-import java.util.Deque;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -31,9 +30,7 @@ import java.util.Optional;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.Set;
-import java.util.SortedSet;
import java.util.TreeMap;
-import java.util.TreeSet;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -50,8 +47,7 @@ public class Application {
private final Instant createdAt;
private final DeploymentSpec deploymentSpec;
private final ValidationOverrides validationOverrides;
- private final Optional<ApplicationVersion> latestVersion;
- private final SortedSet<ApplicationVersion> versions;
+ private final RevisionHistory revisions;
private final OptionalLong projectId;
private final Optional<IssueId> deploymentIssueId;
private final Optional<IssueId> ownershipIssueId;
@@ -63,16 +59,16 @@ public class Application {
/** Creates an empty application. */
public Application(TenantAndApplicationId id, Instant now) {
- this(id, now, DeploymentSpec.empty, ValidationOverrides.empty,
- Optional.empty(), Optional.empty(), Optional.empty(), OptionalInt.empty(),
- new ApplicationMetrics(0, 0), Set.of(), OptionalLong.empty(), Optional.empty(), new TreeSet<>(), List.of());
+ this(id, now, DeploymentSpec.empty, ValidationOverrides.empty, Optional.empty(),
+ Optional.empty(), Optional.empty(), OptionalInt.empty(), new ApplicationMetrics(0, 0),
+ Set.of(), OptionalLong.empty(), RevisionHistory.empty(), List.of());
}
// DO NOT USE! For serialization purposes, only.
public Application(TenantAndApplicationId id, Instant createdAt, DeploymentSpec deploymentSpec, ValidationOverrides validationOverrides,
Optional<IssueId> deploymentIssueId, Optional<IssueId> ownershipIssueId, Optional<User> owner,
OptionalInt majorVersion, ApplicationMetrics metrics, Set<PublicKey> deployKeys, OptionalLong projectId,
- Optional<ApplicationVersion> latestVersion, SortedSet<ApplicationVersion> versions, Collection<Instance> instances) {
+ RevisionHistory revisions, Collection<Instance> instances) {
this.id = Objects.requireNonNull(id, "id cannot be null");
this.createdAt = Objects.requireNonNull(createdAt, "instant of creation cannot be null");
this.deploymentSpec = Objects.requireNonNull(deploymentSpec, "deploymentSpec cannot be null");
@@ -84,13 +80,12 @@ public class Application {
this.metrics = Objects.requireNonNull(metrics, "metrics cannot be null");
this.deployKeys = Objects.requireNonNull(deployKeys, "deployKeys cannot be null");
this.projectId = Objects.requireNonNull(projectId, "projectId cannot be null");
- this.latestVersion = requireNotUnknown(latestVersion);
- this.versions = versions;
+ this.revisions = revisions;
this.instances = instances.stream().collect(
Collectors.collectingAndThen(Collectors.toMap(Instance::name,
Function.identity(),
(i1, i2) -> {
- throw new IllegalArgumentException("Duplicate key " + i1);
+ throw new IllegalArgumentException("Duplicate instance " + i1.id());
},
TreeMap::new),
Collections::unmodifiableMap)
@@ -110,29 +105,8 @@ public class Application {
/** Returns the project id of this application, if it has any. */
public OptionalLong projectId() { return projectId; }
- /** Returns the last submitted version of this application. */
- public Optional<ApplicationVersion> latestVersion() {
- return versions.isEmpty() ? Optional.empty() : Optional.of(versions.last());
- }
-
- /** Returns the currently deployed versions of the application, ordered from oldest to newest. */
- public SortedSet<ApplicationVersion> versions() {
- return versions;
- }
-
- /** Returns the currently deployed versions of the application */
- public Collection<ApplicationVersion> deployableVersions(boolean ascending) {
- Deque<ApplicationVersion> versions = new ArrayDeque<>();
- String previousHash = "";
- for (ApplicationVersion version : versions()) {
- if (version.bundleHash().isEmpty() || ! previousHash.equals(version.bundleHash().get())) {
- if (ascending) versions.addLast(version);
- else versions.addFirst(version);
- }
- previousHash = version.bundleHash().orElse("");
- }
- return versions;
- }
+ /** Returns the known revisions for this application. */
+ public RevisionHistory revisions() { return revisions; }
/**
* Returns the last deployed validation overrides of this application,
@@ -212,10 +186,10 @@ public class Application {
/**
* Returns the oldest application version this has deployed in a permanent zone (not test or staging).
*/
- public Optional<ApplicationVersion> oldestDeployedApplication() {
+ public Optional<RevisionId> oldestDeployedRevision() {
return productionDeployments().values().stream().flatMap(List::stream)
- .map(Deployment::applicationVersion)
- .filter(version -> ! version.isUnknown() && ! version.isDeployedDirectly())
+ .map(Deployment::revision)
+ .filter(RevisionId::isProduction)
.min(Comparator.naturalOrder());
}
@@ -241,15 +215,6 @@ public class Application {
/** Returns the set of deploy keys for this application. */
public Set<PublicKey> deployKeys() { return deployKeys; }
- private static Optional<ApplicationVersion> requireNotUnknown(Optional<ApplicationVersion> latestVersion) {
- Objects.requireNonNull(latestVersion, "latestVersion cannot be null");
- latestVersion.ifPresent(version -> {
- if (version.isUnknown())
- throw new IllegalArgumentException("latestVersion cannot be unknown");
- });
- return latestVersion;
- }
-
@Override
public boolean equals(Object o) {
if (this == o) return true;
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 48f909df6b6..6907747646e 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java
@@ -13,6 +13,7 @@ import com.yahoo.config.provision.TenantName;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.log.LogLevel;
import com.yahoo.text.Text;
+import com.yahoo.transaction.Mutex;
import com.yahoo.vespa.athenz.api.AthenzDomain;
import com.yahoo.vespa.athenz.api.AthenzIdentity;
import com.yahoo.vespa.athenz.api.AthenzPrincipal;
@@ -27,7 +28,6 @@ import com.yahoo.vespa.flags.StringFlag;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.DeploymentData;
import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
import com.yahoo.vespa.hosted.controller.api.identifiers.InstanceId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.RevisionId;
import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingController;
import com.yahoo.vespa.hosted.controller.api.integration.billing.Quota;
import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateMetadata;
@@ -41,6 +41,8 @@ import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationS
import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.ArtifactRepository;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
+import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
+import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterId;
import com.yahoo.vespa.hosted.controller.api.integration.noderepository.RestartFilter;
import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretStore;
@@ -58,6 +60,7 @@ import com.yahoo.vespa.hosted.controller.certificate.EndpointCertificates;
import com.yahoo.vespa.hosted.controller.concurrent.Once;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger;
import com.yahoo.vespa.hosted.controller.deployment.JobStatus;
+import com.yahoo.vespa.hosted.controller.deployment.RevisionHistory;
import com.yahoo.vespa.hosted.controller.deployment.Run;
import com.yahoo.vespa.hosted.controller.deployment.RunStatus;
import com.yahoo.vespa.hosted.controller.notification.Notification;
@@ -82,6 +85,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -89,6 +93,8 @@ import java.util.Optional;
import java.util.OptionalInt;
import java.util.Set;
import java.util.TreeMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.logging.Level;
@@ -160,12 +166,13 @@ public class ApplicationController {
for (InstanceName instance : application.get().deploymentSpec().instanceNames())
if ( ! application.get().instances().containsKey(instance))
application = withNewInstance(application, id.instance(instance));
+
store(application);
});
count++;
}
log.log(Level.INFO, Text.format("Wrote %d applications in %s", count,
- Duration.between(start, clock.instant())));
+ Duration.between(start, clock.instant())));
});
}
@@ -175,7 +182,6 @@ public class ApplicationController {
}
/** Returns the instance with the given id, or null if it is not present */
- // TODO jonmv: remove or inline
public Optional<Instance> getInstance(ApplicationId id) {
return getApplication(TenantAndApplicationId.from(id)).flatMap(application -> application.get(id.instance()));
}
@@ -283,7 +289,7 @@ public class ApplicationController {
if (oldest == null || version.isBefore(oldest))
oldest = version;
- if (run.status() == RunStatus.success)
+ if (run.hasSucceeded())
return Optional.of(oldest);
}
// If no successful run was found, ask the node repository in the relevant zone.
@@ -292,7 +298,7 @@ public class ApplicationController {
/** Reads the oldest installed platform for the given application and zone from the node repo of that zone. */
private Optional<Version> oldestInstalledPlatform(JobId job) {
- return configServer.nodeRepository().list(job.type().zone(controller.system()),
+ return configServer.nodeRepository().list(job.type().zone(),
NodeFilter.all()
.applications(job.application())
.states(active, reserved))
@@ -399,7 +405,7 @@ public class ApplicationController {
* @throws IllegalArgumentException if the application already exists
*/
public Application createApplication(TenantAndApplicationId id, Credentials credentials) {
- try (Lock lock = lock(id)) {
+ try (Mutex lock = lock(id)) {
if (getApplication(id).isPresent())
throw new IllegalArgumentException("Could not create '" + id + "': Application already exists");
if (getApplication(dashToUnderscore(id)).isPresent()) // VESPA-1945
@@ -450,10 +456,10 @@ public class ApplicationController {
throw new IllegalArgumentException("'" + job.application() + "' is a tester application!");
TenantAndApplicationId applicationId = TenantAndApplicationId.from(job.application());
- ZoneId zone = job.type().zone(controller.system());
+ ZoneId zone = job.type().zone();
DeploymentId deployment = new DeploymentId(job.application(), zone);
- try (Lock deploymentLock = lockForDeployment(job.application(), zone)) {
+ try (Mutex deploymentLock = lockForDeployment(job.application(), zone)) {
Set<ContainerEndpoint> containerEndpoints;
Optional<EndpointCertificateMetadata> endpointCertificateMetadata;
@@ -464,11 +470,13 @@ public class ApplicationController {
throw new IllegalStateException("No deployment expected for " + job + " now, as no job is running");
Version platform = run.versions().sourcePlatform().filter(__ -> deploySourceVersions).orElse(run.versions().targetPlatform());
- ApplicationVersion revision = run.versions().sourceApplication().filter(__ -> deploySourceVersions).orElse(run.versions().targetApplication());
+ RevisionId revision = run.versions().sourceRevision().filter(__ -> deploySourceVersions).orElse(run.versions().targetRevision());
ApplicationPackage applicationPackage = new ApplicationPackage(applicationStore.get(deployment, revision));
- try (Lock lock = lock(applicationId)) {
+ AtomicReference<RevisionId> lastRevision = new AtomicReference<>();
+ try (Mutex lock = lock(applicationId)) {
LockedApplication application = new LockedApplication(requireApplication(applicationId), lock);
+ application.get().revisions().last().map(ApplicationVersion::id).ifPresent(lastRevision::set);
Instance instance = application.get().require(job.application().instance());
if ( ! applicationPackage.trustedCertificates().isEmpty()
@@ -488,22 +496,25 @@ public class ApplicationController {
var quotaUsage = deploymentQuotaUsage(zone, job.application());
// For direct deployments use the full deployment ID, but otherwise use just the tenant and application as
- // the source since it's the same application, so it should have the same warnings
- NotificationSource source = zone.environment().isManuallyDeployed() ?
- NotificationSource.from(deployment) : NotificationSource.from(applicationId);
-
- @SuppressWarnings("deprecation")
- List<String> warnings = Optional.ofNullable(result.prepareResponse().log)
- .map(logs -> logs.stream()
- .filter(log -> log.applicationPackage)
- .filter(log -> LogLevel.parse(log.level).intValue() >= Level.WARNING.intValue())
- .map(log -> log.message)
- .sorted()
- .distinct()
- .collect(Collectors.toList()))
- .orElseGet(List::of);
- if (warnings.isEmpty()) controller.notificationsDb().removeNotification(source, Notification.Type.applicationPackage);
- else controller.notificationsDb().setNotification(source, Notification.Type.applicationPackage, Notification.Level.warning, warnings);
+ // the source since it's the same application, so it should have the same warnings.
+ // These notifications are only updated when the last submitted revision is deployed here.
+ NotificationSource source = zone.environment().isManuallyDeployed()
+ ? NotificationSource.from(deployment)
+ : revision.equals(lastRevision.get()) ? NotificationSource.from(applicationId) : null;
+ if (source != null) {
+ @SuppressWarnings("deprecation")
+ List<String> warnings = Optional.ofNullable(result.prepareResponse().log)
+ .map(logs -> logs.stream()
+ .filter(log -> log.applicationPackage)
+ .filter(log -> LogLevel.parse(log.level).intValue() >= Level.WARNING.intValue())
+ .map(log -> log.message)
+ .sorted()
+ .distinct()
+ .collect(Collectors.toList()))
+ .orElseGet(List::of);
+ if (warnings.isEmpty()) controller.notificationsDb().removeNotification(source, Notification.Type.applicationPackage);
+ else controller.notificationsDb().setNotification(source, Notification.Type.applicationPackage, Notification.Level.warning, warnings);
+ }
lockApplicationOrThrow(applicationId, application ->
store(application.with(job.application().instance(),
@@ -538,26 +549,13 @@ public class ApplicationController {
controller.jobController().deploymentStatus(application.get());
for (Notification notification : controller.notificationsDb().listNotifications(NotificationSource.from(application.get().id()), true)) {
- if ( ! notification.source().instance().map(declaredInstances::contains).orElse(false))
+ if ( ! notification.source().instance().map(declaredInstances::contains).orElse(true))
controller.notificationsDb().removeNotifications(notification.source());
if (notification.source().instance().isPresent() &&
! notification.source().zoneId().map(application.get().require(notification.source().instance().get()).deployments()::containsKey).orElse(false))
controller.notificationsDb().removeNotifications(notification.source());
}
- ApplicationVersion oldestDeployedVersion = application.get().oldestDeployedApplication()
- .orElse(ApplicationVersion.unknown);
-
- List<ApplicationVersion> olderVersions = application.get().versions().stream()
- .filter(version -> version.compareTo(oldestDeployedVersion) < 0)
- .sorted()
- .collect(Collectors.toList());
-
- // Remove any version not deployed anywhere - but keep one
- for (ApplicationVersion version : olderVersions) {
- application = application.withoutVersion(version);
- }
-
store(application);
}
@@ -629,7 +627,7 @@ public class ApplicationController {
deploymentQuota, tenantSecretStores, operatorCertificates,
dryRun));
- return new ActivateResult(new RevisionId(applicationPackage.hash()), preparedApplication.prepareResponse(),
+ return new ActivateResult(new com.yahoo.vespa.hosted.controller.api.identifiers.RevisionId(applicationPackage.hash()), preparedApplication.prepareResponse(),
applicationPackage.zippedContent().length);
} finally {
// Even if prepare fails, routing configuration may need to be updated
@@ -697,7 +695,6 @@ public class ApplicationController {
}
applicationStore.removeAll(id.tenant(), id.application());
- applicationStore.removeAllTesters(id.tenant(), id.application());
applicationStore.putMetaTombstone(id.tenant(), id.application(), clock.instant());
credentials.ifPresent(creds -> accessControl.deleteApplication(id, creds));
@@ -753,7 +750,7 @@ public class ApplicationController {
* @param action Function which acts on the locked application.
*/
public void lockApplicationIfPresent(TenantAndApplicationId applicationId, Consumer<LockedApplication> action) {
- try (Lock lock = lock(applicationId)) {
+ try (Mutex lock = lock(applicationId)) {
getApplication(applicationId).map(application -> new LockedApplication(application, lock)).ifPresent(action);
}
}
@@ -766,7 +763,7 @@ public class ApplicationController {
* @throws IllegalArgumentException when application does not exist.
*/
public void lockApplicationOrThrow(TenantAndApplicationId applicationId, Consumer<LockedApplication> action) {
- try (Lock lock = lock(applicationId)) {
+ try (Mutex lock = lock(applicationId)) {
action.accept(new LockedApplication(requireApplication(applicationId), lock));
}
}
@@ -839,14 +836,14 @@ public class ApplicationController {
* 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.
*/
- Lock lock(TenantAndApplicationId application) {
+ Mutex lock(TenantAndApplicationId application) {
return curator.lock(application);
}
/**
* Returns a lock which provides exclusive rights to deploying this application to the given zone.
*/
- private Lock lockForDeployment(ApplicationId application, ZoneId zone) {
+ private Mutex lockForDeployment(ApplicationId application, ZoneId zone) {
return curator.lockForDeployment(application, zone);
}
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 c35e8c5a7ac..48e663e7feb 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
@@ -7,12 +7,12 @@ import com.yahoo.component.Version;
import com.yahoo.component.Vtag;
import com.yahoo.concurrent.maintenance.JobControl;
import com.yahoo.config.provision.CloudName;
+import com.yahoo.config.provision.HostName;
import com.yahoo.config.provision.SystemName;
import com.yahoo.config.provision.zone.ZoneApi;
import com.yahoo.container.jdisc.secretstore.SecretStore;
import com.yahoo.jdisc.Metric;
-import com.yahoo.net.HostName;
-import com.yahoo.vespa.curator.Lock;
+import com.yahoo.transaction.Mutex;
import com.yahoo.vespa.flags.FlagSource;
import com.yahoo.vespa.hosted.controller.api.integration.ServiceRegistry;
import com.yahoo.vespa.hosted.controller.api.integration.maven.MavenRepository;
@@ -28,14 +28,12 @@ import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
import com.yahoo.vespa.hosted.controller.persistence.JobControlFlags;
import com.yahoo.vespa.hosted.controller.security.AccessControl;
import com.yahoo.vespa.hosted.controller.support.access.SupportAccessControl;
-import com.yahoo.vespa.hosted.controller.versions.ControllerVersion;
import com.yahoo.vespa.hosted.controller.versions.OsVersion;
import com.yahoo.vespa.hosted.controller.versions.OsVersionStatus;
import com.yahoo.vespa.hosted.controller.versions.OsVersionTarget;
import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
import com.yahoo.vespa.hosted.rotation.config.RotationsConfig;
-import com.yahoo.vespa.serviceview.bindings.ApplicationView;
import com.yahoo.yolean.concurrent.Sleeper;
import java.time.Clock;
@@ -49,7 +47,6 @@ import java.util.Set;
import java.util.TreeSet;
import java.util.function.Function;
import java.util.function.Predicate;
-import java.util.function.Supplier;
import java.util.logging.Logger;
import java.util.stream.Collectors;
@@ -68,7 +65,6 @@ public class Controller extends AbstractComponent {
private static final Logger log = Logger.getLogger(Controller.class.getName());
- private final Supplier<String> hostnameSupplier;
private final CuratorDb curator;
private final JobControl jobControl;
private final ApplicationController applicationController;
@@ -100,15 +96,14 @@ public class Controller extends AbstractComponent {
public Controller(CuratorDb curator, RotationsConfig rotationsConfig, AccessControl accessControl, FlagSource flagSource,
MavenRepository mavenRepository, ServiceRegistry serviceRegistry, Metric metric, SecretStore secretStore,
ControllerConfig controllerConfig) {
- this(curator, rotationsConfig, accessControl, HostName::getLocalhost, flagSource,
+ this(curator, rotationsConfig, accessControl, flagSource,
mavenRepository, serviceRegistry, metric, secretStore, controllerConfig, Sleeper.DEFAULT);
}
public Controller(CuratorDb curator, RotationsConfig rotationsConfig, AccessControl accessControl,
- Supplier<String> hostnameSupplier, FlagSource flagSource, MavenRepository mavenRepository,
+ FlagSource flagSource, MavenRepository mavenRepository,
ServiceRegistry serviceRegistry, Metric metric, SecretStore secretStore,
ControllerConfig controllerConfig, Sleeper sleeper) {
- this.hostnameSupplier = Objects.requireNonNull(hostnameSupplier, "HostnameSupplier cannot be null");
this.curator = Objects.requireNonNull(curator, "Curator cannot be null");
this.serviceRegistry = Objects.requireNonNull(serviceRegistry, "ServiceRegistry cannot be null");
this.zoneRegistry = Objects.requireNonNull(serviceRegistry.zoneRegistry(), "ZoneRegistry cannot be null");
@@ -128,12 +123,12 @@ public class Controller extends AbstractComponent {
auditLogger = new AuditLogger(curator, clock);
jobControl = new JobControl(new JobControlFlags(curator, flagSource));
archiveBucketDb = new CuratorArchiveBucketDb(this);
- notifier = new Notifier(curator, serviceRegistry.mailer());
+ notifier = new Notifier(curator, serviceRegistry.zoneRegistry(), serviceRegistry.mailer());
notificationsDb = new NotificationsDb(this);
supportAccessControl = new SupportAccessControl(this);
// Record the version of this controller
- curator().writeControllerVersion(this.hostname(), ControllerVersion.CURRENT);
+ curator().writeControllerVersion(this.hostname(), serviceRegistry.controllerVersion());
jobController.updateStorage();
}
@@ -174,11 +169,6 @@ public class Controller extends AbstractComponent {
public ControllerConfig controllerConfig() { return controllerConfig; }
- public ApplicationView getApplicationView(String tenantName, String applicationName, String instanceName,
- String environment, String region) {
- return serviceRegistry.configServer().getApplicationView(tenantName, applicationName, instanceName, environment, region);
- }
-
/** Replace the current version status by a new one */
public void updateVersionStatus(VersionStatus newStatus) {
VersionStatus currentStatus = readVersionStatus();
@@ -199,7 +189,7 @@ public class Controller extends AbstractComponent {
/** Remove confidence override for versions matching given filter */
public void removeConfidenceOverride(Predicate<Version> filter) {
- try (Lock lock = curator.lockConfidenceOverrides()) {
+ try (Mutex lock = curator.lockConfidenceOverrides()) {
Map<Version, VespaVersion.Confidence> overrides = new LinkedHashMap<>(curator.readConfidenceOverrides());
overrides.keySet().removeIf(filter);
curator.writeConfidenceOverrides(overrides);
@@ -238,7 +228,7 @@ public class Controller extends AbstractComponent {
throw new IllegalArgumentException("Cloud '" + cloudName + "' does not exist in this system");
}
Instant scheduledAt = clock.instant();
- try (Lock lock = curator.lockOsVersions()) {
+ try (Mutex lock = curator.lockOsVersions()) {
Map<CloudName, OsVersionTarget> targets = curator.readOsVersionTargets().stream()
.collect(Collectors.toMap(t -> t.osVersion().cloud(),
Function.identity()));
@@ -268,7 +258,7 @@ public class Controller extends AbstractComponent {
/** Replace the current OS version status with a new one */
public void updateOsVersionStatus(OsVersionStatus newStatus) {
- try (Lock lock = curator.lockOsVersionStatus()) {
+ try (Mutex lock = curator.lockOsVersionStatus()) {
OsVersionStatus currentStatus = curator.readOsVersionStatus();
for (CloudName cloud : clouds()) {
Set<Version> newVersions = newStatus.versionsIn(cloud);
@@ -282,8 +272,8 @@ public class Controller extends AbstractComponent {
}
/** Returns the hostname of this controller */
- public com.yahoo.config.provision.HostName hostname() {
- return com.yahoo.config.provision.HostName.from(hostnameSupplier.get());
+ public HostName hostname() {
+ return serviceRegistry.getHostname();
}
public SystemName system() {
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java
index e5ab13d2127..402a4bf49a8 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java
@@ -6,8 +6,8 @@ import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.InstanceName;
import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
+import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
import com.yahoo.vespa.hosted.controller.application.AssignedRotation;
import com.yahoo.vespa.hosted.controller.application.Change;
import com.yahoo.vespa.hosted.controller.application.Deployment;
@@ -45,17 +45,15 @@ public class Instance {
private final RotationStatus rotationStatus;
private final Map<JobType, Instant> jobPauses;
private final Change change;
- private final Optional<ApplicationVersion> latestDeployed;
/** Creates an empty instance */
public Instance(ApplicationId id) {
- this(id, Set.of(), Map.of(), List.of(), RotationStatus.EMPTY, Change.empty(), Optional.empty());
+ this(id, Set.of(), Map.of(), List.of(), RotationStatus.EMPTY, Change.empty());
}
/** Creates an empty instance*/
public Instance(ApplicationId id, Collection<Deployment> deployments, Map<JobType, Instant> jobPauses,
- List<AssignedRotation> rotations, RotationStatus rotationStatus, Change change,
- Optional<ApplicationVersion> latestDeployed) {
+ List<AssignedRotation> rotations, RotationStatus rotationStatus, Change change) {
this.id = Objects.requireNonNull(id, "id cannot be null");
this.deployments = Objects.requireNonNull(deployments, "deployments cannot be null").stream()
.collect(Collectors.toUnmodifiableMap(Deployment::zone, Function.identity()));
@@ -63,19 +61,18 @@ public class Instance {
this.rotations = List.copyOf(Objects.requireNonNull(rotations, "rotations cannot be null"));
this.rotationStatus = Objects.requireNonNull(rotationStatus, "rotationStatus cannot be null");
this.change = Objects.requireNonNull(change, "change cannot be null");
- this.latestDeployed = Objects.requireNonNull(latestDeployed, "latestDeployed cannot be null");
}
- public Instance withNewDeployment(ZoneId zone, ApplicationVersion applicationVersion, Version version,
+ public Instance withNewDeployment(ZoneId zone, RevisionId revision, Version version,
Instant instant, Map<DeploymentMetrics.Warning, Integer> warnings, QuotaUsage quotaUsage) {
// Use info from previous deployment if available, otherwise create a new one.
- Deployment previousDeployment = deployments.getOrDefault(zone, new Deployment(zone, applicationVersion,
+ Deployment previousDeployment = deployments.getOrDefault(zone, new Deployment(zone, revision,
version, instant,
DeploymentMetrics.none,
DeploymentActivity.none,
QuotaUsage.none,
OptionalDouble.empty()));
- Deployment newDeployment = new Deployment(zone, applicationVersion, version, instant,
+ Deployment newDeployment = new Deployment(zone, revision, version, instant,
previousDeployment.metrics().with(warnings),
previousDeployment.activity(),
quotaUsage,
@@ -90,7 +87,7 @@ public class Instance {
else
jobPauses.remove(jobType);
- return new Instance(id, deployments.values(), jobPauses, rotations, rotationStatus, change, latestDeployed);
+ return new Instance(id, deployments.values(), jobPauses, rotations, rotationStatus, change);
}
public Instance recordActivityAt(Instant instant, ZoneId zone) {
@@ -121,19 +118,15 @@ public class Instance {
}
public Instance with(List<AssignedRotation> assignedRotations) {
- return new Instance(id, deployments.values(), jobPauses, assignedRotations, rotationStatus, change, latestDeployed);
+ return new Instance(id, deployments.values(), jobPauses, assignedRotations, rotationStatus, change);
}
public Instance with(RotationStatus rotationStatus) {
- return new Instance(id, deployments.values(), jobPauses, rotations, rotationStatus, change, latestDeployed);
+ return new Instance(id, deployments.values(), jobPauses, rotations, rotationStatus, change);
}
public Instance withChange(Change change) {
- return new Instance(id, deployments.values(), jobPauses, rotations, rotationStatus, change, latestDeployed);
- }
-
- public Instance withLatestDeployed(ApplicationVersion latestDeployed) {
- return new Instance(id, deployments.values(), jobPauses, rotations, rotationStatus, change, Optional.of(latestDeployed));
+ return new Instance(id, deployments.values(), jobPauses, rotations, rotationStatus, change);
}
private Instance with(Deployment deployment) {
@@ -143,7 +136,7 @@ public class Instance {
}
private Instance with(Map<ZoneId, Deployment> deployments) {
- return new Instance(id, deployments.values(), jobPauses, rotations, rotationStatus, change, latestDeployed);
+ return new Instance(id, deployments.values(), jobPauses, rotations, rotationStatus, change);
}
public ApplicationId id() { return id; }
@@ -188,11 +181,6 @@ public class Instance {
return change;
}
- /** Returns the application version that last rolled out to this instance. */
- public Optional<ApplicationVersion> latestDeployed() {
- return latestDeployed;
- }
-
/** Returns the total quota usage for this instance, excluding temporary deployments **/
public QuotaUsage quotaUsage() {
return deployments.values().stream()
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 06ff381e4dc..3e822415e96 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
@@ -4,11 +4,12 @@ package com.yahoo.vespa.hosted.controller;
import com.yahoo.config.application.api.DeploymentSpec;
import com.yahoo.config.application.api.ValidationOverrides;
import com.yahoo.config.provision.InstanceName;
+import com.yahoo.transaction.Mutex;
import com.yahoo.vespa.curator.Lock;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
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.application.TenantAndApplicationId;
+import com.yahoo.vespa.hosted.controller.deployment.RevisionHistory;
import com.yahoo.vespa.hosted.controller.metric.ApplicationMetrics;
import java.security.PublicKey;
@@ -21,8 +22,6 @@ import java.util.Optional;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.Set;
-import java.util.SortedSet;
-import java.util.TreeSet;
import java.util.function.UnaryOperator;
/**
@@ -32,7 +31,7 @@ import java.util.function.UnaryOperator;
*/
public class LockedApplication {
- private final Lock lock;
+ private final Mutex lock;
private final TenantAndApplicationId id;
private final Instant createdAt;
private final DeploymentSpec deploymentSpec;
@@ -44,8 +43,7 @@ public class LockedApplication {
private final ApplicationMetrics metrics;
private final Set<PublicKey> deployKeys;
private final OptionalLong projectId;
- private final Optional<ApplicationVersion> latestVersion;
- private final SortedSet<ApplicationVersion> versions;
+ private final RevisionHistory revisions;
private final Map<InstanceName, Instance> instances;
/**
@@ -54,20 +52,19 @@ public class LockedApplication {
* @param application The application to lock.
* @param lock The lock for the application.
*/
- LockedApplication(Application application, Lock lock) {
+ LockedApplication(Application application, Mutex lock) {
this(Objects.requireNonNull(lock, "lock cannot be null"), application.id(), application.createdAt(),
application.deploymentSpec(), application.validationOverrides(),
application.deploymentIssueId(), application.ownershipIssueId(),
application.owner(), application.majorVersion(), application.metrics(), application.deployKeys(),
- application.projectId(), application.latestVersion(), application.versions(), application.instances());
+ application.projectId(), application.instances(), application.revisions());
}
- private LockedApplication(Lock lock, TenantAndApplicationId id, Instant createdAt, DeploymentSpec deploymentSpec,
+ private LockedApplication(Mutex lock, TenantAndApplicationId id, Instant createdAt, DeploymentSpec deploymentSpec,
ValidationOverrides validationOverrides,
Optional<IssueId> deploymentIssueId, Optional<IssueId> ownershipIssueId, Optional<User> owner,
OptionalInt majorVersion, ApplicationMetrics metrics, Set<PublicKey> deployKeys,
- OptionalLong projectId, Optional<ApplicationVersion> latestVersion, SortedSet<ApplicationVersion> versions,
- Map<InstanceName, Instance> instances) {
+ OptionalLong projectId, Map<InstanceName, Instance> instances, RevisionHistory revisions) {
this.lock = lock;
this.id = id;
this.createdAt = createdAt;
@@ -80,8 +77,7 @@ public class LockedApplication {
this.metrics = metrics;
this.deployKeys = deployKeys;
this.projectId = projectId;
- this.latestVersion = latestVersion;
- this.versions = versions;
+ this.revisions = revisions;
this.instances = Map.copyOf(instances);
}
@@ -89,7 +85,7 @@ public class LockedApplication {
public Application get() {
return new Application(id, createdAt, deploymentSpec, validationOverrides,
deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys,
- projectId, latestVersion, versions, instances.values());
+ projectId, revisions, instances.values());
}
LockedApplication withNewInstance(InstanceName instance) {
@@ -97,7 +93,7 @@ public class LockedApplication {
instances.put(instance, new Instance(id.instance(instance)));
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys,
- projectId, latestVersion, versions, instances);
+ projectId, instances, revisions);
}
public LockedApplication with(InstanceName instance, UnaryOperator<Instance> modification) {
@@ -105,7 +101,7 @@ public class LockedApplication {
instances.put(instance, modification.apply(instances.get(instance)));
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys,
- projectId, latestVersion, versions, instances);
+ projectId, instances, revisions);
}
public LockedApplication without(InstanceName instance) {
@@ -113,51 +109,43 @@ public class LockedApplication {
instances.remove(instance);
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys,
- projectId, latestVersion, versions, instances);
- }
-
- public LockedApplication withNewSubmission(ApplicationVersion latestVersion) {
- SortedSet<ApplicationVersion> applicationVersions = new TreeSet<>(versions);
- applicationVersions.add(latestVersion);
- return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
- deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys,
- projectId, Optional.of(latestVersion), applicationVersions, instances);
+ projectId, instances, revisions);
}
public LockedApplication withProjectId(OptionalLong projectId) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys,
- projectId, latestVersion, versions, instances);
+ projectId, instances, revisions);
}
public LockedApplication withDeploymentIssueId(IssueId issueId) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
Optional.ofNullable(issueId), ownershipIssueId, owner, majorVersion, metrics, deployKeys,
- projectId, latestVersion, versions, instances);
+ projectId, instances, revisions);
}
public LockedApplication with(DeploymentSpec deploymentSpec) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys,
- projectId, latestVersion, versions, instances);
+ projectId, instances, revisions);
}
public LockedApplication with(ValidationOverrides validationOverrides) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys,
- projectId, latestVersion, versions, instances);
+ projectId, instances, revisions);
}
public LockedApplication withOwnershipIssueId(IssueId issueId) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
deploymentIssueId, Optional.of(issueId), owner, majorVersion, metrics, deployKeys,
- projectId, latestVersion, versions, instances);
+ projectId, instances, revisions);
}
public LockedApplication withOwner(User owner) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
deploymentIssueId, ownershipIssueId, Optional.of(owner), majorVersion, metrics, deployKeys,
- projectId, latestVersion, versions, instances);
+ projectId, instances, revisions);
}
/** Set a major version for this, or set to null to remove any major version override */
@@ -165,13 +153,13 @@ public class LockedApplication {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
deploymentIssueId, ownershipIssueId, owner,
majorVersion == null ? OptionalInt.empty() : OptionalInt.of(majorVersion),
- metrics, deployKeys, projectId, latestVersion, versions, instances);
+ metrics, deployKeys, projectId, instances, revisions);
}
public LockedApplication with(ApplicationMetrics metrics) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys,
- projectId, latestVersion, versions, instances);
+ projectId, instances, revisions);
}
public LockedApplication withDeployKey(PublicKey pemDeployKey) {
@@ -179,7 +167,7 @@ public class LockedApplication {
keys.add(pemDeployKey);
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, keys,
- projectId, latestVersion, versions, instances);
+ projectId, instances, revisions);
}
public LockedApplication withoutDeployKey(PublicKey pemDeployKey) {
@@ -187,15 +175,13 @@ public class LockedApplication {
keys.remove(pemDeployKey);
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, keys,
- projectId, latestVersion, versions, instances);
+ projectId, instances, revisions);
}
- public LockedApplication withoutVersion(ApplicationVersion version) {
- SortedSet<ApplicationVersion> applicationVersions = new TreeSet<>(versions);
- applicationVersions.remove(version);
+ public LockedApplication withRevisions(UnaryOperator<RevisionHistory> change) {
return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
- deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics, deployKeys,
- projectId, latestVersion, applicationVersions, instances);
+ deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics,
+ deployKeys, projectId, instances, change.apply(revisions));
}
@Override
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java
index bcc3b9b54c7..7a0e60aacb4 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java
@@ -6,6 +6,7 @@ import com.google.common.collect.HashBiMap;
import com.google.common.collect.ImmutableBiMap;
import com.yahoo.config.provision.TenantName;
import com.yahoo.security.KeyUtils;
+import com.yahoo.transaction.Mutex;
import com.yahoo.vespa.athenz.api.AthenzDomain;
import com.yahoo.vespa.curator.Lock;
import com.yahoo.vespa.hosted.controller.api.identifiers.Property;
@@ -46,7 +47,7 @@ public abstract class LockedTenant {
this.lastLoginInfo = requireNonNull(lastLoginInfo);
}
- static LockedTenant of(Tenant tenant, Lock lock) {
+ static LockedTenant of(Tenant tenant, Mutex lock) {
switch (tenant.type()) {
case athenz: return new Athenz((AthenzTenant) tenant);
case cloud: return new Cloud((CloudTenant) tenant);
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/TenantController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/TenantController.java
index 384a5d0f1ac..beba1cdb358 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/TenantController.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/TenantController.java
@@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.controller;
import com.yahoo.config.provision.TenantName;
import com.yahoo.text.Text;
+import com.yahoo.transaction.Mutex;
import com.yahoo.vespa.curator.Lock;
import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
import com.yahoo.vespa.hosted.controller.application.SystemApplication;
@@ -23,7 +24,6 @@ import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
-import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
@@ -75,7 +75,7 @@ public class TenantController {
/** Locks a tenant for modification and applies the given action. */
public <T extends LockedTenant> void lockIfPresent(TenantName name, Class<T> token, Consumer<T> action) {
- try (Lock lock = lock(name)) {
+ try (Mutex lock = lock(name)) {
get(name).map(tenant -> LockedTenant.of(tenant, lock))
.map(token::cast)
.ifPresent(action);
@@ -84,7 +84,7 @@ public class TenantController {
/** Lock a tenant for modification and apply action. Throws if the tenant does not exist */
public <T extends LockedTenant> void lockOrThrow(TenantName name, Class<T> token, Consumer<T> action) {
- try (Lock lock = lock(name)) {
+ try (Mutex lock = lock(name)) {
action.accept(token.cast(LockedTenant.of(require(name), lock)));
}
}
@@ -112,7 +112,7 @@ public class TenantController {
/** Create a tenant, provided the given credentials are valid. */
public void create(TenantSpec tenantSpec, Credentials credentials) {
- try (Lock lock = lock(tenantSpec.tenant())) {
+ try (Mutex lock = lock(tenantSpec.tenant())) {
TenantId.validate(tenantSpec.tenant().value());
requireNonExistent(tenantSpec.tenant());
curator.writeTenant(accessControl.createTenant(tenantSpec, controller.clock().instant(), credentials, asList()));
@@ -138,7 +138,7 @@ public class TenantController {
/** Updates the tenant contained in the given tenant spec with new data. */
public void update(TenantSpec tenantSpec, Credentials credentials) {
- try (Lock lock = lock(tenantSpec.tenant())) {
+ try (Mutex lock = lock(tenantSpec.tenant())) {
curator.writeTenant(accessControl.updateTenant(tenantSpec, credentials, asList(),
controller.applications().asList(tenantSpec.tenant())));
}
@@ -149,7 +149,7 @@ public class TenantController {
* new instant is later
*/
public void updateLastLogin(TenantName tenantName, List<LastLoginInfo.UserLevel> userLevels, Instant loggedInAt) {
- try (Lock lock = lock(tenantName)) {
+ try (Mutex lock = lock(tenantName)) {
Tenant tenant = require(tenantName);
LastLoginInfo loginInfo = tenant.lastLoginInfo();
for (LastLoginInfo.UserLevel userLevel : userLevels)
@@ -162,7 +162,7 @@ public class TenantController {
/** Deletes the given tenant. */
public void delete(TenantName tenant, Optional<Credentials> credentials, boolean forget) {
- try (Lock lock = lock(tenant)) {
+ try (Mutex lock = lock(tenant)) {
Tenant oldTenant = get(tenant, true)
.orElseThrow(() -> new NotExistsException("Could not delete tenant '" + tenant + "': Tenant not found"));
@@ -203,7 +203,7 @@ public class TenantController {
* Any operation which stores a tenant need to first acquire this lock, then read, modify
* and store the tenant, and finally release (close) the lock.
*/
- private Lock lock(TenantName tenant) {
+ private Mutex lock(TenantName tenant) {
return curator.lock(tenant);
}
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 5c0669ad543..411c2c133f5 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
@@ -39,13 +39,6 @@ public class ApplicationList extends AbstractFilteringList<Application, Applicat
.collect(Collectors.toUnmodifiableList()));
}
- // ----------------------------------- Accessors
-
- /** Returns the ids of the applications in this as an immutable list */
- public List<TenantAndApplicationId> idList() {
- return mapToList(Application::id);
- }
-
// ----------------------------------- Filters
/** Returns the subset of applications which have at least one production deployment */
@@ -54,13 +47,6 @@ public class ApplicationList extends AbstractFilteringList<Application, Applicat
.anyMatch(instance -> instance.productionDeployments().size() > 0));
}
- /** Returns the subset of applications which have at least one deployment on a lower version than the given one */
- public ApplicationList onLowerVersionThan(Version version) {
- return matching(application -> application.instances().values().stream()
- .flatMap(instance -> instance.productionDeployments().values().stream())
- .anyMatch(deployment -> deployment.version().isBefore(version)));
- }
-
/** Returns the subset of applications with at least one declared job in deployment spec. */
public ApplicationList withJobs() {
return matching(application -> application.deploymentSpec().steps().stream()
@@ -72,45 +58,9 @@ public class ApplicationList extends AbstractFilteringList<Application, Applicat
return matching(application -> application.projectId().isPresent());
}
- /** Returns the subset of applications that are allowed to upgrade at the given time */
- public ApplicationList canUpgradeAt(Instant instant) {
- return matching(application -> application.deploymentSpec().instances().stream()
- .allMatch(instance -> instance.canUpgradeAt(instant)));
- }
-
- /** Returns the subset of applications that have at least one assigned rotation */
- public ApplicationList hasRotation() {
- return matching(application -> application.instances().values().stream()
- .anyMatch(instance -> ! instance.rotations().isEmpty()));
- }
-
- /**
- * Returns the subset of applications that hasn't pinned to an an earlier major version than the given one.
- *
- * @param targetMajorVersion the target major version which applications returned allows upgrading to
- * @param defaultMajorVersion the default major version to assume for applications not specifying one
- */
- public ApplicationList allowMajorVersion(int targetMajorVersion, int defaultMajorVersion) {
- return matching(application -> targetMajorVersion <= application.deploymentSpec().majorVersion()
- .orElse(application.majorVersion()
- .orElse(defaultMajorVersion)));
- }
-
/** Returns the subset of application which have submitted a non-empty deployment spec. */
public ApplicationList withDeploymentSpec() {
return matching(application -> ! DeploymentSpec.empty.equals(application.deploymentSpec()));
}
- // ----------------------------------- Sorting
-
- /**
- * Returns this list sorted by increasing deployed version.
- * If multiple versions are deployed the oldest is used.
- * Applications without any deployments are ordered first.
- */
- public ApplicationList byIncreasingDeployedVersion() {
- return sortedBy(Comparator.comparing(application -> application.oldestDeployedPlatform()
- .orElse(Version.emptyVersion)));
- }
-
}
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 244fb952b3f..64cad599168 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
@@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.controller.application;
import com.yahoo.component.Version;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
+import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
import java.util.Objects;
import java.util.Optional;
@@ -28,22 +29,22 @@ public final class Change {
private final Optional<Version> platform;
/** The application version we are changing to, or empty if none */
- private final Optional<ApplicationVersion> application;
+ private final Optional<RevisionId> revision;
/** Whether this change is a pin to its contained Vespa version, or to the application's current. */
private final boolean pinned;
- private Change(Optional<Version> platform, Optional<ApplicationVersion> application, boolean pinned) {
+ private Change(Optional<Version> platform, Optional<RevisionId> revision, boolean pinned) {
this.platform = requireNonNull(platform, "platform cannot be null");
- this.application = requireNonNull(application, "application cannot be null");
- if (application.isPresent() && (application.get().isUnknown() || application.get().isDeployedDirectly())) {
+ this.revision = requireNonNull(revision, "revision cannot be null");
+ if (revision.isPresent() && ( ! revision.get().isProduction())) {
throw new IllegalArgumentException("Application version to deploy must be a known version");
}
this.pinned = pinned;
}
public Change withoutPlatform() {
- return new Change(Optional.empty(), application, pinned);
+ return new Change(Optional.empty(), revision, pinned);
}
public Change withoutApplication() {
@@ -52,7 +53,7 @@ public final class Change {
/** Returns whether a change should currently be deployed */
public boolean hasTargets() {
- return platform.isPresent() || application.isPresent();
+ return platform.isPresent() || revision.isPresent();
}
/** Returns whether this is the empty change. */
@@ -64,7 +65,7 @@ public final class Change {
public Optional<Version> platform() { return platform; }
/** Returns the application version carried by this. */
- public Optional<ApplicationVersion> application() { return application; }
+ public Optional<RevisionId> revision() { return revision; }
public boolean isPinned() { return pinned; }
@@ -76,30 +77,30 @@ public final class Change {
if (pinned)
throw new IllegalArgumentException("Not allowed to set a platform version when pinned.");
- return new Change(Optional.of(platformVersion), application, pinned);
+ return new Change(Optional.of(platformVersion), revision, pinned);
}
- /** Returns a version of this change which replaces or adds this application change */
- public Change with(ApplicationVersion applicationVersion) {
- return new Change(platform, Optional.of(applicationVersion), pinned);
+ /** Returns a version of this change which replaces or adds this revision change */
+ public Change with(RevisionId revision) {
+ return new Change(platform, Optional.of(revision), pinned);
}
/** Returns a change with the versions of this, and with the platform version pinned. */
public Change withPin() {
- return new Change(platform, application, true);
+ return new Change(platform, revision, true);
}
/** Returns a change with the versions of this, and with the platform version unpinned. */
public Change withoutPin() {
- return new Change(platform, application, false);
+ return new Change(platform, revision, false);
}
/** Returns the change obtained when overwriting elements of the given change with any present in this */
public Change onTopOf(Change other) {
if (platform.isPresent())
other = other.with(platform.get());
- if (application.isPresent())
- other = other.with(application.get());
+ if (revision.isPresent())
+ other = other.with(revision.get());
if (pinned)
other = other.withPin();
return other;
@@ -112,12 +113,12 @@ public final class Change {
Change change = (Change) o;
return pinned == change.pinned &&
Objects.equals(platform, change.platform) &&
- Objects.equals(application, change.application);
+ Objects.equals(revision, change.revision);
}
@Override
public int hashCode() {
- return Objects.hash(platform, application, pinned);
+ return Objects.hash(platform, revision, pinned);
}
@Override
@@ -126,23 +127,23 @@ public final class Change {
if (pinned)
changes.add("pin to " + platform.map(Version::toString).orElse("current platform"));
else
- platform.ifPresent(version -> changes.add("upgrade to " + version.toString()));
- application.ifPresent(version -> changes.add("application change to " + version.id()));
+ platform.ifPresent(version -> changes.add("upgrade to " + version));
+ revision.ifPresent(revision -> changes.add("revision change to " + revision));
changes.setEmptyValue("no change");
return changes.toString();
}
- public static Change of(ApplicationVersion applicationVersion) {
- return new Change(Optional.empty(), Optional.of(applicationVersion), false);
+ public static Change of(RevisionId revision) {
+ return new Change(Optional.empty(), Optional.of(revision), false);
}
public static Change of(Version platformChange) {
return new Change(Optional.of(platformChange), Optional.empty(), false);
}
- /** Returns whether this change carries an application downgrade relative to the given version. */
- public boolean downgrades(ApplicationVersion version) {
- return application.map(version::compareTo).orElse(0) > 0;
+ /** Returns whether this change carries a revision downgrade relative to the given revision. */
+ public boolean downgrades(RevisionId revision) {
+ return this.revision.map(revision::compareTo).orElse(0) > 0;
}
/** Returns whether this change carries a platform downgrade relative to the given version. */
@@ -150,9 +151,9 @@ public final class Change {
return platform.map(version::compareTo).orElse(0) > 0;
}
- /** Returns whether this change carries an application upgrade relative to the given version. */
- public boolean upgrades(ApplicationVersion version) {
- return application.map(version::compareTo).orElse(0) < 0;
+ /** Returns whether this change carries a revision upgrade relative to the given revision. */
+ public boolean upgrades(RevisionId revision) {
+ return this.revision.map(revision::compareTo).orElse(0) < 0;
}
/** Returns whether this change carries a platform upgrade relative to the given version. */
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Deployment.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Deployment.java
index 6081c3323c8..2e4afb4e004 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Deployment.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Deployment.java
@@ -2,8 +2,8 @@
package com.yahoo.vespa.hosted.controller.application;
import com.yahoo.component.Version;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
import com.yahoo.config.provision.zone.ZoneId;
+import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
import java.time.Instant;
import java.util.Objects;
@@ -18,7 +18,7 @@ import java.util.OptionalDouble;
public class Deployment {
private final ZoneId zone;
- private final ApplicationVersion applicationVersion;
+ private final RevisionId revision;
private final Version version;
private final Instant deployTime;
private final DeploymentMetrics metrics;
@@ -26,10 +26,10 @@ public class Deployment {
private final QuotaUsage quota;
private final OptionalDouble cost;
- public Deployment(ZoneId zone, ApplicationVersion applicationVersion, Version version, Instant deployTime,
+ public Deployment(ZoneId zone, RevisionId revision, Version version, Instant deployTime,
DeploymentMetrics metrics, DeploymentActivity activity, QuotaUsage quota, OptionalDouble cost) {
this.zone = Objects.requireNonNull(zone, "zone cannot be null");
- this.applicationVersion = Objects.requireNonNull(applicationVersion, "applicationVersion cannot be null");
+ this.revision = Objects.requireNonNull(revision, "revision cannot be null");
this.version = Objects.requireNonNull(version, "version cannot be null");
this.deployTime = Objects.requireNonNull(deployTime, "deployTime cannot be null");
this.metrics = Objects.requireNonNull(metrics, "deploymentMetrics cannot be null");
@@ -41,8 +41,8 @@ public class Deployment {
/** Returns the zone this was deployed to */
public ZoneId zone() { return zone; }
- /** Returns the deployed application version */
- public ApplicationVersion applicationVersion() { return applicationVersion; }
+ /** Returns the deployed application revision */
+ public RevisionId revision() { return revision; }
/** Returns the deployed Vespa version */
public Version version() { return version; }
@@ -65,26 +65,26 @@ public class Deployment {
public OptionalDouble cost() { return cost; }
public Deployment recordActivityAt(Instant instant) {
- return new Deployment(zone, applicationVersion, version, deployTime, metrics,
+ return new Deployment(zone, revision, version, deployTime, metrics,
activity.recordAt(instant, metrics), quota, cost);
}
public Deployment withMetrics(DeploymentMetrics metrics) {
- return new Deployment(zone, applicationVersion, version, deployTime, metrics, activity, quota, cost);
+ return new Deployment(zone, revision, version, deployTime, metrics, activity, quota, cost);
}
public Deployment withQuota(QuotaUsage quota) {
- return new Deployment(zone, applicationVersion, version, deployTime, metrics, activity, quota, cost);
+ return new Deployment(zone, revision, version, deployTime, metrics, activity, quota, cost);
}
public Deployment withCost(double cost) {
if (this.cost.isPresent() && Double.compare(this.cost.getAsDouble(), cost) == 0) return this;
- return new Deployment(zone, applicationVersion, version, deployTime, metrics, activity, quota, OptionalDouble.of(cost));
+ return new Deployment(zone, revision, version, deployTime, metrics, activity, quota, OptionalDouble.of(cost));
}
public Deployment withoutCost() {
if (cost.isEmpty()) return this;
- return new Deployment(zone, applicationVersion, version, deployTime, metrics, activity, quota, OptionalDouble.empty());
+ return new Deployment(zone, revision, version, deployTime, metrics, activity, quota, OptionalDouble.empty());
}
@Override
@@ -93,7 +93,7 @@ public class Deployment {
if (o == null || getClass() != o.getClass()) return false;
Deployment that = (Deployment) o;
return zone.equals(that.zone) &&
- applicationVersion.equals(that.applicationVersion) &&
+ revision.equals(that.revision) &&
version.equals(that.version) &&
deployTime.equals(that.deployTime) &&
metrics.equals(that.metrics) &&
@@ -104,12 +104,12 @@ public class Deployment {
@Override
public int hashCode() {
- return Objects.hash(zone, applicationVersion, version, deployTime, metrics, activity, quota, cost);
+ return Objects.hash(zone, revision, version, deployTime, metrics, activity, quota, cost);
}
@Override
public String toString() {
- return "deployment to " + zone + " of " + applicationVersion + " on version " + version + " at " + deployTime;
+ return "deployment to " + zone + " of " + revision + " on version " + version + " at " + deployTime;
}
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Endpoint.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Endpoint.java
index 7fc6b927bdd..8de72893a7c 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Endpoint.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Endpoint.java
@@ -160,8 +160,7 @@ public class Endpoint {
}
private static URI createUrl(String name, TenantAndApplicationId application, Optional<InstanceName> instance,
- List<Target> targets, Scope scope, SystemName system, Port port, boolean legacy,
- RoutingMethod routingMethod) {
+ List<Target> targets, Scope scope, SystemName system, Port port, boolean legacy) {
String separator = ".";
String portPart = port.isDefault() ? "" : ":" + port.port;
return URI.create("https://" +
@@ -591,8 +590,7 @@ public class Endpoint {
Objects.requireNonNull(scope, "scope must be non-null"),
Objects.requireNonNull(system, "system must be non-null"),
Objects.requireNonNull(port, "port must be non-null"),
- legacy,
- Objects.requireNonNull(routingMethod, "routingMethod must be non-null"));
+ legacy);
return new Endpoint(application,
instance,
endpointId,
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/InstanceList.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/InstanceList.java
index 99b14bf289a..6178bfbb89e 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/InstanceList.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/InstanceList.java
@@ -42,7 +42,7 @@ public class InstanceList extends AbstractFilteringList<ApplicationId, InstanceL
*/
public InstanceList compatibleWithPlatform(Version platform, Function<ApplicationId, VersionCompatibility> compatibility) {
return matching(id -> instance(id).productionDeployments().values().stream()
- .flatMap(deployment -> deployment.applicationVersion().compileVersion().stream())
+ .flatMap(deployment -> application(id).revisions().get(deployment.revision()).compileVersion().stream())
.noneMatch(version -> compatibility.apply(id).refuse(platform, version)));
}
@@ -72,8 +72,9 @@ public class InstanceList extends AbstractFilteringList<ApplicationId, InstanceL
/** Returns the subset of instances which contain declared jobs */
public InstanceList withDeclaredJobs() {
- return matching(id -> instances.get(id).jobSteps().values().stream()
- .anyMatch(job -> job.isDeclared() && job.job().get().application().equals(id)));
+ return matching(id -> instances.get(id).application().revisions().last().isPresent()
+ && instances.get(id).jobSteps().values().stream()
+ .anyMatch(job -> job.isDeclared() && job.job().get().application().equals(id)));
}
/** Returns the subset of instances which have at least one deployment on a lower version than the given one, or which have no production deployments */
@@ -95,7 +96,7 @@ public class InstanceList extends AbstractFilteringList<ApplicationId, InstanceL
/** Returns the subset of instances which are currently deploying a new revision */
public InstanceList changingRevision() {
- return matching(id -> instance(id).change().application().isPresent());
+ return matching(id -> instance(id).change().revision().isPresent());
}
/** Returns the subset of instances which currently have failing jobs on the given version */
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackage.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackage.java
index 258884a4d11..344ed7ec729 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackage.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackage.java
@@ -66,9 +66,9 @@ public class ApplicationPackage {
private static final String trustedCertificatesFile = "security/clients.pem";
private static final String buildMetaFile = "build-meta.json";
- private static final String deploymentFile = "deployment.xml";
+ static final String deploymentFile = "deployment.xml";
private static final String validationOverridesFile = "validation-overrides.xml";
- private static final String servicesFile = "services.xml";
+ static final String servicesFile = "services.xml";
private final String contentHash;
private final String bundleHash;
@@ -78,6 +78,7 @@ public class ApplicationPackage {
private final ZipArchiveCache files;
private final Optional<Version> compileVersion;
private final Optional<Instant> buildTime;
+ private final Optional<Version> parentVersion;
private final List<X509Certificate> trustedCertificates;
/**
@@ -110,6 +111,7 @@ public class ApplicationPackage {
Optional<Inspector> buildMetaObject = files.get(buildMetaFile).map(SlimeUtils::jsonToSlime).map(Slime::get);
this.compileVersion = buildMetaObject.flatMap(object -> parse(object, "compileVersion", field -> Version.fromString(field.asString())));
this.buildTime = buildMetaObject.flatMap(object -> parse(object, "buildTime", field -> Instant.ofEpochMilli(field.asLong())));
+ this.parentVersion = buildMetaObject.flatMap(object -> parse(object, "parentVersion", field -> Version.fromString(field.asString())));
this.trustedCertificates = files.get(trustedCertificatesFile).map(bytes -> X509CertificateUtils.certificateListFromPem(new String(bytes, UTF_8))).orElse(List.of());
@@ -159,6 +161,9 @@ public class ApplicationPackage {
/** Returns the time this package was built, if known. */
public Optional<Instant> buildTime() { return buildTime; }
+ /** Returns the parent version used to compile the package, if known. */
+ public Optional<Version> parentVersion() { return parentVersion; }
+
/** Returns the list of certificates trusted by this application, or an empty list if no trust configured. */
public List<X509Certificate> trustedCertificates() {
return trustedCertificates;
@@ -166,7 +171,7 @@ public class ApplicationPackage {
private static <Type> Optional<Type> parse(Inspector buildMetaObject, String fieldName, Function<Inspector, Type> mapper) {
if ( ! buildMetaObject.field(fieldName).valid())
- throw new IllegalArgumentException("Missing value '" + fieldName + "' in '" + buildMetaFile + "'");
+ return Optional.empty();
try {
return Optional.of(mapper.apply(buildMetaObject.field(fieldName)));
}
@@ -197,11 +202,11 @@ public class ApplicationPackage {
RegionName.defaultName())
.run(); // Populates the zip archive cache with files that would be included.
}
- catch (RuntimeException e) {
+ catch (IllegalArgumentException e) {
throw e;
}
catch (Exception e) {
- throw new RuntimeException(e);
+ throw new IllegalArgumentException(e);
}
}
@@ -212,7 +217,7 @@ public class ApplicationPackage {
entry -> entry.getValue().get())));
}
- static byte[] filesZip(Map<String, byte[]> files) {
+ public static byte[] filesZip(Map<String, byte[]> files) {
try (ZipBuilder zipBuilder = new ZipBuilder(files.values().stream().mapToInt(bytes -> bytes.length).sum() + 512)) {
files.forEach(zipBuilder::add);
zipBuilder.close();
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageValidator.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageValidator.java
index 9ab5096ec8f..950eaea904a 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageValidator.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageValidator.java
@@ -62,7 +62,7 @@ public class ApplicationPackageValidator {
/** Verify that each of the production zones listed in the deployment spec exist in this system */
private void validateSteps(DeploymentSpec deploymentSpec) {
for (var spec : deploymentSpec.instances()) {
- new DeploymentSteps(spec, controller::system).jobs();
+ new DeploymentSteps(spec, controller.zoneRegistry()).jobs();
spec.zones().stream()
.filter(zone -> zone.environment() == Environment.prod)
.forEach(zone -> {
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/TestPackage.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/TestPackage.java
new file mode 100644
index 00000000000..fb352848911
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/TestPackage.java
@@ -0,0 +1,318 @@
+package com.yahoo.vespa.hosted.controller.application.pkg;
+
+import com.yahoo.config.application.api.DeploymentInstanceSpec;
+import com.yahoo.config.application.api.DeploymentSpec;
+import com.yahoo.config.application.api.DeploymentSpec.Step;
+import com.yahoo.config.provision.AthenzDomain;
+import com.yahoo.config.provision.AthenzService;
+import com.yahoo.config.provision.Environment;
+import com.yahoo.config.provision.NodeResources;
+import com.yahoo.config.provision.zone.ZoneId;
+import com.yahoo.path.Path;
+import com.yahoo.security.KeyAlgorithm;
+import com.yahoo.security.KeyUtils;
+import com.yahoo.security.SignatureAlgorithm;
+import com.yahoo.security.X509CertificateBuilder;
+import com.yahoo.security.X509CertificateUtils;
+import com.yahoo.text.Text;
+import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
+import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud.Suite;
+import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterId;
+import com.yahoo.vespa.hosted.controller.config.ControllerConfig;
+import com.yahoo.vespa.hosted.controller.config.ControllerConfig.Steprunner.Testerapp;
+import com.yahoo.yolean.Exceptions;
+
+import javax.security.auth.x500.X500Principal;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.math.BigInteger;
+import java.security.KeyPair;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.jar.JarInputStream;
+import java.util.jar.Manifest;
+import java.util.regex.Pattern;
+
+import static com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud.Suite.production;
+import static com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud.Suite.staging;
+import static com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud.Suite.staging_setup;
+import static com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud.Suite.system;
+import static com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage.deploymentFile;
+import static com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage.servicesFile;
+import static com.yahoo.vespa.hosted.controller.application.pkg.ZipEntries.transferAndWrite;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.stream.Collectors.groupingBy;
+import static java.util.stream.Collectors.mapping;
+import static java.util.stream.Collectors.toList;
+
+/**
+ * Validation and manipulation of test package.
+ *
+ * @author jonmv
+ */
+public class TestPackage {
+
+ // Must match exactly the advertised resources of an AWS instance type. Also consider that the container
+ // will have ~1.8 GB less memory than equivalent resources in AWS (VESPA-16259).
+ static final NodeResources DEFAULT_TESTER_RESOURCES_AWS = new NodeResources(2, 8, 50, 0.3, NodeResources.DiskSpeed.any);
+ static final NodeResources DEFAULT_TESTER_RESOURCES = new NodeResources(1, 4, 50, 0.3, NodeResources.DiskSpeed.any);
+
+ private final ApplicationPackage applicationPackage;
+ private final X509Certificate certificate;
+
+ public TestPackage(byte[] testPackage, boolean isPublicSystem, RunId id, Testerapp testerApp,
+ DeploymentSpec spec, Instant certificateValidFrom, Duration certificateValidDuration) {
+
+ // Copy contents of submitted application-test.zip, and ensure required directories exist within the zip.
+ Map<String, byte[]> entries = new HashMap<>();
+ entries.put("artifacts/.ignore-" + UUID.randomUUID(), new byte[0]);
+ entries.put("tests/.ignore-" + UUID.randomUUID(), new byte[0]);
+
+ entries.put(servicesFile,
+ servicesXml( ! isPublicSystem,
+ certificateValidFrom != null,
+ testerResourcesFor(id.type().zone(), spec.requireInstance(id.application().instance())),
+ testerApp));
+
+ entries.put(deploymentFile,
+ deploymentXml(id.tester(),
+ spec.athenzDomain(),
+ spec.requireInstance(id.application().instance())
+ .athenzService(id.type().zone().environment(), id.type().zone().region())));
+
+ if (certificateValidFrom != null) {
+ KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.RSA, 2048);
+ X500Principal subject = new X500Principal("CN=" + id.tester().id().toFullString() + "." + id.type() + "." + id.number());
+ this.certificate = X509CertificateBuilder.fromKeypair(keyPair,
+ subject,
+ certificateValidFrom,
+ certificateValidFrom.plus(certificateValidDuration),
+ SignatureAlgorithm.SHA512_WITH_RSA,
+ BigInteger.valueOf(1))
+ .build();
+ entries.put("artifacts/key", KeyUtils.toPem(keyPair.getPrivate()).getBytes(UTF_8));
+ entries.put("artifacts/cert", X509CertificateUtils.toPem(certificate).getBytes(UTF_8));
+ }
+ else {
+ this.certificate = null;
+ }
+
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream(testPackage.length + 10_000);
+ transferAndWrite(buffer, new ByteArrayInputStream(testPackage), entries);
+ this.applicationPackage = new ApplicationPackage(buffer.toByteArray());
+ }
+
+ public ApplicationPackage asApplicationPackage() {
+ return applicationPackage;
+ }
+
+ public X509Certificate certificate() {
+ return Objects.requireNonNull(certificate);
+ }
+
+ public static TestSummary validateTests(DeploymentSpec spec, byte[] testPackage) {
+ return validateTests(expectedSuites(spec.steps()), testPackage);
+ }
+
+ static TestSummary validateTests(Collection<Suite> expectedSuites, byte[] testPackage) {
+ List<String> problems = new ArrayList<>();
+ Set<Suite> suites = new LinkedHashSet<>();
+ ZipEntries.from(testPackage, __ -> true, 0, false).asList().stream()
+ .map(entry -> Path.fromString(entry.name()))
+ .collect(groupingBy(path -> path.elements().size() > 1 ? path.elements().get(0) : "",
+ mapping(path -> (path.elements().size() > 1 ? path.getChildPath() : path).getRelative(), toList())))
+ .forEach((directory, paths) -> {
+ switch (directory) {
+ case "components": {
+ for (String path : paths) {
+ if (path.endsWith("-tests.jar")) {
+ try {
+ byte[] testsJar = ZipEntries.readFile(testPackage, "components/" + path, 1 << 30);
+ Manifest manifest = new JarInputStream(new ByteArrayInputStream(testsJar)).getManifest();
+ for (String suite : manifest.getMainAttributes().getValue("X-JDisc-Test-Bundle-Categories").split(","))
+ switch (suite.trim()) {
+ case "SystemTest": suites.add(system); break;
+ case "StagingSetup": suites.add(staging_setup); break;
+ case "StagingTest": suites.add(staging); break;
+ case "ProductionTest": suites.add(production); break;
+ default: problems.add("unexpected test suite name '" + suite + "' in bundle manifest");
+ }
+ }
+ catch (Exception e) {
+ problems.add("failed reading test bundle manifest: " + Exceptions.toMessageString(e));
+ }
+ }
+ }
+ }
+ break;
+ case "tests": {
+ if (paths.stream().anyMatch(Pattern.compile("system-test/.+\\.json").asMatchPredicate())) suites.add(system);
+ if (paths.stream().anyMatch(Pattern.compile("staging-setup/.+\\.json").asMatchPredicate())) suites.add(staging_setup);
+ if (paths.stream().anyMatch(Pattern.compile("staging-test/.+\\.json").asMatchPredicate())) suites.add(staging);
+ if (paths.stream().anyMatch(Pattern.compile("production-test/.+\\.json").asMatchPredicate())) suites.add(production);
+ }
+ break;
+ case "artifacts": {
+ if (paths.stream().anyMatch(Pattern.compile(".+-tests.jar").asMatchPredicate()))
+ suites.addAll(expectedSuites); // ಠ_ಠ
+
+ for (String forbidden : List.of("key", "cert"))
+ if (paths.contains(forbidden))
+ problems.add("test package contains 'artifacts/" + forbidden +
+ "'; this conflicts with credentials used to run tests in Vespa Cloud");
+ }
+ break;
+ }
+ });
+
+ if (expectedSuites.contains(system) && ! suites.contains(system))
+ problems.add("test package has no system tests, but <test /> is declared in deployment.xml");
+
+ if (suites.contains(staging) != suites.contains(staging_setup))
+ problems.add("test package has " + (suites.contains(staging) ? "staging tests" : "staging setup") +
+ ", so it should also include " + (suites.contains(staging) ? "staging setup" : "staging tests"));
+ else if (expectedSuites.contains(staging) && ! suites.contains(staging))
+ problems.add("test package has no staging setup and tests, but <staging /> is declared in deployment.xml");
+
+ if (suites.contains(production) != expectedSuites.contains(production))
+ problems.add("test package has " + (suites.contains(production) ? "" : "no ") + "production tests, " +
+ "but " + (suites.contains(production) ? "no " : "") + "production tests are declared in deployment.xml");
+
+ if ( ! problems.isEmpty())
+ problems.add("see https://docs.vespa.ai/en/testing.html for details on how to write system tests for Vespa");
+
+ return new TestSummary(problems, suites);
+ }
+
+ public static NodeResources testerResourcesFor(ZoneId zone, DeploymentInstanceSpec spec) {
+ NodeResources nodeResources = spec.steps().stream()
+ .filter(step -> step.concerns(zone.environment()))
+ .findFirst()
+ .flatMap(step -> step.zones().get(0).testerFlavor())
+ .map(NodeResources::fromLegacyName)
+ .orElse(zone.region().value().contains("aws-") ? DEFAULT_TESTER_RESOURCES_AWS
+ : DEFAULT_TESTER_RESOURCES);
+ return nodeResources.with(NodeResources.DiskSpeed.any);
+ }
+
+ /** Returns the generated services.xml content for the tester application. */
+ public static byte[] servicesXml(boolean systemUsesAthenz, boolean useTesterCertificate,
+ NodeResources resources, ControllerConfig.Steprunner.Testerapp config) {
+ int jdiscMemoryGb = 2; // 2Gb memory for tester application (excessive?).
+ int jdiscMemoryPct = (int) Math.ceil(100 * jdiscMemoryGb / resources.memoryGb());
+
+ // Of the remaining memory, split 50/50 between Surefire running the tests and the rest
+ int testMemoryMb = (int) (1024 * (resources.memoryGb() - jdiscMemoryGb) / 2);
+
+ String resourceString = Text.format("<resources vcpu=\"%.2f\" memory=\"%.2fGb\" disk=\"%.2fGb\" disk-speed=\"%s\" storage-type=\"%s\"/>",
+ resources.vcpu(), resources.memoryGb(), resources.diskGb(), resources.diskSpeed().name(), resources.storageType().name());
+
+ String runtimeProviderClass = config.runtimeProviderClass();
+ String tenantCdBundle = config.tenantCdBundle();
+
+ String servicesXml =
+ "<?xml version='1.0' encoding='UTF-8'?>\n" +
+ "<services xmlns:deploy='vespa' version='1.0'>\n" +
+ " <container version='1.0' id='tester'>\n" +
+ "\n" +
+ " <component id=\"com.yahoo.vespa.hosted.testrunner.TestRunner\" bundle=\"vespa-testrunner-components\">\n" +
+ " <config name=\"com.yahoo.vespa.hosted.testrunner.test-runner\">\n" +
+ " <artifactsPath>artifacts</artifactsPath>\n" +
+ " <surefireMemoryMb>" + testMemoryMb + "</surefireMemoryMb>\n" +
+ " <useAthenzCredentials>" + systemUsesAthenz + "</useAthenzCredentials>\n" +
+ " <useTesterCertificate>" + useTesterCertificate + "</useTesterCertificate>\n" +
+ " </config>\n" +
+ " </component>\n" +
+ "\n" +
+ " <handler id=\"com.yahoo.vespa.testrunner.TestRunnerHandler\" bundle=\"vespa-osgi-testrunner\">\n" +
+ " <binding>http://*/tester/v1/*</binding>\n" +
+ " </handler>\n" +
+ "\n" +
+ " <component id=\"" + runtimeProviderClass + "\" bundle=\"" + tenantCdBundle + "\" />\n" +
+ "\n" +
+ " <component id=\"com.yahoo.vespa.testrunner.JunitRunner\" bundle=\"vespa-osgi-testrunner\">\n" +
+ " <config name=\"com.yahoo.vespa.testrunner.junit-test-runner\">\n" +
+ " <artifactsPath>artifacts</artifactsPath>\n" +
+ " <useAthenzCredentials>" + systemUsesAthenz + "</useAthenzCredentials>\n" +
+ " </config>\n" +
+ " </component>\n" +
+ "\n" +
+ " <component id=\"com.yahoo.vespa.testrunner.VespaCliTestRunner\" bundle=\"vespa-osgi-testrunner\">\n" +
+ " <config name=\"com.yahoo.vespa.testrunner.vespa-cli-test-runner\">\n" +
+ " <artifactsPath>artifacts</artifactsPath>\n" +
+ " <testsPath>tests</testsPath>\n" +
+ " <useAthenzCredentials>" + systemUsesAthenz + "</useAthenzCredentials>\n" +
+ " </config>\n" +
+ " </component>\n" +
+ "\n" +
+ " <nodes count=\"1\">\n" +
+ " <jvm allocated-memory=\"" + jdiscMemoryPct + "%\"/>\n" +
+ " " + resourceString + "\n" +
+ " </nodes>\n" +
+ " </container>\n" +
+ "</services>\n";
+
+ return servicesXml.getBytes(UTF_8);
+ }
+
+ /** Returns a dummy deployment xml which sets up the service identity for the tester, if present. */
+ public static byte[] deploymentXml(TesterId id, Optional<AthenzDomain> athenzDomain, Optional<AthenzService> athenzService) {
+ String deploymentSpec =
+ "<?xml version='1.0' encoding='UTF-8'?>\n" +
+ "<deployment version=\"1.0\" " +
+ athenzDomain.map(domain -> "athenz-domain=\"" + domain.value() + "\" ").orElse("") +
+ athenzService.map(service -> "athenz-service=\"" + service.value() + "\" ").orElse("") + ">" +
+ " <instance id=\"" + id.id().instance().value() + "\" />" +
+ "</deployment>";
+ return deploymentSpec.getBytes(UTF_8);
+ }
+
+ static Set<Suite> expectedSuites(List<Step> steps) {
+ Set<Suite> suites = new HashSet<>();
+ if (steps.isEmpty()) return suites;
+ for (Step step : steps) {
+ if (step.isTest()) {
+ if (step.concerns(Environment.prod)) suites.add(production);
+ if (step.concerns(Environment.test)) suites.add(system);
+ if (step.concerns(Environment.staging)) { suites.add(staging); suites.add(staging_setup); }
+ }
+ else
+ suites.addAll(expectedSuites(step.steps()));
+ }
+ return suites;
+ }
+
+
+ public static class TestSummary {
+
+ private final List<String> problems;
+ private final List<Suite> suites;
+
+ public TestSummary(List<String> problems, Set<Suite> suites) {
+ this.problems = List.copyOf(problems);
+ this.suites = List.copyOf(suites);
+ }
+
+ public List<String> problems() {
+ return problems;
+ }
+
+ public List<Suite> suites() {
+ return suites;
+ }
+
+ }
+
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ZipEntries.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ZipEntries.java
index a6cb7f23fc3..8392a77bad5 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ZipEntries.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ZipEntries.java
@@ -13,6 +13,8 @@ import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;
@@ -35,19 +37,28 @@ public class ZipEntries {
/** Copies the zipped content from in to out, adding/overwriting an entry with the given name and content. */
public static void transferAndWrite(OutputStream out, InputStream in, String name, byte[] content) {
+ transferAndWrite(out, in, Map.of(name, content));
+ }
+
+ /** Copies the zipped content from in to out, adding/overwriting/removing (on {@code null}) entries as specified. */
+ public static void transferAndWrite(OutputStream out, InputStream in, Map<String, byte[]> entries) {
try (ZipOutputStream zipOut = new ZipOutputStream(out);
ZipInputStream zipIn = new ZipInputStream(in)) {
for (ZipEntry entry = zipIn.getNextEntry(); entry != null; entry = zipIn.getNextEntry()) {
- if (entry.getName().equals(name))
+ if (entries.containsKey(entry.getName()))
continue;
zipOut.putNextEntry(new ZipEntry(entry.getName()));
zipIn.transferTo(zipOut);
zipOut.closeEntry();
}
- zipOut.putNextEntry(new ZipEntry(name));
- zipOut.write(content);
- zipOut.closeEntry();
+ for (Entry<String, byte[]> entry : entries.entrySet()) {
+ if (entry.getValue() != null) {
+ zipOut.putNextEntry(new ZipEntry(entry.getKey()));
+ zipOut.write(entry.getValue());
+ zipOut.closeEntry();
+ }
+ }
}
catch (IOException e) {
throw new UncheckedIOException(e);
@@ -76,6 +87,10 @@ public class ZipEntries {
return new ZipEntries(entries);
}
+ public static byte[] readFile(byte[] zip, String name, int maxEntrySizeInBytes) {
+ return from(zip, name::equals, maxEntrySizeInBytes, true).asList().get(0).contentOrThrow();
+ }
+
public List<ZipEntryWithContent> asList() { return entries; }
public static class ZipEntryWithContent {
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzFacade.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzFacade.java
index f5900604627..2ad28893e18 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzFacade.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzFacade.java
@@ -7,7 +7,6 @@ import com.google.inject.Inject;
import com.yahoo.config.provision.ApplicationName;
import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.TenantName;
-import com.yahoo.config.provision.Zone;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.text.Text;
import com.yahoo.vespa.athenz.api.AthenzDomain;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/auditlog/AuditLogger.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/auditlog/AuditLogger.java
index 78d65766075..34e7955e02a 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/auditlog/AuditLogger.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/auditlog/AuditLogger.java
@@ -2,6 +2,7 @@
package com.yahoo.vespa.hosted.controller.auditlog;
import com.yahoo.container.jdisc.HttpRequest;
+import com.yahoo.transaction.Mutex;
import com.yahoo.vespa.curator.Lock;
import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
@@ -70,7 +71,7 @@ public class AuditLogger {
Instant now = clock.instant();
AuditLog.Entry entry = new AuditLog.Entry(now, principal.getName(), method.get(), pathAndQueryOf(request.getUri()),
Optional.of(new String(data, StandardCharsets.UTF_8)));
- try (Lock lock = db.lockAuditLog()) {
+ try (Mutex lock = db.lockAuditLog()) {
AuditLog auditLog = db.readAuditLog()
.pruneBefore(now.minus(entryTtl))
.with(entry)
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentStatus.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentStatus.java
index a042215616c..cc7031bab5a 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentStatus.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentStatus.java
@@ -10,14 +10,16 @@ import com.yahoo.config.application.api.DeploymentSpec.DeclaredTest;
import com.yahoo.config.application.api.DeploymentSpec.DeclaredZone;
import com.yahoo.config.application.api.DeploymentSpec.UpgradeRollout;
import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.InstanceName;
-import com.yahoo.config.provision.SystemName;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.Instance;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
+import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
+import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry;
import com.yahoo.vespa.hosted.controller.application.Change;
import com.yahoo.vespa.hosted.controller.application.Deployment;
@@ -26,6 +28,7 @@ import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
@@ -42,13 +45,12 @@ import static com.yahoo.config.application.api.DeploymentSpec.RevisionTarget.nex
import static com.yahoo.config.provision.Environment.prod;
import static com.yahoo.config.provision.Environment.staging;
import static com.yahoo.config.provision.Environment.test;
-import static com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType.stagingTest;
-import static com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType.systemTest;
import static java.util.Comparator.comparing;
import static java.util.Comparator.naturalOrder;
import static java.util.Objects.requireNonNull;
import static java.util.function.BinaryOperator.maxBy;
import static java.util.stream.Collectors.collectingAndThen;
+import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toUnmodifiableList;
@@ -59,51 +61,33 @@ import static java.util.stream.Collectors.toUnmodifiableList;
*/
public class DeploymentStatus {
- public static List<JobId> jobsFor(Application application, SystemName system) {
- if (DeploymentSpec.empty.equals(application.deploymentSpec()))
- return List.of();
-
- return application.deploymentSpec().instances().stream()
- .flatMap(spec -> Stream.concat(Stream.of(systemTest, stagingTest),
- flatten(spec).filter(step -> step.concerns(prod))
- .map(step -> {
- if (step instanceof DeclaredZone)
- return JobType.from(system, prod, ((DeclaredZone) step).region().get());
- return JobType.testFrom(system, ((DeclaredTest) step).region());
- })
- .flatMap(Optional::stream))
- .map(type -> new JobId(application.id().instance(spec.name()), type)))
- .collect(toUnmodifiableList());
- }
-
- private static Stream<DeploymentSpec.Step> flatten(DeploymentSpec.Step step) {
- return step instanceof DeploymentSpec.Steps ? step.steps().stream().flatMap(DeploymentStatus::flatten) : Stream.of(step);
- }
-
private static <T> List<T> union(List<T> first, List<T> second) {
return Stream.concat(first.stream(), second.stream()).distinct().collect(toUnmodifiableList());
}
private final Application application;
private final JobList allJobs;
- private final SystemName system;
+ private final JobType systemTest;
+ private final JobType stagingTest;
private final Version systemVersion;
private final Function<InstanceName, VersionCompatibility> versionCompatibility;
private final Instant now;
private final Map<JobId, StepStatus> jobSteps;
private final List<StepStatus> allSteps;
- public DeploymentStatus(Application application, Map<JobId, JobStatus> allJobs, SystemName system,
+ public DeploymentStatus(Application application, Function<JobId, JobStatus> allJobs, ZoneRegistry zones,
Version systemVersion, Function<InstanceName, VersionCompatibility> versionCompatibility, Instant now) {
this.application = requireNonNull(application);
- this.allJobs = JobList.from(allJobs.values());
- this.system = requireNonNull(system);
+ this.systemTest = JobType.systemTest(zones);
+ this.stagingTest = JobType.stagingTest(zones);
this.systemVersion = requireNonNull(systemVersion);
this.versionCompatibility = versionCompatibility;
this.now = requireNonNull(now);
List<StepStatus> allSteps = new ArrayList<>();
- this.jobSteps = jobDependencies(application.deploymentSpec(), allSteps);
+ Map<JobId, JobStatus> jobs = new HashMap<>();
+ this.jobSteps = jobDependencies(application.deploymentSpec(), allSteps, job -> jobs.computeIfAbsent(job, allJobs));
this.allSteps = Collections.unmodifiableList(allSteps);
+ this.allJobs = JobList.from(jobSteps.keySet().stream().map(allJobs).collect(toList()));
}
/** The application this deployment status concerns. */
@@ -143,9 +127,9 @@ public class DeploymentStatus {
}
/** Whether any job is failing on versions selected by the given filter, with errors other than lack of capacity in a test zone.. */
- public boolean hasFailures(Predicate<ApplicationVersion> versionFilter) {
+ public boolean hasFailures(Predicate<RevisionId> revisionFilter) {
return ! allJobs.failingHard()
- .matching(job -> versionFilter.test(job.lastTriggered().get().versions().targetApplication()))
+ .matching(job -> revisionFilter.test(job.lastTriggered().get().versions().targetRevision()))
.isEmpty();
}
@@ -172,6 +156,8 @@ public class DeploymentStatus {
* and any test jobs for any outstanding change, which will likely be needed to later deploy this change.
*/
public Map<JobId, List<Job>> jobsToRun() {
+ if (application.revisions().last().isEmpty()) return Map.of();
+
Map<InstanceName, Change> changes = new LinkedHashMap<>();
for (InstanceName instance : application.deploymentSpec().instanceNames())
changes.put(instance, application.require(instance).change());
@@ -196,6 +182,8 @@ public class DeploymentStatus {
}
private Map<JobId, List<Job>> jobsToRun(Map<InstanceName, Change> changes, boolean eagerTests) {
+ if (application.revisions().last().isEmpty()) return Map.of();
+
Map<JobId, List<Job>> productionJobs = new LinkedHashMap<>();
changes.forEach((instance, change) -> productionJobs.putAll(productionJobs(instance, change, eagerTests)));
Map<JobId, List<Job>> testJobs = testJobs(productionJobs);
@@ -256,7 +244,7 @@ public class DeploymentStatus {
public Optional<Deployment> deploymentFor(JobId job) {
return Optional.ofNullable(application.require(job.application().instance())
- .deployments().get(job.type().zone(system)));
+ .deployments().get(job.type().zone()));
}
/**
@@ -269,19 +257,47 @@ public class DeploymentStatus {
public Change outstandingChange(InstanceName instance) {
StepStatus status = instanceSteps().get(instance);
if (status == null) return Change.empty();
- boolean ascending = next == application.deploymentSpec().requireInstance(instance).revisionTarget();
- for (ApplicationVersion version : application.deployableVersions(ascending)) {
- if (status.dependenciesCompletedAt(Change.of(version), Optional.empty()).map(now::isBefore).orElse(true)) continue;
- Change change = Change.of(version);
+ DeploymentInstanceSpec spec = application.deploymentSpec().requireInstance(instance);
+ boolean ascending = next == spec.revisionTarget();
+ int cumulativeRisk = 0;
+ int nextRisk = 0;
+ int skippedCumulativeRisk = 0;
+ Instant readySince = now;
+ Change candidate = Change.empty();
+ for (ApplicationVersion version : application.revisions().deployable(ascending)) {
+ // A revision is only a candidate if it upgrades, and does not downgrade, this instance.
+ Change change = Change.of(version.id());
if (application.productionDeployments().getOrDefault(instance, List.of()).stream()
- .anyMatch(deployment -> change.downgrades(deployment.applicationVersion()))) continue;
- if ( ! application.require(instance).change().application().map(change::upgrades).orElse(true)) continue;
+ .anyMatch(deployment -> change.downgrades(deployment.revision()))) continue;
+ if ( ! application.require(instance).change().revision().map(change::upgrades).orElse(true)) continue;
if (hasCompleted(instance, change))
- if (ascending) continue;
- else break;
- return change;
+ if (ascending) continue; // Keep looking for the next revision which is an upgrade, or ...
+ else return Change.empty(); // ... if the latest is already complete, there's nothing outstanding.
+
+ // This revision contains something new, so start aggregating the risk score.
+ skippedCumulativeRisk += version.risk();
+ nextRisk = nextRisk > 0 ? nextRisk : version.risk();
+ // If it's not yet ready to roll out, we keep looking.
+ Optional<Instant> readyAt = status.dependenciesCompletedAt(Change.of(version.id()), Optional.empty());
+ if (readyAt.map(now::isBefore).orElse(true)) continue;
+
+ // It's ready. If looking for the latest, max risk is 0, and we'll return now; otherwise, we _may_ keep on looking for more.
+ cumulativeRisk += skippedCumulativeRisk;
+ skippedCumulativeRisk = 0;
+ nextRisk = 0;
+ if (cumulativeRisk >= spec.maxRisk())
+ return candidate.equals(Change.empty()) ? change : candidate; // If the first candidate exceeds max risk, we have to accept that.
+
+ // Otherwise, we may note this as a candidate, and keep looking for a newer revision, unless that makes us exceed max risk.
+ if (readyAt.get().isBefore(readySince)) readySince = readyAt.get();
+ candidate = change;
}
- return Change.empty();
+ // If min risk is ready, or max idle time has passed, we return the candidate. Otherwise, no outstanding change is ready.
+ return instanceJobs(instance).values().stream().allMatch(jobs -> jobs.lastTriggered().isEmpty())
+ || cumulativeRisk >= spec.minRisk()
+ || cumulativeRisk + nextRisk > spec.maxRisk()
+ || ! now.isBefore(readySince.plus(Duration.ofHours(spec.maxIdleHours())))
+ ? candidate : Change.empty();
}
/** Earliest instant when job was triggered with given versions, or both system and staging tests were successful. */
@@ -305,7 +321,7 @@ public class DeploymentStatus {
.type(type).asList().stream()
.flatMap(status -> RunList.from(status)
.on(versions)
- .status(RunStatus.success)
+ .matching(Run::hasSucceeded)
.asList().stream()
.map(Run::start))
.min(naturalOrder());
@@ -324,9 +340,9 @@ public class DeploymentStatus {
// When computing eager test jobs for outstanding changes, assume current change completes successfully.
Optional<Deployment> deployment = deploymentFor(job);
Optional<Version> existingPlatform = deployment.map(Deployment::version);
- Optional<ApplicationVersion> existingApplication = deployment.map(Deployment::applicationVersion);
- boolean deployingCompatibilityChange = areIncompatible(existingPlatform, change.application(), instance)
- || areIncompatible(change.platform(), existingApplication, instance);
+ Optional<RevisionId> existingRevision = deployment.map(Deployment::revision);
+ boolean deployingCompatibilityChange = areIncompatible(existingPlatform, change.revision(), job)
+ || areIncompatible(change.platform(), existingRevision, job);
if (assumeUpgradesSucceed) {
if (deployingCompatibilityChange) // No eager tests for this.
return;
@@ -334,34 +350,36 @@ public class DeploymentStatus {
Change currentChange = application.require(instance).change();
Versions target = Versions.from(currentChange, application, deployment, systemVersion);
existingPlatform = Optional.of(target.targetPlatform());
- existingApplication = Optional.of(target.targetApplication());
+ existingRevision = Optional.of(target.targetRevision());
}
List<Job> toRun = new ArrayList<>();
List<Change> changes = deployingCompatibilityChange ? List.of(change) : changes(job, step, change);
for (Change partial : changes) {
Job jobToRun = new Job(job.type(),
- Versions.from(partial, application, existingPlatform, existingApplication, systemVersion),
+ Versions.from(partial, application, existingPlatform, existingRevision, systemVersion),
step.readyAt(partial, Optional.of(job)),
partial);
toRun.add(jobToRun);
// Assume first partial change is applied before the second.
existingPlatform = Optional.of(jobToRun.versions.targetPlatform());
- existingApplication = Optional.of(jobToRun.versions.targetApplication());
+ existingRevision = Optional.of(jobToRun.versions.targetRevision());
}
jobs.put(job, toRun);
});
return jobs;
}
- private boolean areIncompatible(Optional<Version> platform, Optional<ApplicationVersion> application, InstanceName instance) {
+ private boolean areIncompatible(Optional<Version> platform, Optional<RevisionId> revision, JobId job) {
+ Optional<Version> compileVersion = revision.map(application.revisions()::get)
+ .flatMap(ApplicationVersion::compileVersion);
return platform.isPresent()
- && application.flatMap(ApplicationVersion::compileVersion).isPresent()
- && versionCompatibility.apply(instance).refuse(platform.get(), application.get().compileVersion().get());
+ && compileVersion.isPresent()
+ && versionCompatibility.apply(job.application().instance()).refuse(platform.get(), compileVersion.get());
}
/** Changes to deploy with the given job, possibly split in two steps. */
private List<Change> changes(JobId job, StepStatus step, Change change) {
- if (change.platform().isEmpty() || change.application().isEmpty() || change.isPinned())
+ if (change.platform().isEmpty() || change.revision().isEmpty() || change.isPinned())
return List.of(change);
if ( step.completedAt(change.withoutApplication(), Optional.of(job)).isPresent()
@@ -370,7 +388,7 @@ public class DeploymentStatus {
// For a dual change, where both targets remain, we determine what to run by looking at when the two parts became ready:
// for deployments, we look at dependencies; for production tests, this may be overridden by what is already deployed.
- JobId deployment = new JobId(job.application(), JobType.from(system, job.type().zone(system)).get());
+ JobId deployment = new JobId(job.application(), JobType.deploymentTo(job.type().zone()));
UpgradeRollout rollout = application.deploymentSpec().requireInstance(job.application().instance()).upgradeRollout();
if (job.type().isTest()) {
Optional<Instant> platformDeployedAt = jobSteps.get(deployment).completedAt(change.withoutApplication(), Optional.of(deployment));
@@ -468,7 +486,7 @@ public class DeploymentStatus {
if ( job.type().isProduction() && job.type().isDeployment()
&& allJobs.successOn(productionJob.versions()).type(testType).isEmpty()
&& testJobs.keySet().stream()
- .noneMatch(test -> test.type() == testType
+ .noneMatch(test -> test.type().equals(testType)
&& testJobs.get(test).stream().anyMatch(testJob -> testJob.versions().equals(productionJob.versions())))) {
JobId testJob = firstDeclaredOrElseImplicitTest(testType);
testJobs.merge(testJob,
@@ -486,31 +504,44 @@ public class DeploymentStatus {
private JobId firstDeclaredOrElseImplicitTest(JobType testJob) {
return application.deploymentSpec().instanceNames().stream()
.map(name -> new JobId(application.id().instance(name), testJob))
+ .filter(jobSteps::containsKey)
.min(comparing(id -> ! jobSteps.get(id).isDeclared())).orElseThrow();
}
/** JobId of any declared test of the given type, for the given instance. */
private Optional<JobId> declaredTest(ApplicationId instanceId, JobType testJob) {
JobId jobId = new JobId(instanceId, testJob);
- return jobSteps.get(jobId).isDeclared() ? Optional.of(jobId) : Optional.empty();
+ return jobSteps.containsKey(jobId) && jobSteps.get(jobId).isDeclared() ? Optional.of(jobId) : Optional.empty();
}
/** A DAG of the dependencies between the primitive steps in the spec, with iteration order equal to declaration order. */
- private Map<JobId, StepStatus> jobDependencies(DeploymentSpec spec, List<StepStatus> allSteps) {
+ private Map<JobId, StepStatus> jobDependencies(DeploymentSpec spec, List<StepStatus> allSteps, Function<JobId, JobStatus> jobs) {
if (DeploymentSpec.empty.equals(spec))
return Map.of();
Map<JobId, StepStatus> dependencies = new LinkedHashMap<>();
List<StepStatus> previous = List.of();
for (DeploymentSpec.Step step : spec.steps())
- previous = fillStep(dependencies, allSteps, step, previous, null);
+ previous = fillStep(dependencies, allSteps, step, previous, null, jobs,
+ instanceWithImplicitTest(test, spec),
+ instanceWithImplicitTest(staging, spec));
return Collections.unmodifiableMap(dependencies);
}
+ private static InstanceName instanceWithImplicitTest(Environment environment, DeploymentSpec spec) {
+ InstanceName first = null;
+ for (DeploymentInstanceSpec step : spec.instances()) {
+ if (step.concerns(environment)) return null;
+ first = first != null ? first : step.name();
+ }
+ return first;
+ }
+
/** Adds the primitive steps contained in the given step, which depend on the given previous primitives, to the dependency graph. */
- private List<StepStatus> fillStep(Map<JobId, StepStatus> dependencies, List<StepStatus> allSteps,
- DeploymentSpec.Step step, List<StepStatus> previous, InstanceName instance) {
+ private List<StepStatus> fillStep(Map<JobId, StepStatus> dependencies, List<StepStatus> allSteps, DeploymentSpec.Step step,
+ List<StepStatus> previous, InstanceName instance, Function<JobId, JobStatus> jobs,
+ InstanceName implicitSystemTest, InstanceName implicitStagingTest) {
if (step.steps().isEmpty() && ! (step instanceof DeploymentInstanceSpec)) {
if (instance == null)
return previous; // Ignore test and staging outside all instances.
@@ -522,31 +553,28 @@ public class DeploymentStatus {
}
JobType jobType;
+ JobId jobId;
StepStatus stepStatus;
if (step.concerns(test) || step.concerns(staging)) {
- jobType = JobType.from(system, ((DeclaredZone) step).environment(), null)
- .orElseThrow(() -> new IllegalStateException(application + " specifies " + step + ", but this has no job in " + system));
- stepStatus = JobStepStatus.ofTestDeployment((DeclaredZone) step, List.of(), this, instance, jobType, true);
+ jobType = step.concerns(test) ? systemTest : stagingTest;
+ jobId = new JobId(application.id().instance(instance), jobType);
+ stepStatus = JobStepStatus.ofTestDeployment((DeclaredZone) step, List.of(), this, jobs.apply(jobId), true);
previous = new ArrayList<>(previous);
previous.add(stepStatus);
}
else if (step.isTest()) {
- jobType = JobType.testFrom(system, ((DeclaredTest) step).region())
- .orElseThrow(() -> new IllegalStateException(application + " specifies " + step + ", but this has no job in " + system));
- JobType preType = JobType.from(system, prod, ((DeclaredTest) step).region())
- .orElseThrow(() -> new IllegalStateException(application + " specifies " + step + ", but this has no job in " + system));
- stepStatus = JobStepStatus.ofProductionTest((DeclaredTest) step, previous, this, instance, jobType, preType);
+ jobType = JobType.test(((DeclaredTest) step).region());
+ jobId = new JobId(application.id().instance(instance), jobType);
+ stepStatus = JobStepStatus.ofProductionTest((DeclaredTest) step, previous, this, jobs.apply(jobId));
previous = List.of(stepStatus);
}
else if (step.concerns(prod)) {
- jobType = JobType.from(system, ((DeclaredZone) step).environment(), ((DeclaredZone) step).region().get())
- .orElseThrow(() -> new IllegalStateException(application + " specifies " + step + ", but this has no job in " + system));
- stepStatus = JobStepStatus.ofProductionDeployment((DeclaredZone) step, previous, this, instance, jobType);
+ jobType = JobType.prod(((DeclaredZone) step).region().get());
+ jobId = new JobId(application.id().instance(instance), jobType);
+ stepStatus = JobStepStatus.ofProductionDeployment((DeclaredZone) step, previous, this, jobs.apply(jobId));
previous = List.of(stepStatus);
}
else return previous; // Empty container steps end up here, and are simply ignored.
- JobId jobId = new JobId(application.id().instance(instance), jobType);
- allSteps.removeIf(existing -> existing.job().equals(Optional.of(jobId))); // Replace implicit tests with explicit ones.
allSteps.add(stepStatus);
dependencies.put(jobId, stepStatus);
return previous;
@@ -558,27 +586,32 @@ public class DeploymentStatus {
instance = spec.name();
allSteps.add(instanceStatus);
previous = List.of(instanceStatus);
- for (JobType test : List.of(systemTest, stagingTest)) {
- JobId job = new JobId(application.id().instance(instance), test);
- if ( ! dependencies.containsKey(job)) {
- var testStatus = JobStepStatus.ofTestDeployment(new DeclaredZone(test.environment()), List.of(),
- this, job.application().instance(), test, false);
- dependencies.put(job, testStatus);
- allSteps.add(testStatus);
- }
+ if (instance.equals(implicitSystemTest)) {
+ JobId job = new JobId(application.id().instance(instance), systemTest);
+ JobStepStatus testStatus = JobStepStatus.ofTestDeployment(new DeclaredZone(test), List.of(),
+ this, jobs.apply(job), false);
+ dependencies.put(job, testStatus);
+ allSteps.add(testStatus);
+ }
+ if (instance.equals(implicitStagingTest)) {
+ JobId job = new JobId(application.id().instance(instance), stagingTest);
+ JobStepStatus testStatus = JobStepStatus.ofTestDeployment(new DeclaredZone(staging), List.of(),
+ this, jobs.apply(job), false);
+ dependencies.put(job, testStatus);
+ allSteps.add(testStatus);
}
}
if (step.isOrdered()) {
for (DeploymentSpec.Step nested : step.steps())
- previous = fillStep(dependencies, allSteps, nested, previous, instance);
+ previous = fillStep(dependencies, allSteps, nested, previous, instance, jobs, implicitSystemTest, implicitStagingTest);
return previous;
}
List<StepStatus> parallel = new ArrayList<>();
for (DeploymentSpec.Step nested : step.steps())
- parallel.addAll(fillStep(dependencies, allSteps, nested, previous, instance));
+ parallel.addAll(fillStep(dependencies, allSteps, nested, previous, instance, jobs, implicitSystemTest, implicitStagingTest));
return List.copyOf(parallel);
}
@@ -718,7 +751,7 @@ public class DeploymentStatus {
@Override
Optional<Instant> completedAt(Change change, Optional<JobId> dependent) {
return ( (change.platform().isEmpty() || change.platform().equals(instance.change().platform()))
- && (change.application().isEmpty() || change.application().equals(instance.change().application()))
+ && (change.revision().isEmpty() || change.revision().equals(instance.change().revision()))
|| step().steps().stream().noneMatch(step -> step.concerns(prod)))
? dependenciesCompletedAt(change, dependent).or(() -> Optional.of(Instant.EPOCH).filter(__ -> change.hasTargets()))
: Optional.empty();
@@ -732,7 +765,7 @@ public class DeploymentStatus {
while ( blocker.window().includes(current)
&& now.plus(Duration.ofDays(7)).isAfter(current)
&& ( change.platform().isPresent() && blocker.blocksVersions()
- || change.application().isPresent() && blocker.blocksRevisions())) {
+ || change.revision().isPresent() && blocker.blocksRevisions())) {
blocked = true;
current = current.plus(Duration.ofHours(1)).truncatedTo(ChronoUnit.HOURS);
}
@@ -773,7 +806,7 @@ public class DeploymentStatus {
if (job.firstFailing().isEmpty() || ! job.firstFailing().get().hasEnded()) return Optional.empty();
Versions lastVersions = job.lastCompleted().get().versions();
if (change.platform().isPresent() && ! change.platform().get().equals(lastVersions.targetPlatform())) return Optional.empty();
- if (change.application().isPresent() && ! change.application().get().equals(lastVersions.targetApplication())) return Optional.empty();
+ if (change.revision().isPresent() && ! change.revision().get().equals(lastVersions.targetRevision())) return Optional.empty();
if (job.id().type().environment().isTest() && job.isNodeAllocationFailure()) return Optional.empty();
Instant firstFailing = job.firstFailing().get().end().get();
@@ -787,10 +820,9 @@ public class DeploymentStatus {
}
private static JobStepStatus ofProductionDeployment(DeclaredZone step, List<StepStatus> dependencies,
- DeploymentStatus status, InstanceName instance, JobType jobType) {
+ DeploymentStatus status, JobStatus job) {
ZoneId zone = ZoneId.from(step.environment(), step.region().get());
- JobStatus job = status.instanceJobs(instance).get(jobType);
- Optional<Deployment> existingDeployment = Optional.ofNullable(status.application().require(instance)
+ Optional<Deployment> existingDeployment = Optional.ofNullable(status.application().require(job.id().application().instance())
.deployments().get(zone));
return new JobStepStatus(StepType.deployment, step, dependencies, job, status) {
@@ -811,21 +843,21 @@ public class DeploymentStatus {
&& ! existingDeployment.map(Deployment::version).equals(change.platform()))
return Optional.empty();
- if ( change.application().isPresent()
- && ! existingDeployment.map(Deployment::applicationVersion).equals(change.application())
+ if ( change.revision().isPresent()
+ && ! existingDeployment.map(Deployment::revision).equals(change.revision())
&& dependent.equals(job())) // Job should (re-)run in this case, but other dependents need not wait.
return Optional.empty();
- Change fullChange = status.application().require(instance).change();
- if (existingDeployment.map(deployment -> ! (change.upgrades(deployment.version()) || change.upgrades(deployment.applicationVersion()))
- && (fullChange.downgrades(deployment.version()) || fullChange.downgrades(deployment.applicationVersion())))
+ Change fullChange = status.application().require(job.id().application().instance()).change();
+ if (existingDeployment.map(deployment -> ! (change.upgrades(deployment.version()) || change.upgrades(deployment.revision()))
+ && (fullChange.downgrades(deployment.version()) || fullChange.downgrades(deployment.revision())))
.orElse(false))
return job.lastCompleted().flatMap(Run::end);
Optional<Instant> end = Optional.empty();
for (Run run : job.runs().descendingMap().values()) {
if (run.versions().targetsMatch(change)) {
- if (run.status() == RunStatus.success) end = run.end();
+ if (run.hasSucceeded()) end = run.end();
}
else if (dependent.equals(job())) // If strict completion, consider only last time this change was deployed.
break;
@@ -836,11 +868,8 @@ public class DeploymentStatus {
}
private static JobStepStatus ofProductionTest(DeclaredTest step, List<StepStatus> dependencies,
- DeploymentStatus status, InstanceName instance,
- JobType testType, JobType prodType) {
- JobStatus job = status.instanceJobs(instance).get(testType);
- JobId prodId = new JobId(status.application().id().instance(instance), prodType);
-
+ DeploymentStatus status, JobStatus job) {
+ JobId prodId = new JobId(job.id().application(), JobType.deploymentTo(job.id().type().zone()));
return new JobStepStatus(StepType.test, step, dependencies, job, status) {
@Override
Optional<Instant> readyAt(Change change, Optional<JobId> dependent) {
@@ -855,7 +884,7 @@ public class DeploymentStatus {
Optional<Instant> deployedAt = status.jobSteps().get(prodId).completedAt(change, Optional.of(prodId));
return (dependent.equals(job()) ? job.lastTriggered().filter(run -> deployedAt.map(at -> ! run.start().isBefore(at)).orElse(false)).stream()
: job.runs().values().stream())
- .filter(run -> run.status() == RunStatus.success)
+ .filter(Run::hasSucceeded)
.filter(run -> run.versions().targetsMatch(change))
.flatMap(run -> run.end().stream()).findFirst();
}
@@ -863,9 +892,7 @@ public class DeploymentStatus {
}
private static JobStepStatus ofTestDeployment(DeclaredZone step, List<StepStatus> dependencies,
- DeploymentStatus status, InstanceName instance,
- JobType jobType, boolean declared) {
- JobStatus job = status.instanceJobs(instance).get(jobType);
+ DeploymentStatus status, JobStatus job, boolean declared) {
return new JobStepStatus(StepType.test, step, dependencies, job, status) {
@Override
Optional<Instant> completedAt(Change change, Optional<JobId> dependent) {
@@ -875,9 +902,9 @@ public class DeploymentStatus {
status.application,
Optional.of(deployment),
status.systemVersion)))
- .orElseGet(() -> (change.platform().isEmpty() || change.platform().get().equals(run.versions().targetPlatform()))
- && (change.application().isEmpty() || change.application().get().equals(run.versions().targetApplication()))))
- .status(RunStatus.success)
+ .orElseGet(() -> (change.platform().isEmpty() || change.platform().get().equals(run.versions().targetPlatform()))
+ && (change.revision().isEmpty() || change.revision().get().equals(run.versions().targetRevision()))))
+ .matching(Run::hasSucceeded)
.asList().stream()
.map(run -> run.end().get())
.max(naturalOrder());
@@ -897,7 +924,7 @@ public class DeploymentStatus {
private final Change change;
public Job(JobType type, Versions versions, Optional<Instant> readyAt, Change change) {
- this.versions = type == systemTest ? versions.withoutSources() : versions;
+ this.versions = type.isSystemTest() ? versions.withoutSources() : versions;
this.readyAt = readyAt;
this.change = change;
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentSteps.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentSteps.java
index 7ab895654f3..44079a90097 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentSteps.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentSteps.java
@@ -7,6 +7,7 @@ import com.yahoo.config.provision.Environment;
import com.yahoo.config.provision.SystemName;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
+import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry;
import com.yahoo.vespa.hosted.controller.application.Deployment;
import java.util.Collection;
@@ -29,16 +30,16 @@ import static java.util.stream.Collectors.collectingAndThen;
public class DeploymentSteps {
private final DeploymentInstanceSpec spec;
- private final Supplier<SystemName> system;
+ private final ZoneRegistry zones;
- public DeploymentSteps(DeploymentInstanceSpec spec, Supplier<SystemName> system) {
+ public DeploymentSteps(DeploymentInstanceSpec spec, ZoneRegistry zones) {
this.spec = Objects.requireNonNull(spec, "spec cannot be null");
- this.system = Objects.requireNonNull(system, "system cannot be null");
+ this.zones = Objects.requireNonNull(zones, "system cannot be null");
}
/** Returns jobs for this, in the order they should run */
public List<JobType> jobs() {
- return Stream.concat(production().isEmpty() ? Stream.of() : Stream.of(JobType.systemTest, JobType.stagingTest),
+ return Stream.concat(production().isEmpty() ? Stream.of() : Stream.of(JobType.systemTest(zones), JobType.stagingTest(zones)),
spec.steps().stream().flatMap(step -> toJobs(step).stream()))
.distinct()
.collect(Collectors.toUnmodifiableList());
@@ -67,7 +68,6 @@ public class DeploymentSteps {
public List<JobType> toJobs(DeploymentSpec.Step step) {
return step.zones().stream()
.map(this::toJob)
- .flatMap(Optional::stream)
.collect(Collectors.toUnmodifiableList());
}
@@ -93,8 +93,13 @@ public class DeploymentSteps {
}
/** Resolve job from deployment zone */
- private Optional<JobType> toJob(DeploymentSpec.DeclaredZone zone) {
- return JobType.from(system.get(), zone.environment(), zone.region().orElse(null));
+ private JobType toJob(DeploymentSpec.DeclaredZone zone) {
+ switch (zone.environment()) {
+ case prod: return JobType.prod(zone.region().get());
+ case test: return JobType.systemTest(zones);
+ case staging: return JobType.stagingTest(zones);
+ default: throw new IllegalArgumentException("region must be one with automated deployments, but got: " + zone.environment());
+ }
}
/** Resolve jobs from steps */
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 7ef8a4b4ae6..be07a2b0cb1 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
@@ -8,6 +8,7 @@ import com.yahoo.config.application.api.DeploymentSpec;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.InstanceName;
import com.yahoo.text.Text;
+import com.yahoo.transaction.Mutex;
import com.yahoo.vespa.curator.Lock;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.ApplicationController;
@@ -17,6 +18,7 @@ import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
+import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
import com.yahoo.vespa.hosted.controller.application.ApplicationList;
import com.yahoo.vespa.hosted.controller.application.Change;
import com.yahoo.vespa.hosted.controller.application.Deployment;
@@ -72,22 +74,7 @@ public class DeploymentTrigger {
}
public DeploymentSteps steps(DeploymentInstanceSpec spec) {
- return new DeploymentSteps(spec, controller::system);
- }
-
- public void notifyOfSubmission(TenantAndApplicationId id, ApplicationVersion version, long projectId) {
- if (applications().getApplication(id).isEmpty()) {
- log.log(Level.WARNING, "Ignoring submission from project '" + projectId +
- "': Unknown application '" + id + "'");
- return;
- }
-
- applications().lockApplicationOrThrow(id, application -> {
- application = application.withProjectId(OptionalLong.of(projectId));
- application = application.withNewSubmission(version);
- applications().store(application);
- });
- triggerNewRevision(id);
+ return new DeploymentSteps(spec, controller.zoneRegistry());
}
/**
@@ -100,11 +87,11 @@ public class DeploymentTrigger {
DeploymentStatus status = jobs.deploymentStatus(application.get());
for (InstanceName instanceName : application.get().deploymentSpec().instanceNames()) {
Change outstanding = outstandingChange(status, instanceName);
- if ( outstanding.hasTargets()
+ if (outstanding.hasTargets()
&& status.instanceSteps().get(instanceName)
.readyAt(outstanding)
.map(readyAt -> ! readyAt.isAfter(clock.instant())).orElse(false)
- && acceptNewApplicationVersion(status, instanceName, outstanding.application().get())) {
+ && acceptNewRevision(status, instanceName, outstanding.revision().get())) {
application = application.with(instanceName,
instance -> withRemainingChange(instance, outstanding.onTopOf(instance.change()), status));
}
@@ -116,7 +103,9 @@ public class DeploymentTrigger {
/** Returns any outstanding change for the given instance, coupled with any necessary platform upgrade. */
private Change outstandingChange(DeploymentStatus status, InstanceName instance) {
Change outstanding = status.outstandingChange(instance);
- Optional<Version> compileVersion = outstanding.application().flatMap(ApplicationVersion::compileVersion);
+ Optional<Version> compileVersion = outstanding.revision()
+ .map(status.application().revisions()::get)
+ .flatMap(ApplicationVersion::compileVersion);
// If the outstanding revision requires a certain platform for compatibility, add that here.
VersionCompatibility compatibility = applications().versionCompatibility(status.application().id().instance(instance));
@@ -246,7 +235,7 @@ public class DeploymentTrigger {
DeploymentStatus status = jobs.deploymentStatus(application);
Change change = instance.change();
- if ( ! upgradeRevision && change.application().isPresent()) change = change.withoutApplication();
+ if ( ! upgradeRevision && change.revision().isPresent()) change = change.withoutApplication();
if ( ! upgradePlatform && change.platform().isPresent()) change = change.withoutPlatform();
Versions versions = Versions.from(change, application, status.deploymentFor(job), controller.readSystemVersion());
DeploymentStatus.Job toTrigger = new DeploymentStatus.Job(job.type(), versions, Optional.of(controller.clock().instant()), instance.change());
@@ -267,24 +256,23 @@ public class DeploymentTrigger {
private List<JobId> forceTriggerManualJob(JobId job, String reason) {
Run last = jobs.last(job).orElseThrow(() -> new IllegalArgumentException(job + " has never been run"));
Versions target = new Versions(controller.readSystemVersion(),
- last.versions().targetApplication(),
+ last.versions().targetRevision(),
Optional.of(last.versions().targetPlatform()),
- Optional.of(last.versions().targetApplication()));
+ Optional.of(last.versions().targetRevision()));
jobs.start(job.application(), job.type(), target, true, Optional.of(reason));
return List.of(job);
}
/** Retrigger job. If the job is already running, it will be canceled, and retrigger enqueued. */
public Optional<JobId> reTriggerOrAddToQueue(DeploymentId deployment, String reason) {
- JobType jobType = JobType.from(controller.system(), deployment.zoneId())
- .orElseThrow(() -> new IllegalArgumentException(Text.format("No job to trigger for (system/zone): %s/%s", controller.system().value(), deployment.zoneId().value())));
+ JobType jobType = JobType.deploymentTo(deployment.zoneId());
Optional<Run> existingRun = controller.jobController().active(deployment.applicationId()).stream()
.filter(run -> run.id().type().equals(jobType))
.findFirst();
if (existingRun.isPresent()) {
Run run = existingRun.get();
- try (Lock lock = controller.curator().lockDeploymentRetriggerQueue()) {
+ try (Mutex lock = controller.curator().lockDeploymentRetriggerQueue()) {
List<RetriggerEntry> retriggerEntries = controller.curator().readRetriggerEntries();
List<RetriggerEntry> newList = new ArrayList<>(retriggerEntries);
RetriggerEntry requiredEntry = new RetriggerEntry(new JobId(deployment.applicationId(), jobType), run.id().number() + 1);
@@ -366,7 +354,7 @@ public class DeploymentTrigger {
/** Returns the set of all jobs which have changes to propagate from the upstream steps. */
private List<Job> computeReadyJobs() {
return jobs.deploymentStatuses(ApplicationList.from(applications().readable())
- .withProjectId() // Need to keep this, as we have applications with deployment spec that shouldn't be orchestrated.
+ .withProjectId() // Need to keep this, as we have applications with deployment spec that shouldn't be orchestrated. // Maybe not any longer?
.withDeploymentSpec())
.withChanges()
.asList().stream()
@@ -397,7 +385,7 @@ public class DeploymentTrigger {
/** Returns whether the application is healthy in all other production zones. */
private boolean isUnhealthyInAnotherZone(Application application, JobId job) {
for (Deployment deployment : application.require(job.application().instance()).productionDeployments().values()) {
- if ( ! deployment.zone().equals(job.type().zone(controller.system()))
+ if ( ! deployment.zone().equals(job.type().zone())
&& ! controller.applications().isHealthy(new DeploymentId(job.application(), deployment.zone())))
return true;
}
@@ -426,9 +414,7 @@ public class DeploymentTrigger {
boolean blocked = status.jobs().get(job).get().isRunning();
if ( ! job.type().isTest()) {
- Optional<JobStatus> productionTest = JobType.testFrom(controller.system(), job.type().zone(controller.system()).region())
- .map(type -> new JobId(job.application(), type))
- .flatMap(status.jobs()::get);
+ Optional<JobStatus> productionTest = status.jobs().get(new JobId(job.application(), JobType.productionTestOf(job.type().zone())));
if (productionTest.isPresent()) {
abortIfOutdated(status, jobs, productionTest.get().id());
// Production deployments are also blocked by their declared tests, if the next versions to run
@@ -445,16 +431,16 @@ public class DeploymentTrigger {
// ---------- Change management o_O ----------
- private boolean acceptNewApplicationVersion(DeploymentStatus status, InstanceName instance, ApplicationVersion version) {
+ private boolean acceptNewRevision(DeploymentStatus status, InstanceName instance, RevisionId revision) {
if (status.application().deploymentSpec().instance(instance).isEmpty()) return false; // Unknown instance.
- boolean isChangingRevision = status.application().require(instance).change().application().isPresent();
+ boolean isChangingRevision = status.application().require(instance).change().revision().isPresent();
DeploymentInstanceSpec spec = status.application().deploymentSpec().requireInstance(instance);
- Predicate<ApplicationVersion> versionFilter = spec.revisionTarget() == DeploymentSpec.RevisionTarget.next
- ? failing -> status.application().require(instance).change().application().get().compareTo(failing) == 0
- : failing -> version.compareTo(failing) > 0;
+ Predicate<RevisionId> revisionFilter = spec.revisionTarget() == DeploymentSpec.RevisionTarget.next
+ ? failing -> status.application().require(instance).change().revision().get().compareTo(failing) == 0
+ : failing -> revision.compareTo(failing) > 0;
switch (spec.revisionChange()) {
case whenClear: return ! isChangingRevision;
- case whenFailing: return ! isChangingRevision || status.hasFailures(versionFilter);
+ case whenFailing: return ! isChangingRevision || status.hasFailures(revisionFilter);
case always: return true;
default: throw new IllegalStateException("Unknown revision upgrade policy");
}
@@ -464,18 +450,15 @@ public class DeploymentTrigger {
Change remaining = change;
if (status.hasCompleted(instance.name(), change.withoutApplication()))
remaining = remaining.withoutPlatform();
- if (status.hasCompleted(instance.name(), change.withoutPlatform())) {
+ if (status.hasCompleted(instance.name(), change.withoutPlatform()))
remaining = remaining.withoutApplication();
- if (change.application().isPresent())
- instance = instance.withLatestDeployed(change.application().get());
- }
return instance.withChange(remaining);
}
// ---------- Version and job helpers ----------
private Job deploymentJob(Instance instance, Versions versions, JobType jobType, JobStatus jobStatus, Instant availableSince) {
- return new Job(instance, versions, jobType, availableSince, jobStatus.isNodeAllocationFailure(), instance.change().application().isPresent());
+ return new Job(instance, versions, jobType, availableSince, jobStatus.isNodeAllocationFailure(), instance.change().revision().isPresent());
}
// ---------- Data containers ----------
@@ -510,7 +493,7 @@ public class DeploymentTrigger {
public String toString() {
return jobType + " for " + instanceId +
" on (" + versions.targetPlatform() + versions.sourcePlatform().map(version -> " <-- " + version).orElse("") +
- ", " + versions.targetApplication().id() + versions.sourceApplication().map(version -> " <-- " + version.id()).orElse("") +
+ ", " + versions.targetRevision() + versions.sourceRevision().map(version -> " <-- " + version).orElse("") +
"), ready since " + availableSince;
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java
index beef090d214..52e5431b552 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java
@@ -1,17 +1,14 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.deployment;
+import ai.vespa.http.DomainName;
import com.yahoo.component.Version;
-import com.yahoo.config.application.api.DeploymentInstanceSpec;
import com.yahoo.config.application.api.DeploymentSpec;
import com.yahoo.config.application.api.Notifications;
import com.yahoo.config.application.api.Notifications.When;
import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.AthenzDomain;
-import com.yahoo.config.provision.AthenzService;
import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.NodeResources;
import com.yahoo.config.provision.SystemName;
import com.yahoo.config.provision.zone.RoutingMethod;
import com.yahoo.config.provision.zone.ZoneId;
@@ -21,10 +18,6 @@ import com.yahoo.security.KeyUtils;
import com.yahoo.security.SignatureAlgorithm;
import com.yahoo.security.X509CertificateBuilder;
import com.yahoo.security.X509CertificateUtils;
-import com.yahoo.text.Text;
-import com.yahoo.vespa.flags.FetchVector;
-import com.yahoo.vespa.flags.FlagSource;
-import com.yahoo.vespa.flags.Flags;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.Instance;
@@ -36,11 +29,10 @@ import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeFilter;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.PrepareResponse;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.ServiceConvergence;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
+import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterId;
import com.yahoo.vespa.hosted.controller.api.integration.organization.DeploymentFailureMails;
import com.yahoo.vespa.hosted.controller.api.integration.organization.Mail;
import com.yahoo.vespa.hosted.controller.application.ActivateResult;
@@ -48,7 +40,7 @@ import com.yahoo.vespa.hosted.controller.application.Deployment;
import com.yahoo.vespa.hosted.controller.application.Endpoint;
import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
-import com.yahoo.vespa.hosted.controller.config.ControllerConfig;
+import com.yahoo.vespa.hosted.controller.application.pkg.TestPackage;
import com.yahoo.vespa.hosted.controller.maintenance.JobRunner;
import com.yahoo.vespa.hosted.controller.notification.Notification;
import com.yahoo.vespa.hosted.controller.notification.NotificationSource;
@@ -89,6 +81,7 @@ import static com.yahoo.vespa.hosted.controller.api.integration.configserver.Nod
import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.deploymentFailed;
import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.error;
import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.installationFailed;
+import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.noTests;
import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.nodeAllocationFailure;
import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.reset;
import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.running;
@@ -125,13 +118,6 @@ public class InternalStepRunner implements StepRunner {
private static final Logger logger = Logger.getLogger(InternalStepRunner.class.getName());
- static final NodeResources DEFAULT_TESTER_RESOURCES =
- new NodeResources(1, 4, 50, 0.3, NodeResources.DiskSpeed.any);
- // Must match exactly the advertised resources of an AWS instance type. Also consider that the container
- // will have ~1.8 GB less memory than equivalent resources in AWS (VESPA-16259).
- static final NodeResources DEFAULT_TESTER_RESOURCES_AWS =
- new NodeResources(2, 8, 50, 0.3, NodeResources.DiskSpeed.any);
-
private final Controller controller;
private final TestConfigSerializer testConfigSerializer;
private final DeploymentFailureMails mails;
@@ -184,15 +170,15 @@ public class InternalStepRunner implements StepRunner {
Versions versions = controller.jobController().run(id).get().versions();
logger.log("Deploying platform version " +
versions.sourcePlatform().orElse(versions.targetPlatform()) +
- " and application version " +
- versions.sourceApplication().orElse(versions.targetApplication()).id() + " ...");
+ " and application " +
+ versions.sourceRevision().orElse(versions.targetRevision()) + " ...");
return deployReal(id, true, logger);
}
private Optional<RunStatus> deployReal(RunId id, DualLogger logger) {
Versions versions = controller.jobController().run(id).get().versions();
logger.log("Deploying platform version " + versions.targetPlatform() +
- " and application version " + versions.targetApplication().id() + " ...");
+ " and application " + versions.targetRevision() + " ...");
return deployReal(id, false, logger);
}
@@ -218,7 +204,7 @@ public class InternalStepRunner implements StepRunner {
logger.log("Deploying the tester container on platform " + platform + " ...");
return deploy(() -> controller.applications().deployTester(id.tester(),
testerPackage(id),
- id.type().zone(controller.system()),
+ id.type().zone(),
platform),
controller.jobController().run(id).get()
.stepInfo(deployTester).get()
@@ -315,19 +301,19 @@ public class InternalStepRunner implements StepRunner {
Version platform = setTheStage ? versions.sourcePlatform().orElse(versions.targetPlatform()) : versions.targetPlatform();
Run run = controller.jobController().run(id).get();
- Optional<ServiceConvergence> services = controller.serviceRegistry().configServer().serviceConvergence(new DeploymentId(id.application(), id.type().zone(controller.system())),
+ Optional<ServiceConvergence> services = controller.serviceRegistry().configServer().serviceConvergence(new DeploymentId(id.application(), id.type().zone()),
Optional.of(platform));
if (services.isEmpty()) {
logger.log("Config status not currently available -- will retry.");
return Optional.empty();
}
- List<Node> nodes = controller.serviceRegistry().configServer().nodeRepository().list(id.type().zone(controller.system()),
+ List<Node> nodes = controller.serviceRegistry().configServer().nodeRepository().list(id.type().zone(),
NodeFilter.all()
.applications(id.application())
.states(active));
Set<HostName> parentHostnames = nodes.stream().map(node -> node.parentHostname().get()).collect(toSet());
- List<Node> parents = controller.serviceRegistry().configServer().nodeRepository().list(id.type().zone(controller.system()),
+ List<Node> parents = controller.serviceRegistry().configServer().nodeRepository().list(id.type().zone(),
NodeFilter.all()
.hostnames(parentHostnames));
boolean firstTick = run.convergenceSummary().isEmpty();
@@ -358,8 +344,8 @@ public class InternalStepRunner implements StepRunner {
}
if (summary.converged()) {
controller.jobController().locked(id, lockedRun -> lockedRun.withSummary(null));
- if (endpointsAvailable(id.application(), id.type().zone(controller.system()), logger)) {
- if (containersAreUp(id.application(), id.type().zone(controller.system()), logger)) {
+ if (endpointsAvailable(id.application(), id.type().zone(), logger)) {
+ if (containersAreUp(id.application(), id.type().zone(), logger)) {
logger.log("Installation succeeded!");
return Optional.of(running);
}
@@ -441,7 +427,7 @@ public class InternalStepRunner implements StepRunner {
private Optional<RunStatus> installTester(RunId id, DualLogger logger) {
Run run = controller.jobController().run(id).get();
Version platform = testerPlatformVersion(id);
- ZoneId zone = id.type().zone(controller.system());
+ ZoneId zone = id.type().zone();
ApplicationId testerId = id.tester().id();
Optional<ServiceConvergence> services = controller.serviceRegistry().configServer().serviceConvergence(new DeploymentId(testerId, zone),
@@ -512,7 +498,7 @@ public class InternalStepRunner implements StepRunner {
return false;
}
for (var endpoint : endpoints.get(zone)) {
- HostName endpointName = HostName.from(endpoint.dnsName());
+ DomainName endpointName = DomainName.of(endpoint.dnsName());
var ipAddress = controller.jobController().cloud().resolveHostName(endpointName);
if (ipAddress.isEmpty()) {
logger.log(INFO, "DNS lookup yielded no IP address for '" + endpointName + "'.");
@@ -533,7 +519,7 @@ public class InternalStepRunner implements StepRunner {
var loadBalancerAddress = controller.jobController().cloud().resolveHostName(policy.canonicalName());
if ( ! loadBalancerAddress.equals(ipAddress)) {
logger.log(INFO, "IP address of CNAME '" + endpointName + "' (" + ipAddress.get() + ") and load balancer '" +
- policy.canonicalName() + "' (" + loadBalancerAddress.orElse("empty") + ") are not equal");
+ policy.canonicalName() + "' (" + loadBalancerAddress.orElse(null) + ") are not equal");
return false;
}
}
@@ -610,7 +596,7 @@ public class InternalStepRunner implements StepRunner {
.productionDeployments().keySet().stream()
.map(zone -> new DeploymentId(id.application(), zone))
.collect(Collectors.toSet());
- ZoneId zoneId = id.type().zone(controller.system());
+ ZoneId zoneId = id.type().zone();
deployments.add(new DeploymentId(id.application(), zoneId));
logger.log("Attempting to find endpoints ...");
@@ -637,6 +623,7 @@ public class InternalStepRunner implements StepRunner {
return Optional.of(running);
}
+ @SuppressWarnings("fallthrough")
private Optional<RunStatus> endTests(RunId id, boolean isSetup, DualLogger logger) {
Optional<Deployment> deployment = deployment(id.application(), id.type());
if (deployment.isEmpty()) {
@@ -678,12 +665,14 @@ public class InternalStepRunner implements StepRunner {
controller.jobController().updateTestReport(id);
return Optional.of(error);
case NO_TESTS:
- TesterCloud.Suite suite = TesterCloud.Suite.of(id.type(), isSetup);
- logger.log(INFO, "No tests were found in the test package, for test suite '" + suite + "'");
- logger.log(INFO, "The test package must either contain basic HTTP tests under 'tests/<suite-name>/', " +
- "or a Java test bundle under 'components/' with at least one test with the annotation " +
- "for this suite. See docs.vespa.ai/en/testing.html for details.");
- return Optional.of(allowNoTests(id.application()) ? running : testFailure);
+ if ( ! isSetup) { // TODO: consider changing this Later™
+ TesterCloud.Suite suite = TesterCloud.Suite.of(id.type(), isSetup);
+ logger.log(INFO, "No tests were found in the test package, for test suite '" + suite + "'");
+ logger.log(INFO, "The test package should either contain basic HTTP tests under 'tests/<suite-name>/', " +
+ "or a Java test bundle under 'components/' with at least one test with the annotation " +
+ "for this suite. See docs.vespa.ai/en/testing.html for details.");
+ return Optional.of(noTests);
+ }
case SUCCESS:
logger.log("Tests completed successfully.");
controller.jobController().updateTestReport(id);
@@ -693,12 +682,6 @@ public class InternalStepRunner implements StepRunner {
}
}
- private boolean allowNoTests(ApplicationId appId) {
- return Flags.ALLOW_NO_TESTS.bindTo(controller.flagSource())
- .with(FetchVector.Dimension.TENANT_ID, appId.tenant().value())
- .value();
- }
-
private Optional<RunStatus> copyVespaLogs(RunId id, DualLogger logger) {
if (deployment(id.application(), id.type()).isPresent())
try {
@@ -726,8 +709,8 @@ public class InternalStepRunner implements StepRunner {
private Optional<RunStatus> deactivateReal(RunId id, DualLogger logger) {
try {
- logger.log("Deactivating deployment of " + id.application() + " in " + id.type().zone(controller.system()) + " ...");
- controller.applications().deactivate(id.application(), id.type().zone(controller.system()));
+ logger.log("Deactivating deployment of " + id.application() + " in " + id.type().zone() + " ...");
+ controller.applications().deactivate(id.application(), id.type().zone());
return Optional.of(running);
}
catch (RuntimeException e) {
@@ -741,7 +724,7 @@ public class InternalStepRunner implements StepRunner {
private Optional<RunStatus> deactivateTester(RunId id, DualLogger logger) {
try {
- logger.log("Deactivating tester of " + id.application() + " in " + id.type().zone(controller.system()) + " ...");
+ logger.log("Deactivating tester of " + id.application() + " in " + id.type().zone() + " ...");
controller.jobController().deactivateTester(id.tester(), id.type());
return Optional.of(running);
}
@@ -786,14 +769,14 @@ public class InternalStepRunner implements StepRunner {
Application application = controller.applications().requireApplication(TenantAndApplicationId.from(run.id().application()));
Notifications notifications = application.deploymentSpec().requireInstance(run.id().application().instance()).notifications();
- boolean newCommit = application.require(run.id().application().instance()).change().application()
- .map(run.versions().targetApplication()::equals)
+ boolean newCommit = application.require(run.id().application().instance()).change().revision()
+ .map(run.versions().targetRevision()::equals)
.orElse(false);
When when = newCommit ? failingCommit : failing;
List<String> recipients = new ArrayList<>(notifications.emailAddressesFor(when));
if (notifications.emailRolesFor(when).contains(author))
- run.versions().targetApplication().authorEmail().ifPresent(recipients::add);
+ application.revisions().get(run.versions().targetRevision()).authorEmail().ifPresent(recipients::add);
if (recipients.isEmpty())
return;
@@ -834,6 +817,10 @@ public class InternalStepRunner implements StepRunner {
case testFailure:
updater.accept("one or more verification tests against the deployment failed. Please review test output in the deployment job log.");
return;
+ case noTests:
+ controller.notificationsDb().setNotification(source, Notification.Type.deployment, Notification.Level.warning,
+ "no tests were found for this job type. Please review test output in the deployment job log.");
+ return;
case error:
case endpointCertificateTimeout:
break;
@@ -848,6 +835,7 @@ public class InternalStepRunner implements StepRunner {
switch (run.status()) {
case running:
case aborted:
+ case noTests:
case success:
return Optional.empty();
case nodeAllocationFailure:
@@ -860,16 +848,16 @@ public class InternalStepRunner implements StepRunner {
return Optional.of(mails.testFailure(run.id(), recipients));
case error:
case endpointCertificateTimeout:
- return Optional.of(mails.systemError(run.id(), recipients));
+ break;
default:
logger.log(WARNING, "Don't know what mail to send for run status '" + run.status() + "'");
- return Optional.of(mails.systemError(run.id(), recipients));
}
+ return Optional.of(mails.systemError(run.id(), recipients));
}
/** Returns the deployment of the real application in the zone of the given job, if it exists. */
private Optional<Deployment> deployment(ApplicationId id, JobType type) {
- return Optional.ofNullable(application(id).deployments().get(type.zone(controller.system())));
+ return Optional.ofNullable(application(id).deployments().get(type.zone()));
}
/** Returns the real application with the given id. */
@@ -904,141 +892,29 @@ public class InternalStepRunner implements StepRunner {
/** Returns the application package for the tester application, assembled from a generated config, fat-jar and services.xml. */
private ApplicationPackage testerPackage(RunId id) {
- ApplicationVersion version = controller.jobController().run(id).get().versions().targetApplication();
+ RevisionId revision = controller.jobController().run(id).get().versions().targetRevision();
DeploymentSpec spec = controller.applications().requireApplication(TenantAndApplicationId.from(id.application())).deploymentSpec();
-
- ZoneId zone = id.type().zone(controller.system());
+ byte[] testZip = controller.applications().applicationStore().getTester(id.application().tenant(),
+ id.application().application(), revision);
boolean useTesterCertificate = useTesterCertificate(id);
- byte[] servicesXml = servicesXml( ! controller.system().isPublic(),
- useTesterCertificate,
- testerResourcesFor(zone, spec.requireInstance(id.application().instance())),
- controller.controllerConfig().steprunner().testerapp());
- byte[] testPackage = controller.applications().applicationStore().getTester(id.application().tenant(), id.application().application(), version);
- byte[] deploymentXml = deploymentXml(id.tester(),
- spec.athenzDomain(),
- spec.requireInstance(id.application().instance()).athenzService(zone.environment(), zone.region()));
-
- try (ZipBuilder zipBuilder = new ZipBuilder(testPackage.length + servicesXml.length + deploymentXml.length + 1000)) {
- // Copy contents of submitted application-test.zip, and ensure required directories exist within the zip.
- zipBuilder.add(testPackage);
- zipBuilder.add("artifacts/.ignore-" + UUID.randomUUID(), new byte[0]);
- zipBuilder.add("tests/.ignore-" + UUID.randomUUID(), new byte[0]);
-
- zipBuilder.add("services.xml", servicesXml);
- zipBuilder.add("deployment.xml", deploymentXml);
- if (useTesterCertificate)
- appendAndStoreCertificate(zipBuilder, id);
-
- zipBuilder.close();
- return new ApplicationPackage(zipBuilder.toByteArray());
- }
- }
+ TestPackage testPackage = new TestPackage(testZip,
+ controller.system().isPublic(),
+ id,
+ controller.controllerConfig().steprunner().testerapp(),
+ spec,
+ useTesterCertificate ? controller.clock().instant() : null,
+ timeouts.testerCertificate());
+ if (useTesterCertificate) controller.jobController().storeTesterCertificate(id, testPackage.certificate());
- private void appendAndStoreCertificate(ZipBuilder zipBuilder, RunId id) {
- KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.RSA, 2048);
- X500Principal subject = new X500Principal("CN=" + id.tester().id().toFullString() + "." + id.type() + "." + id.number());
- X509Certificate certificate = X509CertificateBuilder.fromKeypair(keyPair,
- subject,
- controller.clock().instant(),
- controller.clock().instant().plus(timeouts.testerCertificate()),
- SignatureAlgorithm.SHA512_WITH_RSA,
- BigInteger.valueOf(1))
- .build();
- controller.jobController().storeTesterCertificate(id, certificate);
- zipBuilder.add("artifacts/key", KeyUtils.toPem(keyPair.getPrivate()).getBytes(UTF_8));
- zipBuilder.add("artifacts/cert", X509CertificateUtils.toPem(certificate).getBytes(UTF_8));
+ return testPackage.asApplicationPackage();
}
private DeploymentId getTesterDeploymentId(RunId runId) {
- ZoneId zoneId = runId.type().zone(controller.system());
+ ZoneId zoneId = runId.type().zone();
return new DeploymentId(runId.tester().id(), zoneId);
}
- static NodeResources testerResourcesFor(ZoneId zone, DeploymentInstanceSpec spec) {
- NodeResources nodeResources = spec.steps().stream()
- .filter(step -> step.concerns(zone.environment()))
- .findFirst()
- .flatMap(step -> step.zones().get(0).testerFlavor())
- .map(NodeResources::fromLegacyName)
- .orElse(zone.region().value().contains("aws-") ?
- DEFAULT_TESTER_RESOURCES_AWS : DEFAULT_TESTER_RESOURCES);
- return nodeResources.with(NodeResources.DiskSpeed.any);
- }
-
- /** Returns the generated services.xml content for the tester application. */
- static byte[] servicesXml(boolean systemUsesAthenz, boolean useTesterCertificate,
- NodeResources resources, ControllerConfig.Steprunner.Testerapp config) {
- int jdiscMemoryGb = 2; // 2Gb memory for tester application (excessive?).
- int jdiscMemoryPct = (int) Math.ceil(100 * jdiscMemoryGb / resources.memoryGb());
-
- // Of the remaining memory, split 50/50 between Surefire running the tests and the rest
- int testMemoryMb = (int) (1024 * (resources.memoryGb() - jdiscMemoryGb) / 2);
-
- String resourceString = Text.format(
- "<resources vcpu=\"%.2f\" memory=\"%.2fGb\" disk=\"%.2fGb\" disk-speed=\"%s\" storage-type=\"%s\"/>",
- resources.vcpu(), resources.memoryGb(), resources.diskGb(), resources.diskSpeed().name(), resources.storageType().name());
-
- String runtimeProviderClass = config.runtimeProviderClass();
- String tenantCdBundle = config.tenantCdBundle();
-
- String servicesXml =
- "<?xml version='1.0' encoding='UTF-8'?>\n" +
- "<services xmlns:deploy='vespa' version='1.0'>\n" +
- " <container version='1.0' id='tester'>\n" +
- "\n" +
- " <component id=\"com.yahoo.vespa.hosted.testrunner.TestRunner\" bundle=\"vespa-testrunner-components\">\n" +
- " <config name=\"com.yahoo.vespa.hosted.testrunner.test-runner\">\n" +
- " <artifactsPath>artifacts</artifactsPath>\n" +
- " <surefireMemoryMb>" + testMemoryMb + "</surefireMemoryMb>\n" +
- " <useAthenzCredentials>" + systemUsesAthenz + "</useAthenzCredentials>\n" +
- " <useTesterCertificate>" + useTesterCertificate + "</useTesterCertificate>\n" +
- " </config>\n" +
- " </component>\n" +
- "\n" +
- " <handler id=\"com.yahoo.vespa.testrunner.TestRunnerHandler\" bundle=\"vespa-osgi-testrunner\">\n" +
- " <binding>http://*/tester/v1/*</binding>\n" +
- " </handler>\n" +
- "\n" +
- " <component id=\"" + runtimeProviderClass + "\" bundle=\"" + tenantCdBundle + "\" />\n" +
- "\n" +
- " <component id=\"com.yahoo.vespa.testrunner.JunitRunner\" bundle=\"vespa-osgi-testrunner\">\n" +
- " <config name=\"com.yahoo.vespa.testrunner.junit-test-runner\">\n" +
- " <artifactsPath>artifacts</artifactsPath>\n" +
- " <useAthenzCredentials>" + systemUsesAthenz + "</useAthenzCredentials>\n" +
- " </config>\n" +
- " </component>\n" +
- "\n" +
- " <component id=\"com.yahoo.vespa.testrunner.VespaCliTestRunner\" bundle=\"vespa-osgi-testrunner\">\n" +
- " <config name=\"com.yahoo.vespa.testrunner.vespa-cli-test-runner\">\n" +
- " <artifactsPath>artifacts</artifactsPath>\n" +
- " <testsPath>tests</testsPath>\n" +
- " <useAthenzCredentials>" + systemUsesAthenz + "</useAthenzCredentials>\n" +
- " </config>\n" +
- " </component>\n" +
- "\n" +
- " <nodes count=\"1\">\n" +
- " <jvm allocated-memory=\"" + jdiscMemoryPct + "%\"/>\n" +
- " " + resourceString + "\n" +
- " </nodes>\n" +
- " </container>\n" +
- "</services>\n";
-
- return servicesXml.getBytes(UTF_8);
- }
-
- /** Returns a dummy deployment xml which sets up the service identity for the tester, if present. */
- private static byte[] deploymentXml(TesterId id, Optional<AthenzDomain> athenzDomain, Optional<AthenzService> athenzService) {
- String deploymentSpec =
- "<?xml version='1.0' encoding='UTF-8'?>\n" +
- "<deployment version=\"1.0\" " +
- athenzDomain.map(domain -> "athenz-domain=\"" + domain.value() + "\" ").orElse("") +
- athenzService.map(service -> "athenz-service=\"" + service.value() + "\" ").orElse("") + ">" +
- " <instance id=\"" + id.id().instance().value() + "\" />" +
- "</deployment>";
- return deploymentSpec.getBytes(UTF_8);
- }
-
/** Logger which logs to a {@link JobController}, as well as to the parent class' {@link Logger}. */
private class DualLogger {
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java
index b9f09cc31ea..1d56e2db08b 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java
@@ -6,15 +6,18 @@ import com.yahoo.component.Version;
import com.yahoo.component.VersionCompatibility;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.curator.Lock;
+import com.yahoo.transaction.Mutex;
import com.yahoo.vespa.hosted.controller.Application;
+import com.yahoo.vespa.hosted.controller.ApplicationController;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.Instance;
+import com.yahoo.vespa.hosted.controller.LockedApplication;
import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
import com.yahoo.vespa.hosted.controller.api.integration.LogEntry;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
+import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.SourceRevision;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.TestReport;
@@ -25,9 +28,13 @@ import com.yahoo.vespa.hosted.controller.application.Deployment;
import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackageDiff;
+import com.yahoo.vespa.hosted.controller.application.pkg.TestPackage;
+import com.yahoo.vespa.hosted.controller.application.pkg.TestPackage.TestSummary;
+import com.yahoo.vespa.hosted.controller.notification.Notification;
+import com.yahoo.vespa.hosted.controller.notification.Notification.Type;
+import com.yahoo.vespa.hosted.controller.notification.NotificationSource;
import com.yahoo.vespa.hosted.controller.persistence.BufferedLogStore;
import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
-import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
import java.security.cert.X509Certificate;
@@ -37,19 +44,21 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
-import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Optional;
+import java.util.OptionalLong;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
+import java.util.function.Predicate;
import java.util.function.UnaryOperator;
import java.util.logging.Level;
+import java.util.logging.Logger;
import java.util.stream.Stream;
import static com.yahoo.collections.Iterables.reversed;
@@ -63,10 +72,11 @@ import static com.yahoo.vespa.hosted.controller.deployment.Step.endStagingSetup;
import static com.yahoo.vespa.hosted.controller.deployment.Step.endTests;
import static com.yahoo.vespa.hosted.controller.deployment.Step.report;
import static java.time.temporal.ChronoUnit.SECONDS;
+import static java.util.Comparator.naturalOrder;
import static java.util.function.Predicate.not;
import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
import static java.util.stream.Collectors.toList;
-import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toUnmodifiableList;
/**
@@ -86,6 +96,8 @@ public class JobController {
public static final Duration maxHistoryAge = Duration.ofDays(60);
+ private static final Logger log = Logger.getLogger(JobController.class.getName());
+
private final int historyLength;
private final Controller controller;
private final CuratorDb curator;
@@ -125,7 +137,7 @@ public class JobController {
/** Returns the logged entries for the given run, which are after the given id threshold. */
public Optional<RunLog> details(RunId id, long after) {
- try (Lock __ = curator.lock(id.application(), id.type())) {
+ try (Mutex __ = curator.lock(id.application(), id.type())) {
Run run = runs(id.application(), id.type()).get(id);
if (run == null)
return Optional.empty();
@@ -162,7 +174,7 @@ public class JobController {
if ( ! run.hasStep(copyVespaLogs))
return run;
- ZoneId zone = id.type().zone(controller.system());
+ ZoneId zone = id.type().zone();
Optional<Deployment> deployment = Optional.ofNullable(controller.applications().requireInstance(id.application())
.deployments().get(zone));
if (deployment.isEmpty() || deployment.get().at().isBefore(run.start()))
@@ -190,7 +202,7 @@ public class JobController {
if (step.isEmpty())
return run;
- List<LogEntry> entries = cloud.getLog(new DeploymentId(id.tester().id(), id.type().zone(controller.system())),
+ List<LogEntry> entries = cloud.getLog(new DeploymentId(id.tester().id(), id.type().zone()),
run.lastTestLogEntry());
if (entries.isEmpty())
return run;
@@ -202,7 +214,7 @@ public class JobController {
public void updateTestReport(RunId id) {
locked(id, run -> {
- Optional<TestReport> report = cloud.getTestReport(new DeploymentId(id.tester().id(), id.type().zone(controller.system())));
+ Optional<TestReport> report = cloud.getTestReport(new DeploymentId(id.tester().id(), id.type().zone()));
if (report.isEmpty()) {
return run;
}
@@ -229,8 +241,8 @@ public class JobController {
}
/** Returns all job types which have been run for the given application. */
- public List<JobType> jobs(ApplicationId id) {
- return JobType.allIn(controller.system()).stream()
+ private List<JobType> jobs(ApplicationId id) {
+ return JobType.allIn(controller.zoneRegistry()).stream()
.filter(type -> last(id, type).isPresent())
.collect(toUnmodifiableList());
}
@@ -248,6 +260,13 @@ public class JobController {
.collect(toUnmodifiableList());
}
+ /** Returns when given deployment last started deploying, falling back to time of deployment if it cannot be determined from job runs */
+ public Instant lastDeploymentStart(ApplicationId instanceId, Deployment deployment) {
+ return jobStarts(new JobId(instanceId, JobType.deploymentTo(deployment.zone()))).stream()
+ .findFirst()
+ .orElseGet(deployment::at);
+ }
+
/** Returns an immutable map of all known runs for the given application and job type. */
public NavigableMap<RunId, Run> runs(ApplicationId id, JobType type) {
ImmutableSortedMap.Builder<RunId, Run> runs = ImmutableSortedMap.orderedBy(Comparator.comparing(RunId::number));
@@ -309,20 +328,20 @@ public class JobController {
/** Returns a list of all active runs for the given application. */
public List<Run> active(TenantAndApplicationId id) {
return controller.applications().requireApplication(id).instances().keySet().stream()
- .flatMap(name -> Stream.of(JobType.values())
- .map(type -> last(id.instance(name), type))
- .flatMap(Optional::stream)
- .filter(run -> !run.hasEnded()))
+ .flatMap(name -> JobType.allIn(controller.zoneRegistry()).stream()
+ .map(type -> last(id.instance(name), type))
+ .flatMap(Optional::stream)
+ .filter(run -> ! run.hasEnded()))
.collect(toUnmodifiableList());
}
/** Returns a list of all active runs for the given instance. */
public List<Run> active(ApplicationId id) {
- return Stream.of(JobType.values())
- .map(type -> last(id, type))
- .flatMap(Optional::stream)
- .filter(run -> !run.hasEnded())
- .collect(toUnmodifiableList());
+ return JobType.allIn(controller.zoneRegistry()).stream()
+ .map(type -> last(id, type))
+ .flatMap(Optional::stream)
+ .filter(run -> !run.hasEnded())
+ .collect(toUnmodifiableList());
}
/** Returns the job status of the given job, possibly empty. */
@@ -337,12 +356,8 @@ public class JobController {
private DeploymentStatus deploymentStatus(Application application, Version systemVersion) {
return new DeploymentStatus(application,
- DeploymentStatus.jobsFor(application, controller.system()).stream()
- .collect(toMap(job -> job,
- job -> jobStatus(job),
- (j1, j2) -> { throw new IllegalArgumentException("Duplicate key " + j1.id()); },
- LinkedHashMap::new)),
- controller.system(),
+ this::jobStatus,
+ controller.zoneRegistry(),
systemVersion,
instance -> controller.applications().versionCompatibility(application.id().instance(instance)),
controller.clock().instant());
@@ -375,7 +390,7 @@ public class JobController {
* Throws TimeoutException if some step in this job is still being run.
*/
public void finish(RunId id) throws TimeoutException {
- List<Lock> locks = new ArrayList<>();
+ List<Mutex> locks = new ArrayList<>();
try {
// Ensure no step is still running before we finish the run — report depends transitively on all the other steps.
Run unlockedRun = run(id).get();
@@ -396,7 +411,7 @@ public class JobController {
locked(id.application(), id.type(), runs -> {
runs.put(run.id(), finishedRun);
long last = id.number();
- long successes = runs.values().stream().filter(old -> old.status() == RunStatus.success).count();
+ long successes = runs.values().stream().filter(Run::hasSucceeded).count();
var oldEntries = runs.entrySet().iterator();
for (var old = oldEntries.next();
old.getKey().number() <= last - historyLength
@@ -405,7 +420,7 @@ public class JobController {
// Make sure we keep the last success and the first failing
if ( successes == 1
- && old.getValue().status() == RunStatus.success
+ && old.getValue().hasSucceeded()
&& ! old.getValue().start().isBefore(controller.clock().instant().minus(maxHistoryAge))) {
oldEntries.next();
continue;
@@ -417,26 +432,21 @@ public class JobController {
});
logs.flush(id);
metric.jobFinished(run.id().job(), finishedRun.status());
+ pruneRevisions(unlockedRun);
- DeploymentId deploymentId = new DeploymentId(unlockedRun.id().application(), unlockedRun.id().job().type().zone(controller.system()));
- (unlockedRun.versions().targetApplication().isDeployedDirectly() ?
- Stream.of(unlockedRun.id().type()) :
- JobType.allIn(controller.system()).stream().filter(jobType -> !jobType.environment().isManuallyDeployed()))
- .flatMap(jobType -> controller.jobController().runs(unlockedRun.id().application(), jobType).values().stream())
- .mapToLong(r -> r.versions().targetApplication().buildNumber().orElse(Integer.MAX_VALUE))
- .min()
- .ifPresent(oldestBuild -> {
- if (unlockedRun.versions().targetApplication().isDeployedDirectly())
- controller.applications().applicationStore().pruneDevDiffs(deploymentId, oldestBuild);
- else
- controller.applications().applicationStore().pruneDiffs(deploymentId.applicationId().tenant(), deploymentId.applicationId().application(), oldestBuild);
- });
return finishedRun;
});
}
finally {
- for (Lock lock : locks)
- lock.close();
+ for (Mutex lock : locks) {
+ try {
+ lock.close();
+ } catch (Throwable t) {
+ log.log(WARNING, "Failed to close the lock " + lock + ": the lock may or may not " +
+ "have been released in ZooKeeper, and if not this controller " +
+ "must be restarted to release the lock", t);
+ }
+ }
}
}
@@ -451,48 +461,96 @@ public class JobController {
}
/** Accepts and stores a new application package and test jar pair under a generated application version key. */
- public ApplicationVersion submit(TenantAndApplicationId id, Optional<SourceRevision> revision, Optional<String> authorEmail,
- Optional<String> sourceUrl, long projectId, ApplicationPackage applicationPackage,
- byte[] testPackageBytes) {
+ public ApplicationVersion submit(TenantAndApplicationId id, Submission submission, long projectId) {
+ ApplicationController applications = controller.applications();
AtomicReference<ApplicationVersion> version = new AtomicReference<>();
- controller.applications().lockApplicationOrThrow(id, application -> {
- Optional<ApplicationVersion> previousVersion = application.get().latestVersion();
- Optional<ApplicationPackage> previousPackage = previousVersion.flatMap(previous -> controller.applications().applicationStore().find(id.tenant(), id.application(), previous.buildNumber().getAsLong()))
+ applications.lockApplicationOrThrow(id, application -> {
+ Optional<ApplicationVersion> previousVersion = application.get().revisions().last();
+ Optional<ApplicationPackage> previousPackage = previousVersion.flatMap(previous -> applications.applicationStore().find(id.tenant(), id.application(), previous.buildNumber().getAsLong()))
.map(ApplicationPackage::new);
long previousBuild = previousVersion.map(latestVersion -> latestVersion.buildNumber().getAsLong()).orElse(0L);
- String packageHash = applicationPackage.bundleHash() + ApplicationPackage.calculateHash(testPackageBytes);
- version.set(ApplicationVersion.from(revision, 1 + previousBuild, authorEmail,
- applicationPackage.compileVersion(),
- applicationPackage.buildTime(),
- sourceUrl,
- revision.map(SourceRevision::commit),
- false,
- Optional.of(packageHash)));
-
- byte[] diff = previousPackage.map(previous -> ApplicationPackageDiff.diff(previous, applicationPackage))
- .orElseGet(() -> ApplicationPackageDiff.diffAgainstEmpty(applicationPackage));
- controller.applications().applicationStore().put(id.tenant(),
- id.application(),
- version.get(),
- applicationPackage.zippedContent(),
- diff);
- controller.applications().applicationStore().putTester(id.tenant(),
- id.application(),
- version.get(),
- testPackageBytes);
- controller.applications().applicationStore().putMeta(id.tenant(),
- id.application(),
- controller.clock().instant(),
- applicationPackage.metaDataZip());
-
- prunePackages(id);
- controller.applications().storeWithUpdatedConfig(application, applicationPackage);
-
- controller.applications().deploymentTrigger().notifyOfSubmission(id, version.get(), projectId);
+ version.set(submission.toApplicationVersion(1 + previousBuild));
+
+ byte[] diff = previousPackage.map(previous -> ApplicationPackageDiff.diff(previous, submission.applicationPackage()))
+ .orElseGet(() -> ApplicationPackageDiff.diffAgainstEmpty(submission.applicationPackage()));
+ applications.applicationStore().put(id.tenant(),
+ id.application(),
+ version.get().id(),
+ submission.applicationPackage().zippedContent(),
+ submission.testPackage(),
+ diff);
+ applications.applicationStore().putMeta(id.tenant(),
+ id.application(),
+ controller.clock().instant(),
+ submission.applicationPackage().metaDataZip());
+
+ application = application.withProjectId(projectId == -1 ? OptionalLong.empty() : OptionalLong.of(projectId));
+ application = application.withRevisions(revisions -> revisions.with(version.get()));
+ application = withPrunedPackages(application);
+
+ TestSummary testSummary = TestPackage.validateTests(submission.applicationPackage().deploymentSpec(), submission.testPackage());
+ if (testSummary.problems().isEmpty())
+ controller.notificationsDb().removeNotification(NotificationSource.from(id), Type.testPackage);
+ else
+ controller.notificationsDb().setNotification(NotificationSource.from(id),
+ Type.testPackage,
+ Notification.Level.warning,
+ testSummary.problems());
+
+ submission.applicationPackage().parentVersion().ifPresent(parent -> {
+ if (parent.getMajor() < controller.readSystemVersion().getMajor())
+ controller.notificationsDb().setNotification(NotificationSource.from(id),
+ Type.submission,
+ Notification.Level.warning,
+ "Parent version used to compile the application is on a " +
+ "lower major version than the current Vespa Cloud version");
+ else
+ controller.notificationsDb().removeNotification(NotificationSource.from(id), Type.submission);
+ });
+
+ applications.storeWithUpdatedConfig(application, submission.applicationPackage());
+ applications.deploymentTrigger().triggerNewRevision(id);
});
return version.get();
}
+ private LockedApplication withPrunedPackages(LockedApplication application){
+ TenantAndApplicationId id = application.get().id();
+ Optional<RevisionId> oldestDeployed = application.get().oldestDeployedRevision();
+ if (oldestDeployed.isPresent()) {
+ controller.applications().applicationStore().prune(id.tenant(), id.application(), oldestDeployed.get());
+
+ for (ApplicationVersion version : application.get().revisions().withPackage())
+ if (version.id().compareTo(oldestDeployed.get()) < 0)
+ application = application.withRevisions(revisions -> revisions.with(version.withoutPackage()));
+ }
+ return application;
+ }
+
+ /** Forget revisions no longer present in any relevant job history. */
+ private void pruneRevisions(Run run) {
+ TenantAndApplicationId applicationId = TenantAndApplicationId.from(run.id().application());
+ boolean isProduction = run.versions().targetRevision().isProduction();
+ (isProduction ? deploymentStatus(controller.applications().requireApplication(applicationId)).jobs().asList().stream()
+ : Stream.of(jobStatus(run.id().job())))
+ .flatMap(jobs -> jobs.runs().values().stream())
+ .map(r -> r.versions().targetRevision())
+ .filter(id -> id.isProduction() == isProduction)
+ .min(naturalOrder())
+ .ifPresent(oldestRevision -> {
+ controller.applications().lockApplicationOrThrow(applicationId, application -> {
+ if (isProduction) {
+ controller.applications().applicationStore().pruneDiffs(run.id().application().tenant(), run.id().application().application(), oldestRevision.number());
+ controller.applications().store(application.withRevisions(revisions -> revisions.withoutOlderThan(oldestRevision)));
+ }
+ else {
+ controller.applications().applicationStore().pruneDevDiffs(new DeploymentId(run.id().application(), run.id().job().type().zone()), oldestRevision.number());
+ controller.applications().store(application.withRevisions(revisions -> revisions.withoutOlderThan(oldestRevision, run.id().job())));
+ }
+ });
+ });
+ }
+
/** Orders a run of the given type, or throws an IllegalStateException if that job type is already running. */
public void start(ApplicationId id, JobType type, Versions versions, boolean isRedeployment, Optional<String> reason) {
start(id, type, versions, isRedeployment, JobProfile.of(type), reason);
@@ -500,11 +558,12 @@ public class JobController {
/** Orders a run of the given type, or throws an IllegalStateException if that job type is already running. */
public void start(ApplicationId id, JobType type, Versions versions, boolean isRedeployment, JobProfile profile, Optional<String> reason) {
- if (versions.targetApplication().compileVersion()
+ ApplicationVersion revision = controller.applications().requireApplication(TenantAndApplicationId.from(id)).revisions().get(versions.targetRevision());
+ if (revision.compileVersion()
.map(version -> controller.applications().versionCompatibility(id).refuse(versions.targetPlatform(), version))
.orElse(false))
throw new IllegalArgumentException("Will not start a job with incompatible platform version (" + versions.targetPlatform() + ") " +
- "and compile versions (" + versions.targetApplication().compileVersion().get() + ")");
+ "and compile versions (" + revision.compileVersion().get() + ")");
locked(id, type, __ -> {
Optional<Run> last = last(id, type);
@@ -525,6 +584,9 @@ public class JobController {
/** Stores the given package and starts a deployment of it, after aborting any such ongoing deployment.*/
public void deploy(ApplicationId id, JobType type, Optional<Version> platform, ApplicationPackage applicationPackage, boolean dryRun) {
+ if ( ! controller.zoneRegistry().hasZone(type.zone()))
+ throw new IllegalArgumentException(type.zone() + " is not present in this system");
+
controller.applications().lockApplicationOrThrow(TenantAndApplicationId.from(id), application -> {
if ( ! application.get().instances().containsKey(id.instance()))
application = controller.applications().withNewInstance(application, id);
@@ -532,24 +594,23 @@ public class JobController {
controller.applications().store(application);
});
- DeploymentId deploymentId = new DeploymentId(id, type.zone(controller.system()));
+ DeploymentId deploymentId = new DeploymentId(id, type.zone());
Optional<Run> lastRun = last(id, type);
lastRun.filter(run -> ! run.hasEnded()).ifPresent(run -> abortAndWait(run.id()));
- long build = 1 + lastRun.map(run -> run.versions().targetApplication().buildNumber().orElse(0)).orElse(0L);
- ApplicationVersion version = ApplicationVersion.from(Optional.empty(), build, Optional.empty(),
- applicationPackage.compileVersion(),
- Optional.empty(), Optional.empty(),
- Optional.empty(), true, Optional.empty());
+ long build = 1 + lastRun.map(run -> run.versions().targetRevision().number()).orElse(0L);
+ RevisionId revisionId = RevisionId.forDevelopment(build, new JobId(id, type));
+ ApplicationVersion version = ApplicationVersion.forDevelopment(revisionId, applicationPackage.compileVersion());
byte[] diff = getDiff(applicationPackage, deploymentId, lastRun);
controller.applications().lockApplicationOrThrow(TenantAndApplicationId.from(id), application -> {
- controller.applications().applicationStore().putDev(deploymentId, version, applicationPackage.zippedContent(), diff);
- Version targetPlatform = platform.orElseGet(() -> findTargetPlatform(applicationPackage, lastRun, id));
+ controller.applications().applicationStore().putDev(deploymentId, version.id(), applicationPackage.zippedContent(), diff);
+ Version targetPlatform = platform.orElseGet(() -> findTargetPlatform(applicationPackage, deploymentId, application.get().get(id.instance())));
+ controller.applications().store(application.withRevisions(revisions -> revisions.with(version)));
start(id,
type,
- new Versions(targetPlatform, version, lastRun.map(run -> run.versions().targetPlatform()), lastRun.map(run -> run.versions().targetApplication())),
+ new Versions(targetPlatform, version.id(), lastRun.map(run -> run.versions().targetPlatform()), lastRun.map(run -> run.versions().targetRevision())),
false,
dryRun ? JobProfile.developmentDryRun : JobProfile.development,
Optional.empty());
@@ -562,7 +623,7 @@ public class JobController {
/* Application package diff against previous version, or against empty version if previous does not exist or is invalid */
private byte[] getDiff(ApplicationPackage applicationPackage, DeploymentId deploymentId, Optional<Run> lastRun) {
- return lastRun.map(run -> run.versions().targetApplication())
+ return lastRun.map(run -> run.versions().targetRevision())
.map(prevVersion -> {
ApplicationPackage previous;
try {
@@ -575,24 +636,27 @@ public class JobController {
.orElseGet(() -> ApplicationPackageDiff.diffAgainstEmpty(applicationPackage));
}
- private Version findTargetPlatform(ApplicationPackage applicationPackage, Optional<Run> lastRun, ApplicationId id) {
+ private Version findTargetPlatform(ApplicationPackage applicationPackage, DeploymentId id, Optional<Instance> instance) {
Optional<Integer> major = applicationPackage.deploymentSpec().majorVersion();
if (major.isPresent())
return controller.applications().lastCompatibleVersion(major.get())
.orElseThrow(() -> new IllegalArgumentException("major " + major.get() + " specified in deployment.xml, " +
"but no version on this major was found"));
- // Prefer previous platform if possible.
- VersionStatus versionStatus = controller.readVersionStatus();
- VersionCompatibility compatibility = controller.applications().versionCompatibility(id);
- Optional<Version> target = lastRun.map(run -> run.versions().targetPlatform()).filter(versionStatus::isActive);
- if (target.isPresent() && compatibility.accept(target.get(), applicationPackage.compileVersion().orElse(target.get())))
- return target.get();
+ VersionCompatibility compatibility = controller.applications().versionCompatibility(id.applicationId());
+
+ // Prefer previous platform if possible. Candidates are all deployable, ascending, with existing version appended; then reversed.
+ List<Version> versions = controller.readVersionStatus().deployableVersions().stream()
+ .map(VespaVersion::versionNumber)
+ .collect(toList());
+ instance.map(Instance::deployments)
+ .map(deployments -> deployments.get(id.zoneId()))
+ .map(Deployment::version)
+ .ifPresent(versions::add);
- // Otherwise, use newest, compatible version.
- for (VespaVersion platform : reversed(versionStatus.deployableVersions()))
- if (compatibility.accept(platform.versionNumber(), applicationPackage.compileVersion().orElse(platform.versionNumber())))
- return platform.versionNumber();
+ for (Version target : reversed(versions))
+ if (applicationPackage.compileVersion().isEmpty() || compatibility.accept(target, applicationPackage.compileVersion().get()))
+ return target;
throw new IllegalArgumentException("no suitable platform version found" +
applicationPackage.compileVersion()
@@ -626,7 +690,7 @@ public class JobController {
TesterId tester = TesterId.of(id);
for (JobType type : jobs(id))
locked(id, type, deactivateTester, __ -> {
- try (Lock ___ = curator.lock(id, type)) {
+ try (Mutex ___ = curator.lock(id, type)) {
try {
deactivateTester(tester, type);
}
@@ -634,34 +698,24 @@ public class JobController {
// It's probably already deleted, so if we fail, that's OK.
}
curator.deleteRunData(id, type);
- logs.delete(id);
}
});
+ logs.delete(id);
+ curator.deleteRunData(id);
}
catch (Exception e) {
- return; // Don't remove the data if we couldn't clean up all resources.
+ log.log(WARNING, "failed cleaning up after deleted application", e);
}
- curator.deleteRunData(id);
});
}
public void deactivateTester(TesterId id, JobType type) {
- controller.serviceRegistry().configServer().deactivate(new DeploymentId(id.id(), type.zone(controller.system())));
- }
-
- private void prunePackages(TenantAndApplicationId id) {
- controller.applications().lockApplicationIfPresent(id, application -> {
- application.get().oldestDeployedApplication()
- .ifPresent(oldestDeployed -> {
- controller.applications().applicationStore().prune(id.tenant(), id.application(), oldestDeployed);
- controller.applications().applicationStore().pruneTesters(id.tenant(), id.application(), oldestDeployed);
- });
- });
+ controller.serviceRegistry().configServer().deactivate(new DeploymentId(id.id(), type.zone()));
}
/** Locks all runs and modifies the list of historic runs for the given application and job type. */
private void locked(ApplicationId id, JobType type, Consumer<SortedMap<RunId, Run>> modifications) {
- try (Lock __ = curator.lock(id, type)) {
+ try (Mutex __ = curator.lock(id, type)) {
SortedMap<RunId, Run> runs = new TreeMap<>(curator.readHistoricRuns(id, type));
modifications.accept(runs);
curator.writeHistoricRuns(id, type, runs.values());
@@ -670,19 +724,18 @@ public class JobController {
/** Locks and modifies the run with the given id, provided it is still active. */
public void locked(RunId id, UnaryOperator<Run> modifications) {
- try (Lock __ = curator.lock(id.application(), id.type())) {
+ try (Mutex __ = curator.lock(id.application(), id.type())) {
active(id).ifPresent(run -> {
- run = modifications.apply(run);
- curator.writeLastRun(run);
+ curator.writeLastRun(modifications.apply(run));
});
}
}
/** Locks the given step and checks none of its prerequisites are running, then performs the given actions. */
public void locked(ApplicationId id, JobType type, Step step, Consumer<LockedStep> action) throws TimeoutException {
- try (Lock lock = curator.lock(id, type, step)) {
+ try (Mutex lock = curator.lock(id, type, step)) {
for (Step prerequisite : step.allPrerequisites(last(id, type).get().steps().keySet())) // Check that no prerequisite is still running.
- try (Lock __ = curator.lock(id, type, prerequisite)) { ; }
+ try (Mutex __ = curator.lock(id, type, prerequisite)) { ; }
action.accept(new LockedStep(lock, step));
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobList.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobList.java
index d06bdc45583..387ea755414 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobList.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobList.java
@@ -7,6 +7,7 @@ import com.yahoo.config.provision.InstanceName;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
+import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
import java.time.Instant;
import java.util.Collection;
@@ -119,7 +120,7 @@ public class JobList extends AbstractFilteringList<JobStatus, JobList> {
/** Returns the jobs with successful runs matching the given versions — targets only for system test, everything present otherwise. */
public JobList successOn(Versions versions) {
- return matching(job -> ! RunList.from(job).status(RunStatus.success).on(versions).isEmpty());
+ return matching(job -> ! RunList.from(job).matching(Run::hasSucceeded).on(versions).isEmpty());
}
// ----------------------------------- JobRun filtering
@@ -174,8 +175,8 @@ public class JobList extends AbstractFilteringList<JobStatus, JobList> {
}
/** Returns the subset of jobs where the run of the indicated type was on the given version */
- public JobList on(ApplicationVersion version) {
- return matching(run -> run.versions().targetApplication().equals(version));
+ public JobList on(RevisionId revision) {
+ return matching(run -> run.versions().targetRevision().equals(revision));
}
/** Returns the subset of jobs where the run of the indicated type was on the given version */
@@ -196,7 +197,7 @@ public class JobList extends AbstractFilteringList<JobStatus, JobList> {
if (job.isSuccess()) return false;
if (job.lastSuccess().isEmpty()) return true; // An application which never succeeded is surely bad.
if ( ! job.firstFailing().get().versions().targetPlatform().equals(job.lastSuccess().get().versions().targetPlatform())) return false; // Version change may be to blame.
- return ! job.firstFailing().get().versions().targetApplication().equals(job.lastSuccess().get().versions().targetApplication()); // Return whether there is an application change.
+ return ! job.firstFailing().get().versions().targetRevision().equals(job.lastSuccess().get().versions().targetRevision()); // Return whether there is an application change.
}
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobMetrics.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobMetrics.java
index 874b1828f5f..14fce806152 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobMetrics.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobMetrics.java
@@ -21,6 +21,7 @@ public class JobMetrics {
public static final String deploymentFailure = "deployment.deploymentFailure";
public static final String convergenceFailure = "deployment.convergenceFailure";
public static final String testFailure = "deployment.testFailure";
+ public static final String noTests = "deployment.noTests";
public static final String error = "deployment.error";
public static final String abort = "deployment.abort";
public static final String success = "deployment.success";
@@ -46,7 +47,7 @@ public class JobMetrics {
"tenantName", id.application().tenant().value(),
"app", id.application().application().value() + "." + id.application().instance().value(),
"test", Boolean.toString(id.type().isTest()),
- "zone", id.type().zone(system.get()).value());
+ "zone", id.type().zone().value());
}
static String valueOf(RunStatus status) {
@@ -56,6 +57,7 @@ public class JobMetrics {
case deploymentFailed: return deploymentFailure;
case installationFailed: return convergenceFailure;
case testFailure: return testFailure;
+ case noTests: return noTests;
case error: return error;
case aborted: return abort;
case success: return success;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobStatus.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobStatus.java
index aad5d510261..45bf508f026 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobStatus.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobStatus.java
@@ -60,7 +60,7 @@ public class JobStatus {
}
public boolean isSuccess() {
- return lastStatus().isPresent() && lastStatus().get() == RunStatus.success;
+ return lastCompleted.map(last -> ! last.hasFailed()).orElse(false);
}
public boolean isRunning() {
@@ -90,18 +90,17 @@ public class JobStatus {
static Optional<Run> lastSuccess(NavigableMap<RunId, Run> runs) {
return runs.descendingMap().values().stream()
- .filter(run -> run.status() == RunStatus.success)
+ .filter(Run::hasSucceeded)
.findFirst();
}
static Optional<Run> firstFailing(NavigableMap<RunId, Run> runs) {
Run failed = null;
- loop: for (Run run : runs.descendingMap().values())
- switch (run.status()) {
- case running: continue loop;
- case success: break loop;
- default: failed = run;
- }
+ for (Run run : runs.descendingMap().values()) {
+ if ( ! run.hasEnded()) continue;
+ if ( ! run.hasFailed()) break;
+ failed = run;
+ }
return Optional.ofNullable(failed);
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/LockedStep.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/LockedStep.java
index d46516582be..8147ccb3180 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/LockedStep.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/LockedStep.java
@@ -1,6 +1,7 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.deployment;
+import com.yahoo.transaction.Mutex;
import com.yahoo.vespa.curator.Lock;
/**
@@ -9,7 +10,7 @@ import com.yahoo.vespa.curator.Lock;
public class LockedStep {
private final Step step;
- LockedStep(Lock lock, Step step) { this.step = step; }
+ LockedStep(Mutex lock, Step step) { this.step = step; }
public Step get() { return step; }
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RetriggerEntrySerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RetriggerEntrySerializer.java
index 6f456d2e217..e0c1fef91b3 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RetriggerEntrySerializer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RetriggerEntrySerializer.java
@@ -3,6 +3,7 @@
package com.yahoo.vespa.hosted.controller.deployment;
import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.SystemName;
import com.yahoo.slime.Cursor;
import com.yahoo.slime.Inspector;
import com.yahoo.slime.Slime;
@@ -23,41 +24,42 @@ public class RetriggerEntrySerializer {
private static final String JOB_TYPE_KEY = "jobType";
private static final String MIN_REQUIRED_RUN_ID_KEY = "minimumRunId";
- public static List<RetriggerEntry> fromSlime(Slime slime) {
+ public List<RetriggerEntry> fromSlime(Slime slime) {
return SlimeUtils.entriesStream(slime.get().field("entries"))
- .map(RetriggerEntrySerializer::deserializeEntry)
+ .map(this::deserializeEntry)
.collect(Collectors.toList());
}
- public static Slime toSlime(List<RetriggerEntry> entryList) {
+ public Slime toSlime(List<RetriggerEntry> entryList) {
Slime slime = new Slime();
Cursor root = slime.setObject();
Cursor entries = root.setArray("entries");
- entryList.forEach(e -> RetriggerEntrySerializer.serializeEntry(entries, e));
+ entryList.forEach(e -> serializeEntry(entries, e));
return slime;
}
- private static void serializeEntry(Cursor array, RetriggerEntry entry) {
+ private void serializeEntry(Cursor array, RetriggerEntry entry) {
Cursor root = array.addObject();
Cursor jobid = root.setObject(JOB_ID_KEY);
jobid.setString(APPLICATION_ID_KEY, entry.jobId().application().serializedForm());
- jobid.setString(JOB_TYPE_KEY, entry.jobId().type().jobName());
+ jobid.setString(JOB_TYPE_KEY, entry.jobId().type().serialized());
root.setLong(MIN_REQUIRED_RUN_ID_KEY, entry.requiredRun());
}
- private static RetriggerEntry deserializeEntry(Inspector inspector) {
+ private RetriggerEntry deserializeEntry(Inspector inspector) {
Inspector jobid = inspector.field(JOB_ID_KEY);
ApplicationId applicationId = ApplicationId.fromSerializedForm(require(jobid, APPLICATION_ID_KEY).asString());
- JobType jobType = JobType.fromJobName(require(jobid, JOB_TYPE_KEY).asString());
+ JobType jobType = JobType.ofSerialized(require(jobid, JOB_TYPE_KEY).asString());
long minRequiredRunId = require(inspector, MIN_REQUIRED_RUN_ID_KEY).asLong();
return new RetriggerEntry(new JobId(applicationId, jobType), minRequiredRunId);
}
- private static Inspector require(Inspector inspector, String fieldName) {
+ private Inspector require(Inspector inspector, String fieldName) {
Inspector field = inspector.field(fieldName);
if (!field.valid()) {
throw new IllegalStateException("Could not deserialize, field not found in json: " + fieldName);
}
return field;
}
+
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RevisionHistory.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RevisionHistory.java
new file mode 100644
index 00000000000..5bdc980f11a
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RevisionHistory.java
@@ -0,0 +1,149 @@
+package com.yahoo.vespa.hosted.controller.deployment;
+
+import ai.vespa.validation.Validation;
+import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
+import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
+import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
+
+import java.util.ArrayDeque;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Deque;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.Optional;
+import java.util.OptionalLong;
+import java.util.TreeMap;
+
+import static ai.vespa.validation.Validation.require;
+import static java.util.Collections.emptyNavigableMap;
+import static java.util.stream.Collectors.toList;
+
+/**
+ * History of application revisions for an {@link com.yahoo.vespa.hosted.controller.Application}.
+ *
+ * @author jonmv
+ */
+public class RevisionHistory {
+
+ private static final Comparator<JobId> comparator = Comparator.comparing(JobId::application).thenComparing(JobId::type);
+
+ private final NavigableMap<RevisionId, ApplicationVersion> production;
+ private final NavigableMap<JobId, NavigableMap<RevisionId, ApplicationVersion>> development;
+
+ private RevisionHistory(NavigableMap<RevisionId, ApplicationVersion> production,
+ NavigableMap<JobId, NavigableMap<RevisionId, ApplicationVersion>> development) {
+ this.production = production;
+ this.development = development;
+ }
+
+ public static RevisionHistory empty() {
+ return ofRevisions(List.of(), Map.of());
+ }
+
+ public static RevisionHistory ofRevisions(Collection<ApplicationVersion> productionRevisions,
+ Map<JobId, ? extends Collection<ApplicationVersion>> developmentRevisions) {
+ NavigableMap<RevisionId, ApplicationVersion> production = new TreeMap<>();
+ for (ApplicationVersion revision : productionRevisions)
+ production.put(revision.id(), revision);
+
+ // TODO jonmv: remove once it's run once on serialised data
+ String hash = "";
+ for (ApplicationVersion revision : List.copyOf(production.values()))
+ if (hash.equals(hash = revision.bundleHash().orElse("")) && ! hash.isEmpty())
+ production.put(revision.id(), revision.skipped());
+
+ NavigableMap<JobId, NavigableMap<RevisionId, ApplicationVersion>> development = new TreeMap<>(comparator);
+ developmentRevisions.forEach((job, jobRevisions) -> {
+ NavigableMap<RevisionId, ApplicationVersion> revisions = development.computeIfAbsent(job, __ -> new TreeMap<>());
+ for (ApplicationVersion revision : jobRevisions)
+ revisions.put(revision.id(), revision);
+ });
+
+ return new RevisionHistory(production, development);
+ }
+
+ /** Returns a copy of this without any production revisions older than the given. */
+ public RevisionHistory withoutOlderThan(RevisionId id) {
+ if (production.headMap(id).isEmpty()) return this;
+ return new RevisionHistory(production.tailMap(id, true), development);
+ }
+
+ /** Returns a copy of this without any development revisions older than the given. */
+ public RevisionHistory withoutOlderThan(RevisionId id, JobId job) {
+ if ( ! development.containsKey(job) || development.get(job).headMap(id).isEmpty()) return this;
+ NavigableMap<JobId, NavigableMap<RevisionId, ApplicationVersion>> development = new TreeMap<>(this.development);
+ development.compute(job, (__, revisions) -> revisions.tailMap(id, true));
+ return new RevisionHistory(production, development);
+ }
+
+ /** Returns a copy of this with the revision added or updated. */
+ public RevisionHistory with(ApplicationVersion revision) {
+ if (revision.id().isProduction()) {
+ if ( ! production.isEmpty() && revision.bundleHash().flatMap(hash -> production.lastEntry().getValue().bundleHash().map(hash::equals)).orElse(false))
+ revision = revision.skipped();
+
+ NavigableMap<RevisionId, ApplicationVersion> production = new TreeMap<>(this.production);
+ production.put(revision.id(), revision);
+ return new RevisionHistory(production, development);
+ }
+ else {
+ NavigableMap<JobId, NavigableMap<RevisionId, ApplicationVersion>> development = new TreeMap<>(this.development);
+ NavigableMap<RevisionId, ApplicationVersion> revisions = development.compute(revision.id().job(), (__, old) -> new TreeMap<>(old != null ? old : emptyNavigableMap()));
+ if ( ! revisions.isEmpty()) revisions.compute(revisions.lastKey(), (__, last) -> last.withoutPackage());
+ revisions.put(revision.id(), revision);
+ return new RevisionHistory(production, development);
+ }
+ }
+
+ // Fallback for when an application version isn't known for the given key.
+ private static ApplicationVersion revisionOf(RevisionId id) {
+ return new ApplicationVersion(id, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), false, false, Optional.empty(), 0);
+ }
+
+ /** Returns the production {@link ApplicationVersion} with this revision ID. */
+ public ApplicationVersion get(RevisionId id) {
+ return id.isProduction() ? production.getOrDefault(id, revisionOf(id))
+ : development.getOrDefault(id.job(), emptyNavigableMap())
+ .getOrDefault(id, revisionOf(id));
+ }
+
+ /** Returns the last submitted production build. */
+ public Optional<ApplicationVersion> last() {
+ return Optional.ofNullable(production.lastEntry()).map(Map.Entry::getValue);
+ }
+
+ /** Returns all known production revisions we still have the package for, from oldest to newest. */
+ public List<ApplicationVersion> withPackage() {
+ return production.values().stream()
+ .filter(ApplicationVersion::hasPackage)
+ .collect(toList());
+ }
+
+ /** Returns the currently deployable revisions of the application. */
+ public Deque<ApplicationVersion> deployable(boolean ascending) {
+ Deque<ApplicationVersion> versions = new ArrayDeque<>();
+ for (ApplicationVersion version : withPackage()) {
+ if (version.isDeployable()) {
+ if (ascending) versions.addLast(version);
+ else versions.addFirst(version);
+ }
+ }
+ return versions;
+ }
+
+ /** All known production revisions, in ascending order. */
+ public List<ApplicationVersion> production() {
+ return List.copyOf(production.values());
+ }
+
+ /* All known development revisions, in ascending order, per job. */
+ public NavigableMap<JobId, List<ApplicationVersion>> development() {
+ NavigableMap<JobId, List<ApplicationVersion>> copy = new TreeMap<>(comparator);
+ development.forEach((job, revisions) -> copy.put(job, List.copyOf(revisions.values())));
+ return Collections.unmodifiableNavigableMap(copy);
+ }
+
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Run.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Run.java
index e73d3f52e1f..03cc6c6ba8d 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Run.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Run.java
@@ -13,6 +13,7 @@ import java.util.Optional;
import java.util.stream.Collectors;
import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.aborted;
+import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.noTests;
import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.running;
import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.success;
import static com.yahoo.vespa.hosted.controller.deployment.Step.Status.succeeded;
@@ -80,8 +81,9 @@ public class Run {
EnumMap<Step, StepInfo> steps = new EnumMap<>(this.steps);
steps.put(step.get(), stepInfo.with(Step.Status.of(status)));
- return new Run(id, steps, versions, isRedeployment, start, end, sleepUntil, this.status == running ? status : this.status,
- lastTestRecord, lastVespaLogTimestamp, noNodesDownSince, convergenceSummary, testerCertificate, dryRun, reason);
+ RunStatus newStatus = hasFailed() || status == running ? this.status : status;
+ return new Run(id, steps, versions, isRedeployment, start, end, sleepUntil, newStatus, lastTestRecord,
+ lastVespaLogTimestamp, noNodesDownSince, convergenceSummary, testerCertificate, dryRun, reason);
}
/** Returns a new Run with a new start time*/
@@ -210,7 +212,7 @@ public class Run {
/** Returns whether the run has failed, and should switch to its run-always steps. */
public boolean hasFailed() {
- return status != running && status != success;
+ return status != running && status != success && status != noTests;
}
/** Returns whether the run has ended, i.e., has become inactive, and can no longer be updated. */
@@ -218,6 +220,8 @@ public class Run {
return end.isPresent();
}
+ public boolean hasSucceeded() { return hasEnded() && ! hasFailed(); }
+
/** Returns the target, and possibly source, versions for this run. */
public Versions versions() {
return versions;
@@ -297,7 +301,7 @@ public class Run {
return steps.entrySet().stream()
.filter(entry -> entry.getValue().status() == unfinished
&& entry.getKey().prerequisites().stream()
- .allMatch(step -> steps.get(step) == null
+ .allMatch(step -> steps.get(step) == null
|| steps.get(step).status() == succeeded))
.map(Map.Entry::getKey)
.collect(Collectors.toUnmodifiableList());
@@ -310,7 +314,7 @@ public class Run {
&& entry.getKey().alwaysRun()
&& entry.getKey().prerequisites().stream()
.filter(Step::alwaysRun)
- .allMatch(step -> steps.get(step) == null
+ .allMatch(step -> steps.get(step) == null
|| steps.get(step).status() != unfinished))
.map(Map.Entry::getKey)
.collect(Collectors.toUnmodifiableList());
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RunList.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RunList.java
index 00cd4bd5c6c..80c6552d3d4 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RunList.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RunList.java
@@ -38,7 +38,7 @@ public class RunList extends AbstractFilteringList<Run, RunList> {
private static boolean matchingVersions(Run run, Versions versions) {
return versions.targetsMatch(run.versions())
- && (versions.sourcesMatchIfPresent(run.versions()) || run.id().type() == JobType.systemTest);
+ && (versions.sourcesMatchIfPresent(run.versions()) || run.id().type().isSystemTest());
}
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RunStatus.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RunStatus.java
index 0bb4a30425e..9ca634b19fd 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RunStatus.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RunStatus.java
@@ -26,6 +26,9 @@ public enum RunStatus {
/** The verification tests failed. */
testFailure,
+ /** No tests, for a test job. */
+ noTests,
+
/** An unexpected error occurred. */
error,
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Step.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Step.java
index 2e669808c44..82d154dcf03 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Step.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Step.java
@@ -3,11 +3,9 @@ package com.yahoo.vespa.hosted.controller.deployment;
import java.util.Collection;
import java.util.List;
-import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.util.stream.Collectors.toList;
-import static java.util.stream.Collectors.toUnmodifiableList;
/**
* Steps that make up a deployment job. See {@link JobProfile} for preset profiles.
@@ -115,6 +113,7 @@ public enum Step {
case success : throw new AssertionError("Unexpected run status '" + status + "'!");
case reset :
case aborted : return unfinished;
+ case noTests :
case running : return succeeded;
default : return failed;
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Submission.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Submission.java
new file mode 100644
index 00000000000..e366920690b
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Submission.java
@@ -0,0 +1,57 @@
+package com.yahoo.vespa.hosted.controller.deployment;
+
+import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
+import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
+import com.yahoo.vespa.hosted.controller.api.integration.deployment.SourceRevision;
+import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
+
+import java.util.Optional;
+
+import static com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage.calculateHash;
+
+/**
+ * @author jonmv
+ */
+public class Submission {
+
+ private final ApplicationPackage applicationPackage;
+ private final byte[] testPackage;
+ private final Optional<String> sourceUrl;
+ private final Optional<SourceRevision> source;
+ private final Optional<String> authorEmail;
+ private final Optional<String> description;
+ private final int risk;
+
+ public Submission(ApplicationPackage applicationPackage, byte[] testPackage, Optional<String> sourceUrl,
+ Optional<SourceRevision> source, Optional<String> authorEmail, Optional<String> description, int risk) {
+ this.applicationPackage = applicationPackage;
+ this.testPackage = testPackage;
+ this.sourceUrl = sourceUrl;
+ this.source = source;
+ this.authorEmail = authorEmail;
+ this.description = description;
+ this.risk = risk;
+ }
+
+ public static Submission basic(ApplicationPackage applicationPackage, byte[] testPackage) {
+ return new Submission(applicationPackage, testPackage, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), 0);
+ }
+
+ public ApplicationVersion toApplicationVersion(long number) {
+ return ApplicationVersion.forProduction(RevisionId.forProduction(number),
+ source,
+ authorEmail,
+ applicationPackage.compileVersion(),
+ applicationPackage.buildTime(),
+ sourceUrl,
+ source.map(SourceRevision::commit),
+ Optional.of(applicationPackage.bundleHash() + calculateHash(testPackage)),
+ description,
+ risk);
+ }
+
+ public ApplicationPackage applicationPackage() { return applicationPackage; }
+
+ public byte[] testPackage() { return testPackage; }
+
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/TestConfigSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/TestConfigSerializer.java
index c9b488026a5..1680e064234 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/TestConfigSerializer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/TestConfigSerializer.java
@@ -2,7 +2,6 @@
package com.yahoo.vespa.hosted.controller.deployment;
import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.SystemName;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.slime.Cursor;
@@ -13,7 +12,6 @@ import com.yahoo.vespa.hosted.controller.application.Endpoint;
import java.io.IOException;
import java.io.UncheckedIOException;
-import java.net.URI;
import java.util.List;
import java.util.Map;
@@ -39,7 +37,7 @@ public class TestConfigSerializer {
Cursor root = slime.setObject();
root.setString("application", id.serializedForm());
- root.setString("zone", type.zone(system).value());
+ root.setString("zone", type.zone().value());
root.setString("system", system.value());
root.setBool("isCI", isCI);
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Versions.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Versions.java
index d0b8e773cae..f4c4b8bebd4 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Versions.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Versions.java
@@ -5,6 +5,7 @@ import com.yahoo.component.Version;
import com.yahoo.text.Text;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
+import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
import com.yahoo.vespa.hosted.controller.application.Change;
import com.yahoo.vespa.hosted.controller.application.Deployment;
@@ -22,24 +23,24 @@ import static java.util.Objects.requireNonNull;
public class Versions {
private final Version targetPlatform;
- private final ApplicationVersion targetApplication;
+ private final RevisionId targetRevision;
private final Optional<Version> sourcePlatform;
- private final Optional<ApplicationVersion> sourceApplication;
+ private final Optional<RevisionId> sourceRevision;
- public Versions(Version targetPlatform, ApplicationVersion targetApplication, Optional<Version> sourcePlatform,
- Optional<ApplicationVersion> sourceApplication) {
- if (sourcePlatform.isPresent() ^ sourceApplication.isPresent())
+ public Versions(Version targetPlatform, RevisionId targetRevision, Optional<Version> sourcePlatform,
+ Optional<RevisionId> sourceRevision) {
+ if (sourcePlatform.isPresent() ^ sourceRevision.isPresent())
throw new IllegalArgumentException("Sources must both be present or absent.");
this.targetPlatform = requireNonNull(targetPlatform);
- this.targetApplication = requireNonNull(targetApplication);
+ this.targetRevision = requireNonNull(targetRevision);
this.sourcePlatform = requireNonNull(sourcePlatform);
- this.sourceApplication = requireNonNull(sourceApplication);
+ this.sourceRevision = requireNonNull(sourceRevision);
}
/** A copy of this, without source versions. */
public Versions withoutSources() {
- return new Versions(targetPlatform, targetApplication, Optional.empty(), Optional.empty());
+ return new Versions(targetPlatform, targetRevision, Optional.empty(), Optional.empty());
}
/** Target platform version for this */
@@ -47,9 +48,9 @@ public class Versions {
return targetPlatform;
}
- /** Target application version for this */
- public ApplicationVersion targetApplication() {
- return targetApplication;
+ /** Target revision for this */
+ public RevisionId targetRevision() {
+ return targetRevision;
}
/** Source platform version for this */
@@ -58,27 +59,27 @@ public class Versions {
}
/** Source application version for this */
- public Optional<ApplicationVersion> sourceApplication() {
- return sourceApplication;
+ public Optional<RevisionId> sourceRevision() {
+ return sourceRevision;
}
/** Returns whether source versions are present and match those of the given job other versions. */
public boolean sourcesMatchIfPresent(Versions versions) {
return (sourcePlatform.map(targetPlatform::equals).orElse(true) ||
sourcePlatform.equals(versions.sourcePlatform())) &&
- (sourceApplication.map(targetApplication::equals).orElse(true) ||
- sourceApplication.equals(versions.sourceApplication()));
+ (sourceRevision.map(targetRevision::equals).orElse(true) ||
+ sourceRevision.equals(versions.sourceRevision()));
}
public boolean targetsMatch(Versions versions) {
return targetPlatform.equals(versions.targetPlatform()) &&
- targetApplication.equals(versions.targetApplication());
+ targetRevision.equals(versions.targetRevision());
}
/** Returns wheter this change could result in the given target versions. */
public boolean targetsMatch(Change change) {
return change.platform().map(targetPlatform::equals).orElse(true)
- && change.application().map(targetApplication::equals).orElse(true);
+ && change.revision().map(targetRevision::equals).orElse(true);
}
@Override
@@ -87,43 +88,43 @@ public class Versions {
if ( ! (o instanceof Versions)) return false;
Versions versions = (Versions) o;
return Objects.equals(targetPlatform, versions.targetPlatform) &&
- Objects.equals(targetApplication, versions.targetApplication) &&
+ Objects.equals(targetRevision, versions.targetRevision) &&
Objects.equals(sourcePlatform, versions.sourcePlatform) &&
- Objects.equals(sourceApplication, versions.sourceApplication);
+ Objects.equals(sourceRevision, versions.sourceRevision);
}
@Override
public int hashCode() {
- return Objects.hash(targetPlatform, targetApplication, sourcePlatform, sourceApplication);
+ return Objects.hash(targetPlatform, targetRevision, sourcePlatform, sourceRevision);
}
@Override
public String toString() {
- return Text.format("platform %s%s, application %s%s",
- sourcePlatform.filter(source -> !source.equals(targetPlatform))
+ return Text.format("platform %s%s, revision %s%s",
+ sourcePlatform.filter(source -> ! source.equals(targetPlatform))
.map(source -> source + " -> ").orElse(""),
targetPlatform,
- sourceApplication.filter(source -> !source.equals(targetApplication))
- .map(source -> source.id() + " -> ").orElse(""),
- targetApplication.id());
+ sourceRevision.filter(source -> ! source.equals(targetRevision))
+ .map(source -> source + " -> ").orElse(""),
+ targetRevision);
}
/** Create versions using given change and application */
public static Versions from(Change change, Application application, Optional<Version> existingPlatform,
- Optional<ApplicationVersion> existingApplication, Version defaultPlatformVersion) {
+ Optional<RevisionId> existingRevision, Version defaultPlatformVersion) {
return new Versions(targetPlatform(application, change, existingPlatform, defaultPlatformVersion),
- targetApplication(application, change, existingApplication),
+ targetRevision(application, change, existingRevision),
existingPlatform,
- existingApplication);
+ existingRevision);
}
/** Create versions using given change and application */
public static Versions from(Change change, Application application, Optional<Deployment> deployment,
Version defaultPlatformVersion) {
return new Versions(targetPlatform(application, change, deployment.map(Deployment::version), defaultPlatformVersion),
- targetApplication(application, change, deployment.map(Deployment::applicationVersion)),
+ targetRevision(application, change, deployment.map(Deployment::revision)),
deployment.map(Deployment::version),
- deployment.map(Deployment::applicationVersion));
+ deployment.map(Deployment::revision));
}
private static Version targetPlatform(Application application, Change change, Optional<Version> existing,
@@ -135,17 +136,17 @@ public class Versions {
.orElseGet(() -> application.oldestDeployedPlatform().orElse(defaultVersion));
}
- private static ApplicationVersion targetApplication(Application application, Change change,
- Optional<ApplicationVersion> existing) {
- return change.application()
+ private static RevisionId targetRevision(Application application, Change change,
+ Optional<RevisionId> existing) {
+ return change.revision()
.or(() -> existing)
- .orElseGet(() -> defaultApplicationVersion(application));
+ .orElseGet(() -> defaultRevision(application));
}
- private static ApplicationVersion defaultApplicationVersion(Application application) {
- return application.oldestDeployedApplication()
- .or(application::latestVersion)
- .orElse(ApplicationVersion.unknown);
+ private static RevisionId defaultRevision(Application application) {
+ return application.oldestDeployedRevision()
+ .or(() -> application.revisions().last().map(ApplicationVersion::id))
+ .orElseThrow(() -> new IllegalStateException("no known prod revisions, but asked for one, for " + application));
}
private static <T extends Comparable<T>> Optional<T> max(Optional<T> o1, Optional<T> o2) {
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/NameServiceForwarder.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/NameServiceForwarder.java
index aecb1e7a2c1..540e8489e6d 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/NameServiceForwarder.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/NameServiceForwarder.java
@@ -1,6 +1,7 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.dns;
+import com.yahoo.transaction.Mutex;
import com.yahoo.vespa.curator.Lock;
import com.yahoo.vespa.hosted.controller.api.integration.dns.AliasTarget;
import com.yahoo.vespa.hosted.controller.api.integration.dns.NameService;
@@ -79,7 +80,7 @@ public class NameServiceForwarder {
}
protected void forward(NameServiceRequest request, NameServiceQueue.Priority priority) {
- try (Lock lock = db.lockNameServiceQueue()) {
+ try (Mutex lock = db.lockNameServiceQueue()) {
NameServiceQueue queue = db.readNameServiceQueue();
var queued = queue.requests().size();
if (queued >= QUEUE_CAPACITY) {
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
index 0e880bb627c..02e1818932e 100644
--- 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
@@ -1,7 +1,6 @@
// Copyright Yahoo. 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.config.provision.zone.ZoneId;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.ApplicationController;
import com.yahoo.vespa.hosted.controller.Controller;
@@ -103,7 +102,7 @@ public class ApplicationOwnershipConfirmer extends ControllerMaintainer {
}
}
return new ApplicationSummary(app.id().defaultInstance(), app.activity().lastQueried(), app.activity().lastWritten(),
- app.latestVersion().flatMap(version -> version.buildTime()), metrics);
+ app.revisions().last().flatMap(version -> version.buildTime()), metrics);
}
/** Escalate ownership issues which have not been closed before a defined amount of time has passed. */
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java
index 7ede040773e..8765884e23c 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java
@@ -7,12 +7,10 @@ import com.yahoo.vespa.flags.ListFlag;
import com.yahoo.vespa.flags.PermanentFlags;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId;
-import com.yahoo.vespa.hosted.controller.application.Deployment;
import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo;
import com.yahoo.vespa.hosted.controller.tenant.Tenant;
import java.time.Duration;
-import java.time.Instant;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
@@ -49,8 +47,7 @@ public class CloudTrialExpirer extends ControllerMaintainer {
private void moveInactiveTenantsToNonePlan() {
var predicate = tenantReadersNotLoggedIn(loginExpiry)
- .and(this::tenantHasTrialPlan)
- .and(this::tenantHasNoDeployments);
+ .and(this::tenantHasTrialPlan);
forTenant("'none' plan", predicate, this::setPlanNone);
}
@@ -63,11 +60,12 @@ public class CloudTrialExpirer extends ControllerMaintainer {
}
private void forTenant(String name, Predicate<Tenant> p, Consumer<List<Tenant>> c) {
- var predicate = ((Predicate<Tenant>) this::tenantIsCloudTenant)
- .and(this::tenantIsNotExemptFromExpiry);
+ var predicate = p.and(this::tenantIsCloudTenant)
+ .and(this::tenantIsNotExemptFromExpiry)
+ .and(this::tenantHasNoDeployments);
var tenants = controller().tenants().asList().stream()
- .filter(predicate.and(p))
+ .filter(predicate)
.collect(Collectors.toList());
if (! tenants.isEmpty()) {
@@ -121,7 +119,15 @@ public class CloudTrialExpirer extends ControllerMaintainer {
private void tombstoneTenants(List<Tenant> tenants) {
tenants.forEach(tenant -> {
+ deleteApplicationsWithNoDeployments(tenant);
controller().tenants().delete(tenant.name(), Optional.empty(), false);
});
}
+
+ private void deleteApplicationsWithNoDeployments(Tenant tenant) {
+ controller().applications().asList(tenant.name()).forEach(application -> {
+ // this only removes applications with no active deployments
+ controller().applications().deleteApplication(application.id(), Optional.empty());
+ });
+ }
}
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 deafcd35e9b..041d0694ca9 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
@@ -8,6 +8,7 @@ import com.yahoo.config.provision.SystemName;
import com.yahoo.config.provision.zone.ZoneApi;
import com.yahoo.jdisc.Metric;
import com.yahoo.vespa.hosted.controller.Controller;
+import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactory;
import com.yahoo.vespa.hosted.controller.api.integration.user.UserManagement;
import java.time.Duration;
@@ -36,7 +37,7 @@ public class ControllerMaintenance extends AbstractComponent {
@Inject
@SuppressWarnings("unused") // instantiated by Dependency Injection
- public ControllerMaintenance(Controller controller, Metric metric, UserManagement userManagement) {
+ public ControllerMaintenance(Controller controller, Metric metric, UserManagement userManagement, AthenzClientFactory athenzClientFactory) {
Intervals intervals = new Intervals(controller.system());
upgrader = new Upgrader(controller, intervals.defaultInterval);
maintainers.add(upgrader);
@@ -44,7 +45,7 @@ public class ControllerMaintenance extends AbstractComponent {
maintainers.add(new DeploymentExpirer(controller, intervals.defaultInterval));
maintainers.add(new DeploymentUpgrader(controller, intervals.defaultInterval));
maintainers.add(new DeploymentIssueReporter(controller, controller.serviceRegistry().deploymentIssues(), intervals.defaultInterval));
- maintainers.add(new MetricsReporter(controller, metric));
+ maintainers.add(new MetricsReporter(controller, metric, athenzClientFactory.createZmsClient()));
maintainers.add(new OutstandingChangeDeployer(controller, intervals.outstandingChangeDeployer));
maintainers.add(new VersionStatusUpdater(controller, intervals.versionStatusUpdater));
maintainers.add(new ReadyJobsTrigger(controller, intervals.readyJobsTrigger));
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirer.java
index 9a8ba9afca2..97f3f955a20 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirer.java
@@ -8,11 +8,9 @@ import com.yahoo.vespa.hosted.controller.Instance;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
import com.yahoo.vespa.hosted.controller.application.Deployment;
-import com.yahoo.vespa.hosted.controller.deployment.Run;
import com.yahoo.yolean.Exceptions;
import java.time.Duration;
-import java.time.Instant;
import java.util.Optional;
import java.util.logging.Level;
@@ -59,13 +57,8 @@ public class DeploymentExpirer extends ControllerMaintainer {
Optional<Duration> ttl = controller().zoneRegistry().getDeploymentTimeToLive(deployment.zone());
if (ttl.isEmpty()) return false;
- Optional<JobId> jobId = JobType.from(controller().system(), deployment.zone())
- .map(type -> new JobId(instance, type));
- if (jobId.isEmpty()) return false;
-
- return controller().jobController().jobStarts(jobId.get()).stream().findFirst()
- .map(start -> start.plus(ttl.get()).isBefore(controller().clock().instant()))
- .orElse(false);
+ return controller().jobController().lastDeploymentStart(instance, deployment)
+ .plus(ttl.get()).isBefore(controller().clock().instant());
}
}
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 3a047d33be5..6b058537c2d 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
@@ -21,7 +21,6 @@ import java.time.Duration;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
-import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentUpgrader.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentUpgrader.java
index a2abd6493cb..c86f79ce188 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentUpgrader.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentUpgrader.java
@@ -19,8 +19,6 @@ import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
-import static com.yahoo.collections.Iterables.reversed;
-
/**
* Upgrades instances in manually deployed zones to the system version, at a convenient time.
*
@@ -49,12 +47,12 @@ public class DeploymentUpgrader extends ControllerMaintainer {
for (Instance instance : application.instances().values())
for (Deployment deployment : instance.deployments().values())
try {
- JobId job = new JobId(instance.id(), JobType.from(controller().system(), deployment.zone()).get());
+ JobId job = new JobId(instance.id(), JobType.deploymentTo(deployment.zone()));
if ( ! deployment.zone().environment().isManuallyDeployed()) continue;
Run last = controller().jobController().last(job).get();
- Versions target = new Versions(targetPlatform, last.versions().targetApplication(), Optional.of(last.versions().targetPlatform()), Optional.of(last.versions().targetApplication()));
- if (last.versions().targetApplication().compileVersion()
+ Versions target = new Versions(targetPlatform, last.versions().targetRevision(), Optional.of(last.versions().targetPlatform()), Optional.of(last.versions().targetRevision()));
+ if (application.revisions().get(last.versions().targetRevision()).compileVersion()
.map(version -> controller().applications().versionCompatibility(instance.id()).refuse(version, target.targetPlatform()))
.orElse(false)) continue;
if ( ! deployment.version().isBefore(target.targetPlatform())) continue;
@@ -62,7 +60,7 @@ public class DeploymentUpgrader extends ControllerMaintainer {
log.log(Level.FINE, "Upgrading deployment of " + instance.id() + " in " + deployment.zone());
attempts.incrementAndGet();
- controller().jobController().start(instance.id(), JobType.from(controller().system(), deployment.zone()).get(), target, true, Optional.of("automated upgrade"));
+ controller().jobController().start(instance.id(), JobType.deploymentTo(deployment.zone()), target, true, Optional.of("automated upgrade"));
} catch (Exception e) {
failures.incrementAndGet();
log.log(Level.WARNING, "Failed upgrading " + deployment + " of " + instance +
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/EndpointCertificateMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/EndpointCertificateMaintainer.java
index 15f8d6380c0..193fb89eb99 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/EndpointCertificateMaintainer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/EndpointCertificateMaintainer.java
@@ -6,6 +6,7 @@ import com.google.inject.Inject;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.container.jdisc.secretstore.SecretNotFoundException;
import com.yahoo.container.jdisc.secretstore.SecretStore;
+import com.yahoo.transaction.Mutex;
import com.yahoo.vespa.curator.Lock;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.Instance;
@@ -82,7 +83,7 @@ public class EndpointCertificateMaintainer extends ControllerMaintainer {
var refreshedCertificateMetadata = endpointCertificateMetadata
.withVersion(latestAvailableVersion.getAsInt())
.withLastRefreshed(clock.instant().getEpochSecond());
- try (Lock lock = lock(applicationId)) {
+ try (Mutex lock = lock(applicationId)) {
if (Optional.of(endpointCertificateMetadata).equals(curator.readEndpointCertificateMetadata(applicationId))) {
curator.writeEndpointCertificateMetadata(applicationId, refreshedCertificateMetadata); // Certificate not validated here, but on deploy.
}
@@ -103,7 +104,7 @@ public class EndpointCertificateMaintainer extends ControllerMaintainer {
controller().applications().getInstance(applicationId)
.ifPresent(instance -> instance.productionDeployments().forEach((zone, deployment) -> {
if (deployment.at().isBefore(refreshTime)) {
- JobType job = JobType.from(controller().system(), zone).orElseThrow();
+ JobType job = JobType.deploymentTo(zone);
deploymentTrigger.reTrigger(applicationId, job, "re-triggered by EndpointCertificateMaintainer");
log.info("Re-triggering deployment job " + job.jobName() + " for instance " +
applicationId.serializedForm() + " to roll out refreshed endpoint certificate");
@@ -128,7 +129,7 @@ public class EndpointCertificateMaintainer extends ControllerMaintainer {
curator.readAllEndpointCertificateMetadata().forEach((applicationId, storedMetaData) -> {
var lastRequested = Instant.ofEpochSecond(storedMetaData.lastRequested());
if (lastRequested.isBefore(oneMonthAgo) && hasNoDeployments(applicationId)) {
- try (Lock lock = lock(applicationId)) {
+ try (Mutex lock = lock(applicationId)) {
if (Optional.of(storedMetaData).equals(curator.readEndpointCertificateMetadata(applicationId))) {
log.log(Level.INFO, "Cert for app " + applicationId.serializedForm()
+ " has not been requested in a month and app has no deployments, deleting from provider and ZK");
@@ -140,7 +141,7 @@ public class EndpointCertificateMaintainer extends ControllerMaintainer {
});
}
- private Lock lock(ApplicationId applicationId) {
+ private Mutex lock(ApplicationId applicationId) {
return curator.lock(TenantAndApplicationId.from(applicationId));
}
@@ -169,7 +170,7 @@ public class EndpointCertificateMaintainer extends ControllerMaintainer {
EndpointCertificateMetadata storedAppMetadata = storedAppEntry.getValue();
if (storedAppMetadata.certName().equals(unknownCertDetails.cert_key_keyname())) {
matchFound = true;
- try (Lock lock = lock(storedApp)) {
+ try (Mutex lock = lock(storedApp)) {
if (Optional.of(storedAppMetadata).equals(curator.readEndpointCertificateMetadata(storedApp))) {
log.log(Level.INFO, "Cert for app " + storedApp.serializedForm()
+ " has a new leafRequestId " + unknownCertDetails.request_id() + ", updating in ZK");
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 6aa43d4db47..294d5bad42d 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
@@ -7,6 +7,7 @@ import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.HostName;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.jdisc.Metric;
+import com.yahoo.vespa.athenz.client.zms.ZmsClient;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.Instance;
@@ -62,17 +63,20 @@ public class MetricsReporter extends ControllerMaintainer {
public static final String REMAINING_ROTATIONS = "remaining_rotations";
public static final String NAME_SERVICE_REQUESTS_QUEUED = "dns.queuedRequests";
public static final String OPERATION_PREFIX = "operation.";
+ public static final String ZMS_QUOTA_USAGE = "zms.quota.usage";
private final Metric metric;
private final Clock clock;
+ private final ZmsClient zmsClient;
// Keep track of reported node counts for each version
private final ConcurrentHashMap<NodeCountKey, Long> nodeCounts = new ConcurrentHashMap<>();
- public MetricsReporter(Controller controller, Metric metric) {
+ public MetricsReporter(Controller controller, Metric metric, ZmsClient zmsClient) {
super(controller, Duration.ofMinutes(1)); // use fixed rate for metrics
this.metric = metric;
this.clock = controller.clock();
+ this.zmsClient = zmsClient;
}
@Override
@@ -85,6 +89,7 @@ public class MetricsReporter extends ControllerMaintainer {
reportAuditLog();
reportBrokenSystemVersion(versionStatus);
reportTenantMetrics();
+ reportZmsQuotaMetrics();
return 1.0;
}
@@ -167,7 +172,7 @@ public class MetricsReporter extends ControllerMaintainer {
});
for (Application application : applications.asList())
- application.latestVersion()
+ application.revisions().last()
.flatMap(ApplicationVersion::buildTime)
.ifPresent(buildTime -> metric.set(DEPLOYMENT_BUILD_AGE_SECONDS,
controller().clock().instant().getEpochSecond() - buildTime.getEpochSecond(),
@@ -252,6 +257,20 @@ public class MetricsReporter extends ControllerMaintainer {
});
}
+ private void reportZmsQuotaMetrics() {
+ var quota = zmsClient.getQuotaUsage();
+ reportZmsQuota("subdomains", quota.getSubdomainUsage());
+ reportZmsQuota("services", quota.getServiceUsage());
+ reportZmsQuota("policies", quota.getPolicyUsage());
+ reportZmsQuota("roles", quota.getRoleUsage());
+ reportZmsQuota("groups", quota.getGroupUsage());
+ }
+
+ private void reportZmsQuota(String resourceType, double usage) {
+ var context = metric.createContext(Map.of("resourceType", resourceType));
+ metric.set(ZMS_QUOTA_USAGE, usage, context);
+ }
+
private Map<NodeVersion, Duration> platformChangeDurations(VersionStatus versionStatus) {
return changeDurations(versionStatus.versions(), VespaVersion::nodeVersions);
}
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 e412a5cc9f7..4ed34a91029 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
@@ -4,6 +4,7 @@ package com.yahoo.vespa.hosted.controller.maintenance;
import com.yahoo.component.Version;
import com.yahoo.config.application.api.DeploymentSpec.UpgradePolicy;
import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.transaction.Mutex;
import com.yahoo.vespa.curator.Lock;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.application.ApplicationList;
@@ -66,7 +67,9 @@ public class Upgrader extends ControllerMaintainer {
/** Returns a list of all production application instances, except those which are pinned, which we should not manipulate here. */
private InstanceList instances(Version systemVersion) {
- return InstanceList.from(controller().jobController().deploymentStatuses(ApplicationList.from(controller().applications().readable()), systemVersion))
+ return InstanceList.from(controller().jobController().deploymentStatuses(ApplicationList.from(controller().applications().readable())
+ .withProjectId(),
+ systemVersion))
.withDeclaredJobs()
.shuffle(random)
.byIncreasingDeployedVersion()
@@ -174,7 +177,7 @@ public class Upgrader extends ControllerMaintainer {
" for version " + version.toFullString() +
": Version may be in use by applications");
}
- try (Lock lock = curator.lockConfidenceOverrides()) {
+ try (Mutex lock = curator.lockConfidenceOverrides()) {
Map<Version, Confidence> overrides = new LinkedHashMap<>(curator.readConfidenceOverrides());
overrides.put(version, confidence);
curator.writeConfidenceOverrides(overrides);
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/UserManagementMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/UserManagementMaintainer.java
index 05a7e2368d1..163a570768f 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/UserManagementMaintainer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/UserManagementMaintainer.java
@@ -3,7 +3,6 @@ package com.yahoo.vespa.hosted.controller.maintenance;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.InstanceName;
-import com.yahoo.config.provision.SystemName;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.api.integration.user.RoleMaintainer;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/VcmrMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/VcmrMaintainer.java
index f7e085bd90f..d0b3b9f4c7f 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/VcmrMaintainer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/VcmrMaintainer.java
@@ -106,6 +106,10 @@ public class VcmrMaintainer extends ControllerMaintainer {
return Status.REQUIRES_OPERATOR_ACTION;
}
+ if (byActionState.getOrDefault(State.OUT_OF_SYNC, 0L) > 0) {
+ return Status.OUT_OF_SYNC;
+ }
+
if (byActionState.getOrDefault(State.RETIRING, 0L) > 0) {
return Status.IN_PROGRESS;
}
@@ -170,6 +174,9 @@ public class VcmrMaintainer extends ControllerMaintainer {
addReport(zoneId, changeRequest, node);
+ if (isOutOfSync(node, hostAction))
+ return hostAction.withState(State.OUT_OF_SYNC);
+
if (isPostponed(changeRequest, hostAction)) {
LOG.fine(() -> changeRequest.getChangeRequestSource().getId() + " is postponed, recycling " + node.hostname());
recycleNode(zoneId, node, hostAction);
@@ -239,7 +246,7 @@ public class VcmrMaintainer extends ControllerMaintainer {
}
private boolean hasRetired(Node node, HostAction hostAction) {
- return hostAction.getState() == State.RETIRING &&
+ return List.of(State.RETIRING, State.REQUIRES_OPERATOR_ACTION).contains(hostAction.getState()) &&
node.state() == Node.State.parked;
}
@@ -248,6 +255,12 @@ public class VcmrMaintainer extends ControllerMaintainer {
&& node.state() == Node.State.active;
}
+ // Determines if node state is unexpected based on previous action taken
+ private boolean isOutOfSync(Node node, HostAction action) {
+ return action.getState() == State.RETIRED && node.state() != Node.State.parked ||
+ action.getState() == State.RETIRING && !node.wantToRetire();
+ }
+
private Map<ZoneId, List<Node>> nodesByZone() {
return controller().zoneRegistry()
.zones()
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/VersionStatusUpdater.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/VersionStatusUpdater.java
index 6597d59027c..71b8f1cd9b7 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/VersionStatusUpdater.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/VersionStatusUpdater.java
@@ -10,11 +10,11 @@ import com.yahoo.yolean.Exceptions;
import java.time.Duration;
import java.util.logging.Level;
+import static com.yahoo.vespa.hosted.controller.api.integration.organization.SystemMonitor.Confidence.aborted;
import static com.yahoo.vespa.hosted.controller.api.integration.organization.SystemMonitor.Confidence.broken;
import static com.yahoo.vespa.hosted.controller.api.integration.organization.SystemMonitor.Confidence.high;
import static com.yahoo.vespa.hosted.controller.api.integration.organization.SystemMonitor.Confidence.low;
import static com.yahoo.vespa.hosted.controller.api.integration.organization.SystemMonitor.Confidence.normal;
-import static com.yahoo.vespa.hosted.controller.api.integration.organization.SystemMonitor.Confidence.aborted;
/**
* This maintenance job periodically updates the version status.
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notification.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notification.java
index c39ee031e27..8a363405c41 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notification.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notification.java
@@ -66,10 +66,16 @@ public class Notification {
}
public enum Type {
- /** Related to contents of application package, e.g. usage of deprecated features/syntax */
+ /** Related to contents of application package, e.g., usage of deprecated features/syntax */
applicationPackage,
- /** Related to deployment of application, e.g. system test failure, node allocation failure, internal errors, etc. */
+ /** Related to contents of application package, e.g., old parent or compile version, or errors detectable on submission */
+ submission,
+
+ /** Related to contents of application test package, e.g., mismatch between deployment spec and provided tests */
+ testPackage,
+
+ /** Related to deployment of application, e.g., system test failure, node allocation failure, internal errors, etc. */
deployment,
/** Application cluster is (near) external feed blocked */
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDb.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDb.java
index aa62028749b..7876099cb21 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDb.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDb.java
@@ -5,6 +5,7 @@ import com.yahoo.collections.Pair;
import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.TenantName;
import com.yahoo.text.Text;
+import com.yahoo.transaction.Mutex;
import com.yahoo.vespa.curator.Lock;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.ClusterMetrics;
@@ -65,7 +66,7 @@ public class NotificationsDb {
*/
public void setNotification(NotificationSource source, Type type, Level level, List<String> messages) {
Optional<Notification> changed = Optional.empty();
- try (Lock lock = curatorDb.lockNotifications(source.tenant())) {
+ try (Mutex lock = curatorDb.lockNotifications(source.tenant())) {
var existingNotifications = curatorDb.readNotifications(source.tenant());
List<Notification> notifications = existingNotifications.stream()
.filter(notification -> !source.equals(notification.source()) || type != notification.type())
@@ -82,7 +83,7 @@ public class NotificationsDb {
/** Remove the notification with the given source and type */
public void removeNotification(NotificationSource source, Type type) {
- try (Lock lock = curatorDb.lockNotifications(source.tenant())) {
+ try (Mutex lock = curatorDb.lockNotifications(source.tenant())) {
List<Notification> initial = curatorDb.readNotifications(source.tenant());
List<Notification> filtered = initial.stream()
.filter(notification -> !source.equals(notification.source()) || type != notification.type())
@@ -94,7 +95,7 @@ public class NotificationsDb {
/** Remove all notifications for this source or sources contained by this source */
public void removeNotifications(NotificationSource source) {
- try (Lock lock = curatorDb.lockNotifications(source.tenant())) {
+ try (Mutex lock = curatorDb.lockNotifications(source.tenant())) {
if (source.application().isEmpty()) { // Source is tenant
curatorDb.deleteNotifications(source.tenant());
return;
@@ -130,7 +131,7 @@ public class NotificationsDb {
.collect(Collectors.toUnmodifiableList());
NotificationSource deploymentSource = NotificationSource.from(deploymentId);
- try (Lock lock = curatorDb.lockNotifications(deploymentSource.tenant())) {
+ try (Mutex lock = curatorDb.lockNotifications(deploymentSource.tenant())) {
List<Notification> initial = curatorDb.readNotifications(deploymentSource.tenant());
List<Notification> updated = Stream.concat(
initial.stream()
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notify/Notifier.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notify/Notifier.java
index 594908a3bc1..6b14872b07d 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notify/Notifier.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notify/Notifier.java
@@ -1,16 +1,21 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.notify;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Environment;
import com.yahoo.text.Text;
+import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
import com.yahoo.vespa.hosted.controller.api.integration.organization.Mail;
import com.yahoo.vespa.hosted.controller.api.integration.organization.Mailer;
import com.yahoo.vespa.hosted.controller.api.integration.organization.MailerException;
+import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry;
import com.yahoo.vespa.hosted.controller.notification.Notification;
import com.yahoo.vespa.hosted.controller.notification.NotificationSource;
import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
import com.yahoo.vespa.hosted.controller.tenant.TenantContacts;
+import java.net.URI;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
@@ -25,12 +30,14 @@ import java.util.stream.Collectors;
*/
public class Notifier {
private final CuratorDb curatorDb;
+ private final ZoneRegistry zoneRegistry;
private final Mailer mailer;
private static final Logger log = Logger.getLogger(Notifier.class.getName());
- public Notifier(CuratorDb curatorDb, Mailer mailer) {
+ public Notifier(CuratorDb curatorDb, ZoneRegistry zoneRegistry, Mailer mailer) {
this.curatorDb = Objects.requireNonNull(curatorDb);
+ this.zoneRegistry = Objects.requireNonNull(zoneRegistry);
this.mailer = Objects.requireNonNull(mailer);
}
@@ -56,7 +63,12 @@ public class Notifier {
private boolean skipSource(NotificationSource source) {
// Limit sources to production systems only. Dev and test systems cause too much noise at the moment.
- return source.jobType().map(t -> !t.isProduction()).orElse(false);
+ if (source.zoneId().map(z -> z.environment() != Environment.prod).orElse(false)) {
+ return true;
+ } else if (source.jobType().map(t -> !t.isProduction()).orElse(false)) {
+ return true;
+ }
+ return false;
}
public void dispatch(Notification notification) {
@@ -82,12 +94,41 @@ public class Notifier {
}
private Mail mailOf(Notification n, Collection<String> recipients) {
- var subject = Text.format("[%s] Vespa Notification for %s", n.level().toString().toUpperCase(), n.type().name());
+ var source = n.source();
+ var subject = Text.format("[%s] %s Vespa Notification for %s", n.level().toString().toUpperCase(), n.type().name(), applicationIdSource(source));
var body = new StringBuilder();
body.append("Source: ").append(n.source().toString()).append("\n")
.append("\n")
- .append(String.join("\n", n.messages()));
- return new Mail(recipients, subject.toString(), body.toString());
+ .append(String.join("\n", n.messages()))
+ .append("\n")
+ .append(url(source).toString());
+ return new Mail(recipients, subject, body.toString());
+ }
+
+ private String applicationIdSource(NotificationSource source) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(source.tenant().value());
+ source.application().ifPresent(applicationName -> sb.append(".").append(applicationName.value()));
+ source.instance().ifPresent(instanceName -> sb.append(".").append(instanceName.value()));
+ return sb.toString();
+ }
+
+ private URI url(NotificationSource source) {
+ if (source.application().isPresent()) {
+ if (source.instance().isPresent()) {
+ if (source.jobType().isPresent() && source.runNumber().isPresent()) {
+ return zoneRegistry.dashboardUrl(
+ new RunId(ApplicationId.from(source.tenant(),
+ source.application().get(),
+ source.instance().get()),
+ source.jobType().get(),
+ source.runNumber().getAsLong()));
+ }
+ return zoneRegistry.dashboardUrl(ApplicationId.from(source.tenant(), source.application().get(), source.instance().get()));
+ }
+ return zoneRegistry.dashboardUrl(source.tenant(), source.application().get());
+ }
+ return zoneRegistry.dashboardUrl(source.tenant());
}
}
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 dee3c822465..48d8627f407 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
@@ -4,9 +4,11 @@ package com.yahoo.vespa.hosted.controller.persistence;
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.InstanceName;
import com.yahoo.config.provision.RegionName;
+import com.yahoo.config.provision.SystemName;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.security.KeyUtils;
import com.yahoo.slime.ArrayTraverser;
@@ -18,7 +20,9 @@ import com.yahoo.slime.SlimeUtils;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.Instance;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
+import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
+import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.SourceRevision;
import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
import com.yahoo.vespa.hosted.controller.api.integration.organization.User;
@@ -30,6 +34,7 @@ import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics;
import com.yahoo.vespa.hosted.controller.application.EndpointId;
import com.yahoo.vespa.hosted.controller.application.QuotaUsage;
import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
+import com.yahoo.vespa.hosted.controller.deployment.RevisionHistory;
import com.yahoo.vespa.hosted.controller.metric.ApplicationMetrics;
import com.yahoo.vespa.hosted.controller.routing.rotation.RotationId;
import com.yahoo.vespa.hosted.controller.routing.rotation.RotationState;
@@ -49,8 +54,6 @@ import java.util.Optional;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.Set;
-import java.util.SortedSet;
-import java.util.TreeSet;
/**
* Serializes {@link Application}s to/from slime.
@@ -76,8 +79,9 @@ public class ApplicationSerializer {
private static final String instancesField = "instances";
private static final String deployingField = "deployingField";
private static final String projectIdField = "projectId";
- private static final String latestVersionField = "latestVersion";
private static final String versionsField = "versions";
+ private static final String prodVersionsField = "prodVersions";
+ private static final String devVersionsField = "devVersions";
private static final String pinnedField = "pinned";
private static final String deploymentIssueField = "deploymentIssueId";
private static final String ownershipIssueIdField = "ownershipIssueId";
@@ -97,7 +101,6 @@ public class ApplicationSerializer {
private static final String deploymentJobsField = "deploymentJobs"; // TODO jonmv: clean up serialisation format
private static final String assignedRotationsField = "assignedRotations";
private static final String assignedRotationEndpointField = "endpointId";
- private static final String latestDeployedField = "latestDeployed";
// Deployment fields
private static final String zoneField = "zone";
@@ -110,8 +113,12 @@ public class ApplicationSerializer {
private static final String repositoryField = "repositoryField";
private static final String branchField = "branchField";
private static final String commitField = "commitField";
+ private static final String descriptionField = "description";
+ private static final String riskField = "risk";
private static final String authorEmailField = "authorEmailField";
private static final String deployedDirectlyField = "deployedDirectly";
+ private static final String hasPackageField = "hasPackage";
+ private static final String shouldSkipField = "shouldSkip";
private static final String compileVersionField = "compileVersion";
private static final String buildTimeField = "buildTime";
private static final String sourceUrlField = "sourceUrl";
@@ -167,8 +174,7 @@ public class ApplicationSerializer {
root.setDouble(queryQualityField, application.metrics().queryServiceQuality());
root.setDouble(writeQualityField, application.metrics().writeServiceQuality());
deployKeysToSlime(application.deployKeys(), root.setArray(pemDeployKeysField));
- application.latestVersion().ifPresent(version -> toSlime(version, root.setObject(latestVersionField)));
- versionsToSlime(application, root.setArray(versionsField));
+ revisionsToSlime(application.revisions(), root.setArray(prodVersionsField), root.setArray(devVersionsField));
instancesToSlime(application, root.setArray(instancesField));
return slime;
}
@@ -182,7 +188,6 @@ public class ApplicationSerializer {
assignedRotationsToSlime(instance.rotations(), instanceObject);
toSlime(instance.rotationStatus(), instanceObject.setArray(rotationStatusField));
toSlime(instance.change(), instanceObject, deployingField);
- instance.latestDeployed().ifPresent(version -> toSlime(version, instanceObject.setObject(latestDeployedField)));
}
}
@@ -199,7 +204,7 @@ public class ApplicationSerializer {
zoneIdToSlime(deployment.zone(), object.setObject(zoneField));
object.setString(versionField, deployment.version().toString());
object.setLong(deployTimeField, deployment.at().toEpochMilli());
- toSlime(deployment.applicationVersion(), object.setObject(applicationPackageRevisionField));
+ toSlime(deployment.revision(), object.setObject(applicationPackageRevisionField));
deploymentMetricsToSlime(deployment.metrics(), object);
deployment.activity().lastQueried().ifPresent(instant -> object.setLong(lastQueriedField, instant.toEpochMilli()));
deployment.activity().lastWritten().ifPresent(instant -> object.setLong(lastWrittenField, instant.toEpochMilli()));
@@ -228,8 +233,23 @@ public class ApplicationSerializer {
object.setString(regionField, zone.region().value());
}
- private void versionsToSlime(Application application, Cursor object) {
- application.versions().forEach(version -> toSlime(version, object.addObject()));
+ private void revisionsToSlime(RevisionHistory revisions, Cursor revisionsArray, Cursor devRevisionsArray) {
+ revisionsToSlime(revisions.production(), revisionsArray);
+ revisions.development().forEach((job, devRevisions) -> {
+ Cursor devRevisionsObject = devRevisionsArray.addObject();
+ devRevisionsObject.setString(instanceNameField, job.application().instance().value());
+ devRevisionsObject.setString(jobTypeField, job.type().serialized());
+ revisionsToSlime(devRevisions, devRevisionsObject.setArray(versionsField));
+ });
+ }
+
+ private void revisionsToSlime(Iterable<ApplicationVersion> revisions, Cursor revisionsArray) {
+ revisions.forEach(version -> toSlime(version, revisionsArray.addObject()));
+ }
+
+ private void toSlime(RevisionId revision, Cursor object) {
+ object.setLong(applicationBuildNumberField, revision.number());
+ object.setBool(deployedDirectlyField, ! revision.isProduction());
}
private void toSlime(ApplicationVersion applicationVersion, Cursor object) {
@@ -241,6 +261,10 @@ public class ApplicationSerializer {
applicationVersion.sourceUrl().ifPresent(url -> object.setString(sourceUrlField, url));
applicationVersion.commit().ifPresent(commit -> object.setString(commitField, commit));
object.setBool(deployedDirectlyField, applicationVersion.isDeployedDirectly());
+ object.setBool(hasPackageField, applicationVersion.hasPackage());
+ object.setBool(shouldSkipField, applicationVersion.shouldSkip());
+ applicationVersion.description().ifPresent(description -> object.setString(descriptionField, description));
+ if (applicationVersion.risk() != 0) object.setLong(riskField, applicationVersion.risk());
applicationVersion.bundleHash().ifPresent(bundleHash -> object.setString(bundleHashField, bundleHash));
}
@@ -254,7 +278,7 @@ public class ApplicationSerializer {
Cursor jobStatusArray = cursor.setArray(jobStatusField);
jobPauses.forEach((type, until) -> {
Cursor jobPauseObject = jobStatusArray.addObject();
- jobPauseObject.setString(jobTypeField, type.jobName());
+ jobPauseObject.setString(jobTypeField, type.serialized());
jobPauseObject.setLong(pausedUntilField, until.toEpochMilli());
});
}
@@ -265,8 +289,8 @@ public class ApplicationSerializer {
Cursor object = parentObject.setObject(fieldName);
if (deploying.platform().isPresent())
object.setString(versionField, deploying.platform().get().toString());
- if (deploying.application().isPresent())
- toSlime(deploying.application().get(), object);
+ if (deploying.revision().isPresent())
+ toSlime(deploying.revision().get(), object);
if (deploying.isPinned())
object.setBool(pinnedField, true);
}
@@ -321,42 +345,50 @@ public class ApplicationSerializer {
Set<PublicKey> deployKeys = deployKeysFromSlime(root.field(pemDeployKeysField));
List<Instance> instances = instancesFromSlime(id, root.field(instancesField));
OptionalLong projectId = SlimeUtils.optionalLong(root.field(projectIdField));
- Optional<ApplicationVersion> latestVersion = latestVersionFromSlime(root.field(latestVersionField));
- SortedSet<ApplicationVersion> versions = versionsFromSlime(root.field(versionsField));
+ RevisionHistory revisions = revisionsFromSlime(root.field(prodVersionsField), root.field(devVersionsField), id);
return new Application(id, createdAt, deploymentSpec, validationOverrides,
deploymentIssueId, ownershipIssueId, owner, majorVersion, metrics,
- deployKeys, projectId, latestVersion, versions, instances);
+ deployKeys, projectId, revisions, instances);
}
- private Optional<ApplicationVersion> latestVersionFromSlime(Inspector latestVersionObject) {
- return Optional.of(applicationVersionFromSlime(latestVersionObject))
- .filter(version -> ! version.isUnknown());
+ private RevisionHistory revisionsFromSlime(Inspector prodVersionsArray, Inspector devVersionsArray, TenantAndApplicationId id) {
+ List<ApplicationVersion> revisions = revisionsFromSlime(prodVersionsArray, null);
+ Map<JobId, List<ApplicationVersion>> devRevisions = new HashMap<>();
+ devVersionsArray.traverse((ArrayTraverser) (__, devRevisionsObject) -> {
+ JobId job = jobIdFromSlime(id, devRevisionsObject);
+ devRevisions.put(job, revisionsFromSlime(devRevisionsObject.field(versionsField), job));
+ });
+
+ return RevisionHistory.ofRevisions(revisions, devRevisions);
}
- private SortedSet<ApplicationVersion> versionsFromSlime(Inspector versionsObject) {
- SortedSet<ApplicationVersion> versions = new TreeSet<>();
- versionsObject.traverse((ArrayTraverser) (name, object) -> versions.add(applicationVersionFromSlime(object)));
- return versions;
+ private JobId jobIdFromSlime(TenantAndApplicationId base, Inspector idObject) {
+ return new JobId(base.instance(idObject.field(instanceNameField).asString()),
+ JobType.ofSerialized(idObject.field(jobTypeField).asString()));
+ }
+
+ private List<ApplicationVersion> revisionsFromSlime(Inspector versionsArray, JobId job) {
+ List<ApplicationVersion> revisions = new ArrayList<>();
+ versionsArray.traverse((ArrayTraverser) (__, revisionObject) -> revisions.add(applicationVersionFromSlime(revisionObject, job)));
+ return revisions;
}
private List<Instance> instancesFromSlime(TenantAndApplicationId id, Inspector field) {
List<Instance> instances = new ArrayList<>();
field.traverse((ArrayTraverser) (name, object) -> {
InstanceName instanceName = InstanceName.from(object.field(instanceNameField).asString());
- List<Deployment> deployments = deploymentsFromSlime(object.field(deploymentsField));
+ List<Deployment> deployments = deploymentsFromSlime(object.field(deploymentsField), id.instance(instanceName));
Map<JobType, Instant> jobPauses = jobPausesFromSlime(object.field(deploymentJobsField));
List<AssignedRotation> assignedRotations = assignedRotationsFromSlime(object);
RotationStatus rotationStatus = rotationStatusFromSlime(object);
Change change = changeFromSlime(object.field(deployingField));
- Optional<ApplicationVersion> latestDeployed = latestVersionFromSlime(object.field(latestDeployedField));
instances.add(new Instance(id.instance(instanceName),
deployments,
jobPauses,
assignedRotations,
rotationStatus,
- change,
- latestDeployed));
+ change));
});
return instances;
}
@@ -367,15 +399,16 @@ public class ApplicationSerializer {
return keys;
}
- private List<Deployment> deploymentsFromSlime(Inspector array) {
+ private List<Deployment> deploymentsFromSlime(Inspector array, ApplicationId id) {
List<Deployment> deployments = new ArrayList<>();
- array.traverse((ArrayTraverser) (int i, Inspector item) -> deployments.add(deploymentFromSlime(item)));
+ array.traverse((ArrayTraverser) (int i, Inspector item) -> deployments.add(deploymentFromSlime(item, id)));
return deployments;
}
- private Deployment deploymentFromSlime(Inspector deploymentObject) {
- return new Deployment(zoneIdFromSlime(deploymentObject.field(zoneField)),
- applicationVersionFromSlime(deploymentObject.field(applicationPackageRevisionField)),
+ private Deployment deploymentFromSlime(Inspector deploymentObject, ApplicationId id) {
+ ZoneId zone = zoneIdFromSlime(deploymentObject.field(zoneField));
+ return new Deployment(zone,
+ revisionFromSlime(deploymentObject.field(applicationPackageRevisionField), new JobId(id, JobType.deploymentTo(zone))),
Version.fromString(deploymentObject.field(versionField).asString()),
SlimeUtils.instant(deploymentObject.field(deployTimeField)),
deploymentMetricsFromSlime(deploymentObject.field(deploymentMetricsField)),
@@ -432,22 +465,30 @@ public class ApplicationSerializer {
return ZoneId.from(object.field(environmentField).asString(), object.field(regionField).asString());
}
- private ApplicationVersion applicationVersionFromSlime(Inspector object) {
- if ( ! object.valid()) return ApplicationVersion.unknown;
- OptionalLong applicationBuildNumber = SlimeUtils.optionalLong(object.field(applicationBuildNumberField));
- if (applicationBuildNumber.isEmpty())
- return ApplicationVersion.unknown;
+ private RevisionId revisionFromSlime(Inspector object, JobId job) {
+ long build = object.field(applicationBuildNumberField).asLong();
+ boolean production = object.field(deployedDirectlyField).valid() // TODO jonmv: remove after migration
+ && build > 0
+ && ! object.field(deployedDirectlyField).asBool();
+ return production ? RevisionId.forProduction(build) : RevisionId.forDevelopment(build, job);
+ }
+ private ApplicationVersion applicationVersionFromSlime(Inspector object, JobId job) {
+ RevisionId id = revisionFromSlime(object, job);
Optional<SourceRevision> sourceRevision = sourceRevisionFromSlime(object.field(sourceRevisionField));
Optional<String> authorEmail = SlimeUtils.optionalString(object.field(authorEmailField));
Optional<Version> compileVersion = SlimeUtils.optionalString(object.field(compileVersionField)).map(Version::fromString);
Optional<Instant> buildTime = SlimeUtils.optionalInstant(object.field(buildTimeField));
Optional<String> sourceUrl = SlimeUtils.optionalString(object.field(sourceUrlField));
Optional<String> commit = SlimeUtils.optionalString(object.field(commitField));
- boolean deployedDirectly = object.field(deployedDirectlyField).asBool();
+ boolean hasPackage = object.field(hasPackageField).asBool();
+ boolean shouldSkip = object.field(shouldSkipField).asBool();
+ Optional<String> description = SlimeUtils.optionalString(object.field(descriptionField));
+ int risk = (int) object.field(riskField).asLong();
Optional<String> bundleHash = SlimeUtils.optionalString(object.field(bundleHashField));
- return new ApplicationVersion(sourceRevision, applicationBuildNumber, authorEmail, compileVersion, buildTime, sourceUrl, commit, deployedDirectly, bundleHash);
+ return new ApplicationVersion(id, sourceRevision, authorEmail, compileVersion, buildTime, sourceUrl,
+ commit, bundleHash, hasPackage, shouldSkip, description, risk);
}
private Optional<SourceRevision> sourceRevisionFromSlime(Inspector object) {
@@ -460,9 +501,8 @@ public class ApplicationSerializer {
private Map<JobType, Instant> jobPausesFromSlime(Inspector object) {
Map<JobType, Instant> jobPauses = new HashMap<>();
object.field(jobStatusField).traverse((ArrayTraverser) (__, jobPauseObject) ->
- JobType.fromOptionalJobName(jobPauseObject.field(jobTypeField).asString())
- .ifPresent(jobType -> jobPauses.put(jobType,
- SlimeUtils.instant(jobPauseObject.field(pausedUntilField)))));
+ jobPauses.put(JobType.ofSerialized(jobPauseObject.field(jobTypeField).asString()),
+ SlimeUtils.instant(jobPauseObject.field(pausedUntilField))));
return jobPauses;
}
@@ -473,7 +513,7 @@ public class ApplicationSerializer {
if (versionFieldValue.valid())
change = Change.of(Version.fromString(versionFieldValue.asString()));
if (object.field(applicationBuildNumberField).valid())
- change = change.with(applicationVersionFromSlime(object));
+ change = change.with(revisionFromSlime(object, null));
if (object.field(pinnedField).asBool())
change = change.withPin();
return change;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/BufferedLogStore.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/BufferedLogStore.java
index 059eb37bb59..9721026c628 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/BufferedLogStore.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/BufferedLogStore.java
@@ -2,10 +2,10 @@
package com.yahoo.vespa.hosted.controller.persistence;
import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.vespa.hosted.controller.api.integration.LogEntry;
import com.yahoo.vespa.hosted.controller.api.integration.RunDataStore;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
-import com.yahoo.vespa.hosted.controller.api.integration.LogEntry;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.TestReport;
import com.yahoo.vespa.hosted.controller.deployment.RunLog;
import com.yahoo.vespa.hosted.controller.deployment.Step;
@@ -110,10 +110,8 @@ public class BufferedLogStore {
store.delete(id);
}
- /** Deletes all logs for the given application. */
+ /** Deletes all logs in permanent storage for the given application. */
public void delete(ApplicationId id) {
- for (JobType type : JobType.values())
- buffer.deleteLog(id, type);
store.delete(id);
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ControllerVersionSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ControllerVersionSerializer.java
index 8b599b45558..1ec349b7dab 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ControllerVersionSerializer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ControllerVersionSerializer.java
@@ -4,10 +4,10 @@ package com.yahoo.vespa.hosted.controller.persistence;
import com.yahoo.component.Version;
import com.yahoo.slime.Slime;
import com.yahoo.slime.SlimeUtils;
-import com.yahoo.vespa.hosted.controller.versions.ControllerVersion;
+import com.yahoo.vespa.hosted.controller.api.identifiers.ControllerVersion;
/**
- * Serializer for {@link com.yahoo.vespa.hosted.controller.versions.ControllerVersion}.
+ * Serializer for {@link ControllerVersion}.
*
* @author mpolden
*/
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 84ff3d5d8c3..c3c68f7596f 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
@@ -7,19 +7,18 @@ import com.yahoo.component.Version;
import com.yahoo.concurrent.UncheckedTimeoutException;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.HostName;
+import com.yahoo.config.provision.SystemName;
import com.yahoo.config.provision.TenantName;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.path.Path;
import com.yahoo.slime.Slime;
import com.yahoo.slime.SlimeUtils;
+import com.yahoo.transaction.Mutex;
import com.yahoo.vespa.curator.Curator;
-import com.yahoo.vespa.curator.Lock;
-import com.yahoo.vespa.curator.MultiplePathsLock;
-import com.yahoo.vespa.flags.FlagSource;
-import com.yahoo.vespa.flags.Flags;
-import com.yahoo.vespa.flags.StringFlag;
import com.yahoo.vespa.hosted.controller.Application;
+import com.yahoo.vespa.hosted.controller.api.identifiers.ControllerVersion;
import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
+import com.yahoo.vespa.hosted.controller.api.integration.ServiceRegistry;
import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveBucket;
import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateMetadata;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
@@ -38,12 +37,10 @@ import com.yahoo.vespa.hosted.controller.routing.RoutingStatus;
import com.yahoo.vespa.hosted.controller.routing.ZoneRoutingPolicy;
import com.yahoo.vespa.hosted.controller.support.access.SupportAccess;
import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-import com.yahoo.vespa.hosted.controller.versions.ControllerVersion;
import com.yahoo.vespa.hosted.controller.versions.OsVersionStatus;
import com.yahoo.vespa.hosted.controller.versions.OsVersionTarget;
import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
-
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
@@ -103,8 +100,6 @@ public class CuratorDb {
private final ControllerVersionSerializer controllerVersionSerializer = new ControllerVersionSerializer();
private final ConfidenceOverrideSerializer confidenceOverrideSerializer = new ConfidenceOverrideSerializer();
private final TenantSerializer tenantSerializer = new TenantSerializer();
- private final ApplicationSerializer applicationSerializer = new ApplicationSerializer();
- private final RunSerializer runSerializer = new RunSerializer();
private final OsVersionSerializer osVersionSerializer = new OsVersionSerializer();
private final OsVersionTargetSerializer osVersionTargetSerializer = new OsVersionTargetSerializer(osVersionSerializer);
private final OsVersionStatusSerializer osVersionStatusSerializer = new OsVersionStatusSerializer(osVersionSerializer, nodeVersionSerializer);
@@ -112,10 +107,13 @@ public class CuratorDb {
private final ZoneRoutingPolicySerializer zoneRoutingPolicySerializer = new ZoneRoutingPolicySerializer(routingPolicySerializer);
private final AuditLogSerializer auditLogSerializer = new AuditLogSerializer();
private final NameServiceQueueSerializer nameServiceQueueSerializer = new NameServiceQueueSerializer();
+ private final ApplicationSerializer applicationSerializer = new ApplicationSerializer();
+ private final RunSerializer runSerializer = new RunSerializer();
+ private final RetriggerEntrySerializer retriggerEntrySerializer = new RetriggerEntrySerializer();
+ private final NotificationsSerializer notificationsSerializer = new NotificationsSerializer();
private final Curator curator;
private final Duration tryLockTimeout;
- private final StringFlag lockScheme;
// For each application id (path), store the ZK node version and its deserialised data - update when version changes.
// This will grow to keep all applications in memory, but this should be OK
@@ -125,14 +123,13 @@ public class CuratorDb {
private final Map<Path, Pair<Integer, NavigableMap<RunId, Run>>> cachedHistoricRuns = new ConcurrentHashMap<>();
@Inject
- public CuratorDb(Curator curator, FlagSource flagSource) {
- this(curator, defaultTryLockTimeout, flagSource);
+ public CuratorDb(Curator curator, ServiceRegistry services) {
+ this(curator, defaultTryLockTimeout, services.zoneRegistry().system());
}
- CuratorDb(Curator curator, Duration tryLockTimeout, FlagSource flagSource) {
+ CuratorDb(Curator curator, Duration tryLockTimeout, SystemName system) {
this.curator = curator;
this.tryLockTimeout = tryLockTimeout;
- this.lockScheme = Flags.CONTROLLER_LOCK_SCHEME.bindTo(flagSource);
}
/** Returns all hostnames configured to be part of this ZooKeeper cluster */
@@ -145,71 +142,35 @@ public class CuratorDb {
// -------------- Locks ---------------------------------------------------
- public Lock lock(TenantName name) {
+ public Mutex lock(TenantName name) {
return curator.lock(lockPath(name), defaultLockTimeout.multipliedBy(2));
}
- public Lock lock(TenantAndApplicationId id) {
- switch (lockScheme.value()) {
- case "BOTH":
- return new MultiplePathsLock(lockPath(id), legacyLockPath(id), defaultLockTimeout.multipliedBy(2), curator);
- case "OLD":
- return curator.lock(legacyLockPath(id), defaultLockTimeout.multipliedBy(2));
- case "NEW":
- return curator.lock(lockPath(id), defaultLockTimeout.multipliedBy(2));
- default:
- throw new IllegalArgumentException("Unknown lock scheme " + lockScheme.value());
- }
+ public Mutex lock(TenantAndApplicationId id) {
+ return curator.lock(lockPath(id), defaultLockTimeout.multipliedBy(2));
}
- public Lock lockForDeployment(ApplicationId id, ZoneId zone) {
- switch (lockScheme.value()) {
- case "BOTH":
- return new MultiplePathsLock(lockPath(id, zone), legacyLockPath(id, zone), deployLockTimeout, curator);
- case "OLD":
- return curator.lock(legacyLockPath(id, zone), deployLockTimeout);
- case "NEW":
- return curator.lock(lockPath(id, zone), deployLockTimeout);
- default:
- throw new IllegalArgumentException("Unknown lock scheme " + lockScheme.value());
- }
+ public Mutex lockForDeployment(ApplicationId id, ZoneId zone) {
+ return curator.lock(lockPath(id, zone), deployLockTimeout);
}
- public Lock lock(ApplicationId id, JobType type) {
- switch (lockScheme.value()) {
- case "BOTH":
- return new MultiplePathsLock(lockPath(id, type), legacyLockPath(id, type), defaultLockTimeout, curator);
- case "OLD":
- return curator.lock(legacyLockPath(id, type), defaultLockTimeout);
- case "NEW":
- return curator.lock(lockPath(id, type), defaultLockTimeout);
- default:
- throw new IllegalArgumentException("Unknown lock scheme " + lockScheme.value());
- }
+ public Mutex lock(ApplicationId id, JobType type) {
+ return curator.lock(lockPath(id, type), defaultLockTimeout);
}
- public Lock lock(ApplicationId id, JobType type, Step step) throws TimeoutException {
- switch (lockScheme.value()) {
- case "BOTH":
- return tryLock(lockPath(id, type, step), legacyLockPath(id, type, step));
- case "OLD":
- return tryLock(legacyLockPath(id, type, step));
- case "NEW":
- return tryLock(lockPath(id, type, step));
- default:
- throw new IllegalArgumentException("Unknown lock scheme " + lockScheme.value());
- }
+ public Mutex lock(ApplicationId id, JobType type, Step step) throws TimeoutException {
+ return tryLock(lockPath(id, type, step));
}
- public Lock lockRotations() {
+ public Mutex lockRotations() {
return curator.lock(lockRoot.append("rotations"), defaultLockTimeout);
}
- public Lock lockConfidenceOverrides() {
+ public Mutex lockConfidenceOverrides() {
return curator.lock(lockRoot.append("confidenceOverrides"), defaultLockTimeout);
}
- public Lock lockMaintenanceJob(String jobName) {
+ public Mutex lockMaintenanceJob(String jobName) {
try {
return tryLock(lockRoot.append("maintenanceJobLocks").append(jobName));
} catch (TimeoutException e) {
@@ -217,52 +178,51 @@ public class CuratorDb {
}
}
- @SuppressWarnings("unused") // Called by internal code
- public Lock lockProvisionState(String provisionStateId) {
+ public Mutex lockProvisionState(String provisionStateId) {
return curator.lock(lockPath(provisionStateId), Duration.ofSeconds(1));
}
- public Lock lockOsVersions() {
+ public Mutex lockOsVersions() {
return curator.lock(lockRoot.append("osTargetVersion"), defaultLockTimeout);
}
- public Lock lockOsVersionStatus() {
+ public Mutex lockOsVersionStatus() {
return curator.lock(lockRoot.append("osVersionStatus"), defaultLockTimeout);
}
- public Lock lockRoutingPolicies() {
+ public Mutex lockRoutingPolicies() {
return curator.lock(lockRoot.append("routingPolicies"), defaultLockTimeout);
}
- public Lock lockAuditLog() {
+ public Mutex lockAuditLog() {
return curator.lock(lockRoot.append("auditLog"), defaultLockTimeout);
}
- public Lock lockNameServiceQueue() {
+ public Mutex lockNameServiceQueue() {
return curator.lock(lockRoot.append("nameServiceQueue"), defaultLockTimeout);
}
- public Lock lockMeteringRefreshTime() throws TimeoutException {
+ public Mutex lockMeteringRefreshTime() throws TimeoutException {
return tryLock(lockRoot.append("meteringRefreshTime"));
}
- public Lock lockArchiveBuckets(ZoneId zoneId) {
+ public Mutex lockArchiveBuckets(ZoneId zoneId) {
return curator.lock(lockRoot.append("archiveBuckets").append(zoneId.value()), defaultLockTimeout);
}
- public Lock lockChangeRequests() {
+ public Mutex lockChangeRequests() {
return curator.lock(lockRoot.append("changeRequests"), defaultLockTimeout);
}
- public Lock lockNotifications(TenantName tenantName) {
+ public Mutex lockNotifications(TenantName tenantName) {
return curator.lock(lockRoot.append("notifications").append(tenantName.value()), defaultLockTimeout);
}
- public Lock lockSupportAccess(DeploymentId deploymentId) {
+ public Mutex lockSupportAccess(DeploymentId deploymentId) {
return curator.lock(lockRoot.append("supportAccess").append(deploymentId.dottedString()), defaultLockTimeout);
}
- public Lock lockDeploymentRetriggerQueue() {
+ public Mutex lockDeploymentRetriggerQueue() {
return curator.lock(lockRoot.append("deploymentRetriggerQueue"), defaultLockTimeout);
}
@@ -272,7 +232,7 @@ public class CuratorDb {
*
* Useful for maintenance jobs, where there is no point in running the jobs back to back.
*/
- private Lock tryLock(Path path) throws TimeoutException {
+ private Mutex tryLock(Path path) throws TimeoutException {
try {
return curator.lock(path, tryLockTimeout);
}
@@ -281,19 +241,6 @@ public class CuratorDb {
}
}
- /** Try locking with a low timeout, meaning it is OK to fail lock acquisition.
- *
- * Useful for maintenance jobs, where there is no point in running the jobs back to back.
- */
- private Lock tryLock(Path path, Path path2) throws TimeoutException {
- try {
- return new MultiplePathsLock(path, path2, tryLockTimeout, curator);
- }
- catch (UncheckedTimeoutException e) {
- throw new TimeoutException(e.getMessage());
- }
- }
-
private <T> Optional<T> read(Path path, Function<byte[], T> mapper) {
return curator.getData(path).filter(data -> data.length > 0).map(mapper);
}
@@ -383,7 +330,7 @@ public class CuratorDb {
}
public Optional<Tenant> readTenant(TenantName name) {
- return readSlime(tenantPath(name)).map(bytes -> tenantSerializer.tenantFrom(bytes));
+ return readSlime(tenantPath(name)).map(tenantSerializer::tenantFrom);
}
public List<Tenant> readTenants() {
@@ -415,7 +362,7 @@ public class CuratorDb {
.map(stat -> cachedApplications.compute(path, (__, old) ->
old != null && old.getFirst() == stat.getVersion()
? old
- : new Pair<>(stat.getVersion(), read(path, applicationSerializer::fromSlime).get())).getSecond());
+ : new Pair<>(stat.getVersion(), read(path, bytes -> applicationSerializer.fromSlime(bytes)).get())).getSecond());
}
public List<Application> readApplications(boolean canFail) {
@@ -676,7 +623,7 @@ public class CuratorDb {
public List<Notification> readNotifications(TenantName tenantName) {
return readSlime(notificationsPath(tenantName))
- .map(slime -> NotificationsSerializer.fromSlime(tenantName, slime)).orElseGet(List::of);
+ .map(slime -> notificationsSerializer.fromSlime(tenantName, slime)).orElseGet(List::of);
}
@@ -687,7 +634,7 @@ public class CuratorDb {
}
public void writeNotifications(TenantName tenantName, List<Notification> notifications) {
- curator.set(notificationsPath(tenantName), asJson(NotificationsSerializer.toSlime(notifications)));
+ curator.set(notificationsPath(tenantName), asJson(notificationsSerializer.toSlime(notifications)));
}
public void deleteNotifications(TenantName tenantName) {
@@ -708,11 +655,11 @@ public class CuratorDb {
// -------------- Job Retrigger entries -----------------------------------
public List<RetriggerEntry> readRetriggerEntries() {
- return readSlime(deploymentRetriggerPath()).map(RetriggerEntrySerializer::fromSlime).orElseGet(List::of);
+ return readSlime(deploymentRetriggerPath()).map(retriggerEntrySerializer::fromSlime).orElseGet(List::of);
}
public void writeRetriggerEntries(List<RetriggerEntry> retriggerEntries) {
- curator.set(deploymentRetriggerPath(), asJson(RetriggerEntrySerializer.toSlime(retriggerEntries)));
+ curator.set(deploymentRetriggerPath(), asJson(retriggerEntrySerializer.toSlime(retriggerEntries)));
}
// -------------- Paths ---------------------------------------------------
@@ -722,32 +669,6 @@ public class CuratorDb {
.append(tenant.value());
}
- private Path legacyLockPath(TenantAndApplicationId application) {
- return lockPath(application.tenant())
- .append(application.application().value());
- }
-
- private Path legacyLockPath(ApplicationId instance) {
- return legacyLockPath(TenantAndApplicationId.from(instance))
- .append(instance.instance().value());
- }
-
- private Path legacyLockPath(ApplicationId instance, ZoneId zone) {
- return legacyLockPath(instance)
- .append(zone.environment().value())
- .append(zone.region().value());
- }
-
- private Path legacyLockPath(ApplicationId instance, JobType type) {
- return legacyLockPath(instance)
- .append(type.jobName());
- }
-
- private Path legacyLockPath(ApplicationId instance, JobType type, Step step) {
- return legacyLockPath(instance, type)
- .append(step.name());
- }
-
private Path lockPath(TenantAndApplicationId application) {
return lockRoot.append(application.tenant().value() + ":" + application.application().value());
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/MockCuratorDb.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/MockCuratorDb.java
index 65c9859ad72..21414339a87 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/MockCuratorDb.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/MockCuratorDb.java
@@ -2,9 +2,9 @@
package com.yahoo.vespa.hosted.controller.persistence;
import com.google.inject.Inject;
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.config.provision.SystemName;
import com.yahoo.vespa.curator.mock.MockCurator;
-import com.yahoo.vespa.flags.InMemoryFlagSource;
-
import java.time.Duration;
/**
@@ -18,21 +18,21 @@ public class MockCuratorDb extends CuratorDb {
private final MockCurator curator;
@Inject
- public MockCuratorDb() {
- this("test-controller:2222");
+ public MockCuratorDb(ConfigserverConfig config) {
+ this("test-controller:2222", SystemName.from(config.system()));
+ }
+
+ public MockCuratorDb(SystemName system) {
+ this("test-controller:2222", system);
}
- public MockCuratorDb(String zooKeeperEnsembleConnectionSpec) {
- this(new MockCurator() {
- @Override
- public String zooKeeperEnsembleConnectionSpec() {
- return zooKeeperEnsembleConnectionSpec;
- }
- });
+ public MockCuratorDb(String zooKeeperEnsembleConnectionSpec, SystemName system) {
+ this(new MockCurator() { @Override public String zooKeeperEnsembleConnectionSpec() { return zooKeeperEnsembleConnectionSpec; } },
+ system);
}
- public MockCuratorDb(MockCurator curator) {
- super(curator, Duration.ofMillis(100), new InMemoryFlagSource());
+ public MockCuratorDb(MockCurator curator, SystemName system) {
+ super(curator, Duration.ofMillis(100), system);
this.curator = curator;
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NodeVersionSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NodeVersionSerializer.java
index 10ec9dce5f7..0f1f531d589 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NodeVersionSerializer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NodeVersionSerializer.java
@@ -46,7 +46,7 @@ public class NodeVersionSerializer {
public List<NodeVersion> nodeVersionsFromSlime(Inspector array, Version version) {
List<NodeVersion> nodeVersions = new ArrayList<>();
array.traverse((ArrayTraverser) (i, entry) -> {
- var hostname = HostName.from(entry.field(hostnameField).asString());
+ var hostname = HostName.of(entry.field(hostnameField).asString());
var zone = ZoneId.from(entry.field(zoneField).asString());
var wantedVersion = Version.fromString(entry.field(wantedVersionField).asString());
var suspendedAt = SlimeUtils.optionalInstant(entry.field(suspendedAtField));
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java
index 10763e1f22c..16ec240a116 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java
@@ -4,6 +4,7 @@ package com.yahoo.vespa.hosted.controller.persistence;
import com.yahoo.config.provision.ApplicationName;
import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.InstanceName;
+import com.yahoo.config.provision.SystemName;
import com.yahoo.config.provision.TenantName;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.slime.Cursor;
@@ -43,7 +44,7 @@ public class NotificationsSerializer {
private static final String jobTypeField = "jobId";
private static final String runNumberField = "runNumber";
- public static Slime toSlime(List<Notification> notifications) {
+ public Slime toSlime(List<Notification> notifications) {
Slime slime = new Slime();
Cursor notificationsArray = slime.setObject().setArray(notificationsFieldName);
@@ -59,20 +60,20 @@ public class NotificationsSerializer {
notification.source().instance().ifPresent(instance -> notificationObject.setString(instanceField, instance.value()));
notification.source().zoneId().ifPresent(zoneId -> notificationObject.setString(zoneField, zoneId.value()));
notification.source().clusterId().ifPresent(clusterId -> notificationObject.setString(clusterIdField, clusterId.value()));
- notification.source().jobType().ifPresent(jobType -> notificationObject.setString(jobTypeField, jobType.jobName()));
+ notification.source().jobType().ifPresent(jobType -> notificationObject.setString(jobTypeField, jobType.serialized()));
notification.source().runNumber().ifPresent(runNumber -> notificationObject.setLong(runNumberField, runNumber));
}
return slime;
}
- public static List<Notification> fromSlime(TenantName tenantName, Slime slime) {
+ public List<Notification> fromSlime(TenantName tenantName, Slime slime) {
return SlimeUtils.entriesStream(slime.get().field(notificationsFieldName))
.map(inspector -> fromInspector(tenantName, inspector))
.collect(Collectors.toUnmodifiableList());
}
- private static Notification fromInspector(TenantName tenantName, Inspector inspector) {
+ private Notification fromInspector(TenantName tenantName, Inspector inspector) {
return new Notification(
SlimeUtils.instant(inspector.field(atFieldName)),
typeFrom(inspector.field(typeField)),
@@ -83,7 +84,7 @@ public class NotificationsSerializer {
SlimeUtils.optionalString(inspector.field(instanceField)).map(InstanceName::from),
SlimeUtils.optionalString(inspector.field(zoneField)).map(ZoneId::from),
SlimeUtils.optionalString(inspector.field(clusterIdField)).map(ClusterSpec.Id::from),
- SlimeUtils.optionalString(inspector.field(jobTypeField)).map(JobType::fromJobName),
+ SlimeUtils.optionalString(inspector.field(jobTypeField)).map(jobName -> JobType.ofSerialized(jobName)),
SlimeUtils.optionalLong(inspector.field(runNumberField))),
SlimeUtils.entriesStream(inspector.field(messagesField)).map(Inspector::asString).collect(Collectors.toUnmodifiableList()));
}
@@ -91,6 +92,8 @@ public class NotificationsSerializer {
private static String asString(Notification.Type type) {
switch (type) {
case applicationPackage: return "applicationPackage";
+ case submission: return "submission";
+ case testPackage: return "testPackage";
case deployment: return "deployment";
case feedBlock: return "feedBlock";
case reindex: return "reindex";
@@ -101,6 +104,8 @@ public class NotificationsSerializer {
private static Notification.Type typeFrom(Inspector field) {
switch (field.asString()) {
case "applicationPackage": return Notification.Type.applicationPackage;
+ case "submission": return Notification.Type.submission;
+ case "testPackage": return Notification.Type.testPackage;
case "deployment": return Notification.Type.deployment;
case "feedBlock": return Notification.Type.feedBlock;
case "reindex": return Notification.Type.reindex;
@@ -125,4 +130,5 @@ public class NotificationsSerializer {
default: throw new IllegalArgumentException("Unknown serialized notification level value '" + field.asString() + "'");
}
}
+
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RoutingPolicySerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RoutingPolicySerializer.java
index 17337f823c0..72c16ae0110 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RoutingPolicySerializer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RoutingPolicySerializer.java
@@ -1,9 +1,9 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.persistence;
+import ai.vespa.http.DomainName;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.HostName;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.slime.ArrayTraverser;
import com.yahoo.slime.Cursor;
@@ -83,7 +83,7 @@ public class RoutingPolicySerializer {
ClusterSpec.Id.from(inspect.field(clusterField).asString()),
ZoneId.from(inspect.field(zoneField).asString()));
policies.add(new RoutingPolicy(id,
- HostName.from(inspect.field(canonicalNameField).asString()),
+ DomainName.of(inspect.field(canonicalNameField).asString()),
SlimeUtils.optionalString(inspect.field(dnsZoneField)),
instanceEndpoints,
applicationEndpoints,
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializer.java
index c3d81b8dcd5..dd28978d948 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializer.java
@@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.controller.persistence;
import com.yahoo.component.Version;
import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.SystemName;
import com.yahoo.security.X509CertificateUtils;
import com.yahoo.slime.ArrayTraverser;
import com.yahoo.slime.Cursor;
@@ -10,10 +11,9 @@ import com.yahoo.slime.Inspector;
import com.yahoo.slime.ObjectTraverser;
import com.yahoo.slime.Slime;
import com.yahoo.slime.SlimeUtils;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
+import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.SourceRevision;
import com.yahoo.vespa.hosted.controller.deployment.ConvergenceSummary;
import com.yahoo.vespa.hosted.controller.deployment.Run;
import com.yahoo.vespa.hosted.controller.deployment.RunStatus;
@@ -28,7 +28,6 @@ import java.util.Collections;
import java.util.EnumMap;
import java.util.NavigableMap;
import java.util.Optional;
-import java.util.OptionalLong;
import java.util.TreeMap;
import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.aborted;
@@ -36,6 +35,7 @@ import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.deploymentF
import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.endpointCertificateTimeout;
import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.error;
import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.installationFailed;
+import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.noTests;
import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.nodeAllocationFailure;
import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.reset;
import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.running;
@@ -74,7 +74,6 @@ class RunSerializer {
// - REMOVING FIELDS: Stop reading the field first. Stop writing it on a later version.
// - CHANGING THE FORMAT OF A FIELD: Don't do it bro.
- // TODO: Remove "steps" when there are no traces of it in the controllers
private static final String stepsField = "steps";
private static final String stepDetailsField = "stepDetails";
private static final String startTimeField = "startTime";
@@ -88,17 +87,9 @@ class RunSerializer {
private static final String versionsField = "versions";
private static final String isRedeploymentField = "isRedeployment";
private static final String platformVersionField = "platform";
- private static final String repositoryField = "repository";
- private static final String branchField = "branch";
- private static final String commitField = "commit";
- private static final String authorEmailField = "authorEmail";
private static final String deployedDirectlyField = "deployedDirectly";
- private static final String compileVersionField = "compileVersion";
- private static final String buildTimeField = "buildTime";
- private static final String sourceUrlField = "sourceUrl";
private static final String buildField = "build";
private static final String sourceField = "source";
- private static final String bundleHashField = "bundleHash";
private static final String lastTestRecordField = "lastTestRecord";
private static final String lastVespaLogTimestampField = "lastVespaLogTimestamp";
private static final String noNodesDownSinceField = "noNodesDownSince";
@@ -134,11 +125,12 @@ class RunSerializer {
steps.put(typedStep, new StepInfo(typedStep, stepStatusOf(status.asString()), startTime));
});
- return new Run(new RunId(ApplicationId.fromSerializedForm(runObject.field(applicationField).asString()),
- JobType.fromJobName(runObject.field(jobTypeField).asString()),
- runObject.field(numberField).asLong()),
+ RunId id = new RunId(ApplicationId.fromSerializedForm(runObject.field(applicationField).asString()),
+ JobType.ofSerialized(runObject.field(jobTypeField).asString()),
+ runObject.field(numberField).asLong());
+ return new Run(id,
steps,
- versionsFromSlime(runObject.field(versionsField)),
+ versionsFromSlime(runObject.field(versionsField), id),
runObject.field(isRedeploymentField).asBool(),
SlimeUtils.instant(runObject.field(startField)),
SlimeUtils.optionalInstant(runObject.field(endField)),
@@ -155,40 +147,26 @@ class RunSerializer {
SlimeUtils.optionalString(runObject.field(reasonField)));
}
- private Versions versionsFromSlime(Inspector versionsObject) {
+ private Versions versionsFromSlime(Inspector versionsObject, RunId id) {
Version targetPlatformVersion = Version.fromString(versionsObject.field(platformVersionField).asString());
- ApplicationVersion targetApplicationVersion = applicationVersionFrom(versionsObject);
+ RevisionId targetRevision = revisionFrom(versionsObject, id);
Optional<Version> sourcePlatformVersion = versionsObject.field(sourceField).valid()
? Optional.of(Version.fromString(versionsObject.field(sourceField).field(platformVersionField).asString()))
: Optional.empty();
- Optional<ApplicationVersion> sourceApplicationVersion = versionsObject.field(sourceField).valid()
- ? Optional.of(applicationVersionFrom(versionsObject.field(sourceField)))
+ Optional<RevisionId> sourceRevision = versionsObject.field(sourceField).valid()
+ ? Optional.of(revisionFrom(versionsObject.field(sourceField), id))
: Optional.empty();
- return new Versions(targetPlatformVersion, targetApplicationVersion, sourcePlatformVersion, sourceApplicationVersion);
+ return new Versions(targetPlatformVersion, targetRevision, sourcePlatformVersion, sourceRevision);
}
- private ApplicationVersion applicationVersionFrom(Inspector versionObject) {
- if ( ! versionObject.field(buildField).valid())
- return ApplicationVersion.unknown;
-
+ private RevisionId revisionFrom(Inspector versionObject, RunId id) {
long buildNumber = versionObject.field(buildField).asLong();
- // TODO jonmv: Remove source revision
- Optional<SourceRevision> source = Optional.of(new SourceRevision(versionObject.field(repositoryField).asString(),
- versionObject.field(branchField).asString(),
- versionObject.field(commitField).asString()))
- .filter(revision -> ! revision.commit().isBlank() && ! revision.repository().isBlank() && ! revision.branch().isBlank());
- Optional<String> authorEmail = SlimeUtils.optionalString(versionObject.field(authorEmailField));
- Optional<Version> compileVersion = SlimeUtils.optionalString(versionObject.field(compileVersionField)).map(Version::fromString);
- Optional<Instant> buildTime = SlimeUtils.optionalInstant(versionObject.field(buildTimeField));
- Optional<String> sourceUrl = SlimeUtils.optionalString(versionObject.field(sourceUrlField));
- Optional<String> commit = SlimeUtils.optionalString(versionObject.field(commitField));
- boolean deployedDirectly = versionObject.field(deployedDirectlyField).asBool();
- Optional<String> bundleHash = SlimeUtils.optionalString(versionObject.field(bundleHashField));
-
- return new ApplicationVersion(source, OptionalLong.of(buildNumber), authorEmail,
- compileVersion, buildTime, sourceUrl, commit, deployedDirectly, bundleHash);
+ boolean production = versionObject.field(deployedDirectlyField).valid() // TODO jonmv: remove after migration
+ && buildNumber > 0
+ && ! versionObject.field(deployedDirectlyField).asBool();
+ return production ? RevisionId.forProduction(buildNumber) : RevisionId.forDevelopment(buildNumber, id.job());
}
// Don't change this — introduce a separate array instead.
@@ -228,7 +206,7 @@ class RunSerializer {
private void toSlime(Run run, Cursor runObject) {
runObject.setString(applicationField, run.id().application().serializedForm());
- runObject.setString(jobTypeField, run.id().type().jobName());
+ runObject.setString(jobTypeField, run.id().type().serialized());
runObject.setBool(isRedeploymentField, run.isRedeployment());
runObject.setLong(numberField, run.id().number());
runObject.setLong(startField, run.start().toEpochMilli());
@@ -251,10 +229,10 @@ class RunSerializer {
stepDetailsObject.setObject(valueOf(step)).setLong(startTimeField, valueOf(startTime))));
Cursor versionsObject = runObject.setObject(versionsField);
- toSlime(run.versions().targetPlatform(), run.versions().targetApplication(), versionsObject);
+ toSlime(run.versions().targetPlatform(), run.versions().targetRevision(), versionsObject);
run.versions().sourcePlatform().ifPresent(sourcePlatformVersion -> {
toSlime(sourcePlatformVersion,
- run.versions().sourceApplication()
+ run.versions().sourceRevision()
.orElseThrow(() -> new IllegalArgumentException("Source versions must be both present or absent.")),
versionsObject.setObject(sourceField));
});
@@ -262,19 +240,10 @@ class RunSerializer {
run.reason().ifPresent(reason -> runObject.setString(reasonField, reason));
}
- private void toSlime(Version platformVersion, ApplicationVersion applicationVersion, Cursor versionsObject) {
+ private void toSlime(Version platformVersion, RevisionId revsion, Cursor versionsObject) {
versionsObject.setString(platformVersionField, platformVersion.toString());
- applicationVersion.buildNumber().ifPresent(number -> versionsObject.setLong(buildField, number));
- // TODO jonmv: Remove source revision.
- applicationVersion.source().map(SourceRevision::repository).ifPresent(repository -> versionsObject.setString(repositoryField, repository));
- applicationVersion.source().map(SourceRevision::branch).ifPresent(branch -> versionsObject.setString(branchField, branch));
- applicationVersion.source().map(SourceRevision::commit).ifPresent(commit -> versionsObject.setString(commitField, commit));
- applicationVersion.authorEmail().ifPresent(email -> versionsObject.setString(authorEmailField, email));
- applicationVersion.compileVersion().ifPresent(version -> versionsObject.setString(compileVersionField, version.toString()));
- applicationVersion.buildTime().ifPresent(time -> versionsObject.setLong(buildTimeField, time.toEpochMilli()));
- applicationVersion.sourceUrl().ifPresent(url -> versionsObject.setString(sourceUrlField, url));
- applicationVersion.commit().ifPresent(commit -> versionsObject.setString(commitField, commit));
- versionsObject.setBool(deployedDirectlyField, applicationVersion.isDeployedDirectly());
+ versionsObject.setLong(buildField, revsion.number());
+ versionsObject.setBool(deployedDirectlyField, ! revsion.isProduction());
}
// Don't change this - introduce a separate array with new values if needed.
@@ -368,6 +337,7 @@ class RunSerializer {
case deploymentFailed : return "deploymentFailed";
case installationFailed : return "installationFailed";
case testFailure : return "testFailure";
+ case noTests : return "noTests";
case error : return "error";
case success : return "success";
case aborted : return "aborted";
@@ -380,11 +350,11 @@ class RunSerializer {
static RunStatus runStatusOf(String status) {
switch (status) {
case "running" : return running;
- case "outOfCapacity" : return nodeAllocationFailure; // TODO: Remove after March 2022
case "nodeAllocationFailure" : return nodeAllocationFailure;
case "endpointCertificateTimeout" : return endpointCertificateTimeout;
case "deploymentFailed" : return deploymentFailed;
case "installationFailed" : return installationFailed;
+ case "noTests" : return noTests;
case "testFailure" : return testFailure;
case "error" : return error;
case "success" : return success;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/SupportAccessSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/SupportAccessSerializer.java
index e9e5f8cf032..c200ca467da 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/SupportAccessSerializer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/SupportAccessSerializer.java
@@ -13,7 +13,6 @@ import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.List;
-import java.util.Optional;
import java.util.stream.Collectors;
/**
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
index 79f0088a214..fdd93eedbff 100644
--- 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
@@ -1,12 +1,12 @@
// Copyright Yahoo. 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 ai.vespa.http.HttpURL;
import ai.vespa.http.HttpURL.Path;
import ai.vespa.http.HttpURL.Query;
+import com.yahoo.container.jdisc.HttpRequest;
import com.yahoo.text.Text;
+
import java.io.InputStream;
import java.net.URI;
import java.util.List;
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
index 05cdc0d0565..886dc27b404 100644
--- 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
@@ -1,9 +1,9 @@
// Copyright Yahoo. 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 ai.vespa.http.HttpURL;
import ai.vespa.http.HttpURL.Path;
+import com.yahoo.container.jdisc.HttpResponse;
import java.io.IOException;
import java.io.OutputStream;
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 924f7c0b0a5..88b319d0051 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,7 +2,10 @@
package com.yahoo.vespa.hosted.controller.restapi.application;
import ai.vespa.hosted.api.Signatures;
+import ai.vespa.http.DomainName;
+import ai.vespa.http.HttpURL;
import ai.vespa.http.HttpURL.Query;
+import ai.vespa.validation.Validation;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Joiner;
@@ -26,10 +29,8 @@ import com.yahoo.container.jdisc.HttpRequest;
import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.container.jdisc.ThreadedHttpRequestHandler;
import com.yahoo.io.IOUtils;
-import ai.vespa.http.DomainName;
import com.yahoo.restapi.ByteArrayResponse;
import com.yahoo.restapi.ErrorResponse;
-import ai.vespa.http.HttpURL;
import com.yahoo.restapi.MessageResponse;
import com.yahoo.restapi.Path;
import com.yahoo.restapi.ResourceResponse;
@@ -53,7 +54,6 @@ import com.yahoo.vespa.hosted.controller.api.application.v4.model.configserverbi
import com.yahoo.vespa.hosted.controller.api.application.v4.model.configserverbindings.RestartAction;
import com.yahoo.vespa.hosted.controller.api.application.v4.model.configserverbindings.ServiceInfo;
import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.Hostname;
import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
import com.yahoo.vespa.hosted.controller.api.integration.aws.TenantRoles;
import com.yahoo.vespa.hosted.controller.api.integration.billing.Quota;
@@ -67,11 +67,12 @@ import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeReposi
import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
+import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.SourceRevision;
import com.yahoo.vespa.hosted.controller.api.integration.noderepository.RestartFilter;
import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretStore;
-import com.yahoo.vespa.hosted.controller.api.integration.user.User;
+import com.yahoo.jdisc.http.filter.security.misc.User;
import com.yahoo.vespa.hosted.controller.api.role.Role;
import com.yahoo.vespa.hosted.controller.api.role.RoleDefinition;
import com.yahoo.vespa.hosted.controller.api.role.SecurityContext;
@@ -93,6 +94,7 @@ import com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger;
import com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger.ChangesToCancel;
import com.yahoo.vespa.hosted.controller.deployment.JobStatus;
import com.yahoo.vespa.hosted.controller.deployment.Run;
+import com.yahoo.vespa.hosted.controller.deployment.Submission;
import com.yahoo.vespa.hosted.controller.deployment.TestConfigSerializer;
import com.yahoo.vespa.hosted.controller.maintenance.ResourceMeterMaintainer;
import com.yahoo.vespa.hosted.controller.notification.Notification;
@@ -118,7 +120,6 @@ import com.yahoo.vespa.hosted.controller.tenant.TenantContacts;
import com.yahoo.vespa.hosted.controller.tenant.TenantInfo;
import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
-import com.yahoo.vespa.serviceview.bindings.ApplicationView;
import com.yahoo.yolean.Exceptions;
import javax.ws.rs.ForbiddenException;
@@ -148,7 +149,6 @@ import java.util.Map;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Scanner;
-import java.util.SortedSet;
import java.util.StringJoiner;
import java.util.function.Function;
import java.util.logging.Level;
@@ -260,7 +260,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/deploying")) return deploying(path.get("tenant"), path.get("application"), path.get("instance"), request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/deploying/pin")) return deploying(path.get("tenant"), path.get("application"), path.get("instance"), request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/job")) return JobControllerApiHandlerHelper.jobTypeResponse(controller, appIdFromPath(path), request.getUri());
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/job/{jobtype}")) return JobControllerApiHandlerHelper.runResponse(controller.jobController().runs(appIdFromPath(path), jobTypeFromPath(path)).descendingMap(), Optional.ofNullable(request.getProperty("limit")), request.getUri());
+ if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/job/{jobtype}")) return JobControllerApiHandlerHelper.runResponse(controller.applications().requireApplication(TenantAndApplicationId.from(path.get("tenant"), path.get("application"))), controller.jobController().runs(appIdFromPath(path), jobTypeFromPath(path)).descendingMap(), Optional.ofNullable(request.getProperty("limit")), request.getUri()); // (((\(✘෴✘)/)))
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/job/{jobtype}/package")) return devApplicationPackage(appIdFromPath(path), jobTypeFromPath(path));
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/job/{jobtype}/diff/{number}")) return devApplicationPackageDiff(runIdFromPath(path));
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/job/{jobtype}/test-config")) return testConfig(appIdFromPath(path), jobTypeFromPath(path));
@@ -268,10 +268,9 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}")) 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}/instance/{instance}/environment/{environment}/region/{region}/reindexing")) return getReindexing(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}/instance/{instance}/environment/{environment}/region/{region}/suspended")) return suspended(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}/instance/{instance}/environment/{environment}/region/{region}/service")) return services(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}/instance/{instance}/environment/{environment}/region/{region}/service/{service}/state/v1/{*}")) return service(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), path.get("service"), path.getRest(), request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/service/{service}/{host}/status/{*}")) return status(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), path.get("service"), path.get("host"), path.getRest(), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/service/{service}/{host}/state/v1/metrics")) return stateV1Metrics(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), path.get("service"), path.get("host"));
+ if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/service/{service}/{host}/state/v1/{*}")) return stateV1(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), path.get("service"), path.get("host"), path.getRest(), request);
+ if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/orchestrator")) return orchestrator(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"));
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/nodes")) return nodes(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"));
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/clusters")) return clusters(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"));
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/content/{*}")) return content(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), path.getRest(), request);
@@ -284,8 +283,6 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
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}")) 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}/suspended")) return suspended(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);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/service/{service}/state/v1/{*}")) return service(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), path.get("service"), path.getRest(), request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/service/{service}/{host}/status/{*}")) return status(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), path.get("service"), path.get("host"), path.getRest(), request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/nodes")) return nodes(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"));
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/clusters")) return clusters(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"));
@@ -356,6 +353,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying")) return cancelDeploy(path.get("tenant"), path.get("application"), "default", "all");
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying/{choice}")) return cancelDeploy(path.get("tenant"), path.get("application"), "default", path.get("choice"));
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/key")) return removeDeployKey(path.get("tenant"), path.get("application"), request);
+ if (path.matches("/application/v4/tenant/{tenant}/application/{application}/submit/{build}")) return cancelBuild(path.get("tenant"), path.get("application"), path.get("build"));
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}")) return deleteInstance(path.get("tenant"), path.get("application"), path.get("instance"), request);
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/deploying")) return cancelDeploy(path.get("tenant"), path.get("application"), path.get("instance"), "all");
if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/deploying/{choice}")) return cancelDeploy(path.get("tenant"), path.get("application"), path.get("instance"), path.get("choice"));
@@ -711,7 +709,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
propertyEquals(request, "application", ApplicationName::from, notification.source().application()) &&
propertyEquals(request, "instance", InstanceName::from, notification.source().instance()) &&
propertyEquals(request, "zone", ZoneId::from, notification.source().zoneId()) &&
- propertyEquals(request, "job", JobType::fromJobName, notification.source().jobType()) &&
+ propertyEquals(request, "job", job -> JobType.fromJobName(job, controller.zoneRegistry()), notification.source().jobType()) &&
propertyEquals(request, "type", Notification.Type::valueOf, Optional.of(notification.type())) &&
propertyEquals(request, "level", Notification.Level::valueOf, Optional.of(notification.level())))
.forEach(notification -> toSlime(notificationsArray.addObject(), notification, includeTenantFieldInResponse, excludeMessages));
@@ -747,7 +745,9 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
private static String notificationTypeAsString(Notification.Type type) {
switch (type) {
+ case submission:
case applicationPackage: return "applicationPackage";
+ case testPackage: return "testPackage";
case deployment: return "deployment";
case feedBlock: return "feedBlock";
case reindex: return "reindex";
@@ -800,47 +800,41 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
}
private HttpResponse devApplicationPackage(ApplicationId id, JobType type) {
- if ( ! type.environment().isManuallyDeployed())
- throw new IllegalArgumentException("Only manually deployed zones have dev packages");
-
- ZoneId zone = type.zone(controller.system());
- ApplicationVersion version = controller.jobController().last(id, type).get().versions().targetApplication();
- byte[] applicationPackage = controller.applications().applicationStore().get(new DeploymentId(id, zone), version);
+ ZoneId zone = type.zone();
+ RevisionId revision = controller.jobController().last(id, type).get().versions().targetRevision();
+ byte[] applicationPackage = controller.applications().applicationStore().get(new DeploymentId(id, zone), revision);
return new ZipResponse(id.toFullString() + "." + zone.value() + ".zip", applicationPackage);
}
private HttpResponse devApplicationPackageDiff(RunId runId) {
- DeploymentId deploymentId = new DeploymentId(runId.application(), runId.job().type().zone(controller.system()));
+ DeploymentId deploymentId = new DeploymentId(runId.application(), runId.job().type().zone());
return controller.applications().applicationStore().getDevDiff(deploymentId, runId.number())
.map(ByteArrayResponse::new)
.orElseThrow(() -> new NotExistsException("No application package diff found for " + runId));
}
private HttpResponse applicationPackage(String tenantName, String applicationName, HttpRequest request) {
- var tenantAndApplication = TenantAndApplicationId.from(tenantName, applicationName);
- SortedSet<ApplicationVersion> versions = controller.applications().requireApplication(tenantAndApplication).versions();
- if (versions.isEmpty())
- throw new NotExistsException("No application package has been submitted for '" + tenantAndApplication + "'");
-
- ApplicationVersion version = Optional.ofNullable(request.getProperty("build"))
- .map(build -> {
- try {
- return Long.parseLong(build);
- } catch (NumberFormatException e) {
- throw new IllegalArgumentException("Invalid build number", e);
- }
- })
- .map(build -> versions.stream()
- .filter(ver -> ver.buildNumber().orElse(-1) == build)
- .findFirst()
- .orElseThrow(() -> new NotExistsException("No application package found for '" + tenantAndApplication + "' with build number " + build)))
- .orElseGet(versions::last);
-
+ TenantAndApplicationId tenantAndApplication = TenantAndApplicationId.from(tenantName, applicationName);
+ long build;
+ String parameter = request.getProperty("build");
+ if (parameter != null)
+ try {
+ build = Validation.requireAtLeast(Long.parseLong(request.getProperty("build")), "build number", 1L);
+ }
+ catch (NumberFormatException e) {
+ throw new IllegalArgumentException("invalid value for request parameter 'build'", e);
+ }
+ else {
+ build = controller.applications().requireApplication(tenantAndApplication).revisions().last()
+ .map(version -> version.id().number())
+ .orElseThrow(() -> new NotExistsException("no application package has been submitted for " + tenantAndApplication));
+ }
+ RevisionId revision = RevisionId.forProduction(build);
boolean tests = request.getBooleanProperty("tests");
byte[] applicationPackage = tests ?
- controller.applications().applicationStore().getTester(tenantAndApplication.tenant(), tenantAndApplication.application(), version) :
- controller.applications().applicationStore().get(new DeploymentId(tenantAndApplication.defaultInstance(), ZoneId.defaultId()), version);
- String filename = tenantAndApplication + (tests ? "-tests" : "-build") + version.buildNumber().getAsLong() + ".zip";
+ controller.applications().applicationStore().getTester(tenantAndApplication.tenant(), tenantAndApplication.application(), revision) :
+ controller.applications().applicationStore().get(new DeploymentId(tenantAndApplication.defaultInstance(), ZoneId.defaultId()), revision);
+ String filename = tenantAndApplication + (tests ? "-tests" : "-build") + revision.number() + ".zip";
return new ZipResponse(filename, applicationPackage);
}
@@ -1121,7 +1115,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
toSlime(node.resources(), nodeObject);
nodeObject.setString("clusterId", node.clusterId());
nodeObject.setString("clusterType", valueOf(node.clusterType()));
- nodeObject.setBool("down", node.history().stream().anyMatch(event -> "down".equals(event.name())));
+ nodeObject.setBool("down", node.down());
nodeObject.setBool("retired", node.retired() || node.wantToRetire());
nodeObject.setBool("restarting", node.wantedRestartGeneration() > node.restartGeneration());
nodeObject.setBool("rebooting", node.wantedRebootGeneration() > node.rebootGeneration());
@@ -1318,7 +1312,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
request.getUri()).toString());
DeploymentStatus status = controller.jobController().deploymentStatus(application);
- application.latestVersion().ifPresent(version -> JobControllerApiHandlerHelper.toSlime(object.setObject("latestVersion"), version));
+ application.revisions().last().ifPresent(version -> JobControllerApiHandlerHelper.toSlime(object.setObject("latestVersion"), version));
application.projectId().ifPresent(id -> object.setLong("projectId", id));
@@ -1326,11 +1320,11 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
application.instances().values().stream().findFirst().ifPresent(instance -> {
// Currently deploying change
if ( ! instance.change().isEmpty())
- toSlime(object.setObject("deploying"), instance.change());
+ toSlime(object.setObject("deploying"), instance.change(), application);
// Outstanding change
if ( ! status.outstandingChange(instance.name()).isEmpty())
- toSlime(object.setObject("outstandingChange"), status.outstandingChange(instance.name()));
+ toSlime(object.setObject("outstandingChange"), status.outstandingChange(instance.name()), application);
});
application.majorVersion().ifPresent(majorVersion -> object.setLong("majorVersion", majorVersion));
@@ -1370,11 +1364,11 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
.sortedJobs(status.instanceJobs(instance.name()).values());
if ( ! instance.change().isEmpty())
- toSlime(object.setObject("deploying"), instance.change());
+ toSlime(object.setObject("deploying"), instance.change(), status.application());
// Outstanding change
if ( ! status.outstandingChange(instance.name()).isEmpty())
- toSlime(object.setObject("outstandingChange"), status.outstandingChange(instance.name()));
+ toSlime(object.setObject("outstandingChange"), status.outstandingChange(instance.name()), status.application());
// Change blockers
Cursor changeBlockers = object.setArray("changeBlockers");
@@ -1395,7 +1389,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
// Deployments sorted according to deployment spec
List<Deployment> deployments = deploymentSpec.instance(instance.name())
- .map(spec -> new DeploymentSteps(spec, controller::system))
+ .map(spec -> new DeploymentSteps(spec, controller.zoneRegistry()))
.map(steps -> steps.sortedDeployments(instance.deployments().values()))
.orElse(List.copyOf(instance.deployments().values()));
@@ -1441,7 +1435,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
"/instance/" + instance.id().instance().value() + "/job/",
request.getUri()).toString());
- application.latestVersion().ifPresent(version -> {
+ application.revisions().last().ifPresent(version -> {
version.sourceUrl().ifPresent(url -> object.setString("sourceUrl", url));
version.commit().ifPresent(commit -> object.setString("commit", commit));
});
@@ -1455,11 +1449,11 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
.sortedJobs(status.instanceJobs(instance.name()).values());
if ( ! instance.change().isEmpty())
- toSlime(object.setObject("deploying"), instance.change());
+ toSlime(object.setObject("deploying"), instance.change(), application);
// Outstanding change
if ( ! status.outstandingChange(instance.name()).isEmpty())
- toSlime(object.setObject("outstandingChange"), status.outstandingChange(instance.name()));
+ toSlime(object.setObject("outstandingChange"), status.outstandingChange(instance.name()), application);
// Change blockers
Cursor changeBlockers = object.setArray("changeBlockers");
@@ -1483,7 +1477,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
// Deployments sorted according to deployment spec
List<Deployment> deployments =
application.deploymentSpec().instance(instance.name())
- .map(spec -> new DeploymentSteps(spec, controller::system))
+ .map(spec -> new DeploymentSteps(spec, controller.zoneRegistry()))
.map(steps -> steps.sortedDeployments(instance.deployments().values()))
.orElse(List.copyOf(instance.deployments().values()));
Cursor instancesArray = object.setArray("instances");
@@ -1525,7 +1519,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
controller.jobController().active(instance.id()).stream()
.map(run -> run.id().job())
.filter(job -> job.type().environment().isManuallyDeployed()))
- .map(job -> job.type().zone(controller.system()))
+ .map(job -> job.type().zone())
.filter(zone -> ! instance.deployments().containsKey(zone))
.forEach(zone -> {
Cursor deploymentObject = instancesArray.addObject();
@@ -1574,11 +1568,9 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
return new SlimeJsonResponse(slime);
}
- private void toSlime(Cursor object, Change change) {
+ private void toSlime(Cursor object, Change change, Application application) {
change.platform().ifPresent(version -> object.setString("version", version.toString()));
- change.application()
- .filter(version -> !version.isUnknown())
- .ifPresent(version -> JobControllerApiHandlerHelper.toSlime(object.setObject("revision"), version));
+ change.revision().ifPresent(revision -> JobControllerApiHandlerHelper.toSlime(object.setObject("revision"), application.revisions().get(revision)));
}
private void toSlime(Endpoint endpoint, Cursor object) {
@@ -1623,8 +1615,9 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
response.setString("nodes", withPathAndQuery("/zone/v2/" + deploymentId.zoneId().environment() + "/" + deploymentId.zoneId().region() + "/nodes/v2/node/", "recursive=true&application=" + deploymentId.applicationId().tenant() + "." + deploymentId.applicationId().application() + "." + deploymentId.applicationId().instance(), request.getUri()).toString());
response.setString("yamasUrl", monitoringSystemUri(deploymentId).toString());
response.setString("version", deployment.version().toFullString());
- response.setString("revision", deployment.applicationVersion().id());
- Instant lastDeploymentStart = lastDeploymentStart(deploymentId.applicationId(), deployment);
+ response.setString("revision", application.revisions().get(deployment.revision()).stringId()); // TODO jonmv or freva: ƪ(`▿▿▿▿´ƪ)
+ response.setLong("build", deployment.revision().number());
+ Instant lastDeploymentStart = controller.jobController().lastDeploymentStart(deploymentId.applicationId(), deployment);
response.setLong("deployTimeEpochMs", lastDeploymentStart.toEpochMilli());
controller.zoneRegistry().getDeploymentTimeToLive(deploymentId.zoneId())
.ifPresent(deploymentTimeToLive -> response.setLong("expiryTimeEpochMs", lastDeploymentStart.plus(deploymentTimeToLive).toEpochMilli()));
@@ -1638,22 +1631,18 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
if (!deployment.zone().environment().isManuallyDeployed()) {
DeploymentStatus status = controller.jobController().deploymentStatus(application);
- JobType.from(controller.system(), deployment.zone())
- .map(type -> new JobId(instance.id(), type))
- .map(status.jobSteps()::get)
+ JobId jobId = new JobId(instance.id(), JobType.deploymentTo(deployment.zone()));
+ Optional.ofNullable(status.jobSteps().get(jobId))
.ifPresent(stepStatus -> {
- JobControllerApiHandlerHelper.toSlime(
- response.setObject("applicationVersion"), deployment.applicationVersion());
- if (!status.jobsToRun().containsKey(stepStatus.job().get()))
+ JobControllerApiHandlerHelper.toSlime(response.setObject("applicationVersion"), application.revisions().get(deployment.revision()));
+ if ( ! status.jobsToRun().containsKey(stepStatus.job().get()))
response.setString("status", "complete");
else if (stepStatus.readyAt(instance.change()).map(controller.clock().instant()::isBefore).orElse(true))
response.setString("status", "pending");
else response.setString("status", "running");
});
} else {
- var deploymentRun = JobType.from(controller.system(), deploymentId.zoneId())
- .flatMap(jobType -> controller.jobController().last(deploymentId.applicationId(), jobType));
-
+ var deploymentRun = controller.jobController().last(deploymentId.applicationId(), JobType.deploymentTo(deploymentId.zoneId()));
deploymentRun.ifPresent(run -> {
response.setString("status", run.hasEnded() ? "complete" : "running");
});
@@ -1685,11 +1674,6 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
metrics.instant().ifPresent(instant -> metricsObject.setLong("lastUpdated", instant.toEpochMilli()));
}
- private Instant lastDeploymentStart(ApplicationId instanceId, Deployment deployment) {
- return controller.jobController().jobStarts(new JobId(instanceId, JobType.from(controller.system(), deployment.zone()).get()))
- .stream().findFirst().orElse(deployment.at());
- }
-
private void toSlime(RotationState state, Cursor object) {
Cursor bcpStatus = object.setObject("bcpStatus");
bcpStatus.setString("rotationStatus", rotationStateString(state));
@@ -1770,7 +1754,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
Cursor root = slime.setObject();
if ( ! instance.change().isEmpty()) {
instance.change().platform().ifPresent(version -> root.setString("platform", version.toString()));
- instance.change().application().ifPresent(applicationVersion -> root.setString("application", applicationVersion.id()));
+ instance.change().revision().ifPresent(revision -> root.setString("application", revision.toString()));
root.setBool("pinned", instance.change().isPinned());
}
return new SlimeJsonResponse(slime);
@@ -1786,17 +1770,6 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
return new SlimeJsonResponse(slime);
}
- private HttpResponse services(String tenantName, String applicationName, String instanceName, String environment, String region, HttpRequest request) {
- ApplicationView applicationView = controller.getApplicationView(tenantName, applicationName, instanceName, environment, region);
- ZoneId zone = requireZone(environment, region);
- ServiceApiResponse response = new ServiceApiResponse(zone,
- new ApplicationId.Builder().tenant(tenantName).applicationName(applicationName).instanceName(instanceName).build(),
- List.of(controller.zoneRegistry().getConfigServerVipUri(zone)),
- request.getUri());
- response.setResponse(applicationView);
- return response;
- }
-
private HttpResponse status(String tenantName, String applicationName, String instanceName, String environment, String region, String serviceName, String host, HttpURL.Path restPath, HttpRequest request) {
DeploymentId deploymentId = new DeploymentId(ApplicationId.from(tenantName, applicationName, instanceName), requireZone(environment, region));
return controller.serviceRegistry().configServer().getServiceNodePage(deploymentId,
@@ -1806,21 +1779,17 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
Query.empty().add(request.getJDiscRequest().parameters()));
}
- private HttpResponse stateV1Metrics(String tenantName, String applicationName, String instanceName, String environment, String region, String serviceName, String host) {
+ private HttpResponse orchestrator(String tenantName, String applicationName, String instanceName, String environment, String region) {
DeploymentId deploymentId = new DeploymentId(ApplicationId.from(tenantName, applicationName, instanceName), requireZone(environment, region));
- return controller.serviceRegistry().configServer().getServiceNodePage(
- deploymentId, serviceName, DomainName.of(host), HttpURL.Path.parse("/state/v1/metrics"), Query.empty());
+ return controller.serviceRegistry().configServer().getServiceNodes(deploymentId);
}
- private HttpResponse service(String tenantName, String applicationName, String instanceName, String environment, String region, String serviceName, HttpURL.Path restPath, HttpRequest request) {
+ private HttpResponse stateV1(String tenantName, String applicationName, String instanceName, String environment, String region, String serviceName, String host, HttpURL.Path rest, HttpRequest request) {
DeploymentId deploymentId = new DeploymentId(ApplicationId.from(tenantName, applicationName, instanceName), requireZone(environment, region));
- Map<?,?> result = controller.serviceRegistry().configServer().getServiceApiResponse(deploymentId, serviceName, restPath);
- ServiceApiResponse response = new ServiceApiResponse(deploymentId.zoneId(),
- deploymentId.applicationId(),
- List.of(controller.zoneRegistry().getConfigServerVipUri(deploymentId.zoneId())),
- request.getUri());
- response.setResponse(result, serviceName, HttpURL.Path.parse("/state/v1").append(restPath));
- return response;
+ Query query = Query.empty().add(request.getJDiscRequest().parameters());
+ query = query.set("forwarded-url", HttpURL.from(request.getUri()).withQuery(Query.empty()).asURI().toString());
+ return controller.serviceRegistry().configServer().getServiceNodePage(
+ deploymentId, serviceName, DomainName.of(host), HttpURL.Path.parse("/state/v1").append(rest), query);
}
private HttpResponse content(String tenantName, String applicationName, String instanceName, String environment, String region, HttpURL.Path restPath, HttpRequest request) {
@@ -1915,18 +1884,19 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
StringBuilder response = new StringBuilder();
controller.applications().lockApplicationOrThrow(TenantAndApplicationId.from(id), application -> {
- ApplicationVersion version = build == -1 ? application.get().latestVersion().get()
- : getApplicationVersion(application.get(), build);
- Change change = Change.of(version);
+ RevisionId revision = build == -1 ? application.get().revisions().last().get().id()
+ : getRevision(application.get(), build);
+ Change change = Change.of(revision);
controller.applications().deploymentTrigger().forceChange(id, change);
response.append("Triggered ").append(change).append(" for ").append(id);
});
return new MessageResponse(response.toString());
}
- private ApplicationVersion getApplicationVersion(Application application, Long build) {
- return application.versions().stream()
- .filter(version -> version.buildNumber().stream().anyMatch(build::equals))
+ private RevisionId getRevision(Application application, long build) {
+ return application.revisions().withPackage().stream()
+ .map(ApplicationVersion::id)
+ .filter(version -> version.number() == build)
.findFirst()
.filter(version -> controller.applications().applicationStore().hasBuild(application.id().tenant(),
application.id().application(),
@@ -1934,6 +1904,15 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
.orElseThrow(() -> new IllegalArgumentException("Build number '" + build + "' was not found"));
}
+ private HttpResponse cancelBuild(String tenantName, String applicationName, String build){
+ TenantAndApplicationId id = TenantAndApplicationId.from(tenantName, applicationName);
+ RevisionId revision = RevisionId.forProduction(Long.parseLong(build));
+ controller.applications().lockApplicationOrThrow(id, application -> {
+ controller.applications().store(application.withRevisions(revisions -> revisions.with(revisions.get(revision).skipped())));
+ });
+ return new MessageResponse("Marked build '" + build + "' as non-deployable");
+ }
+
/** Cancel ongoing change for given application, e.g., everything with {"cancel":"all"} */
private HttpResponse cancelDeploy(String tenantName, String applicationName, String instanceName, String choice) {
ApplicationId id = ApplicationId.from(tenantName, applicationName, instanceName);
@@ -2053,7 +2032,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
DeploymentId deploymentId = new DeploymentId(ApplicationId.from(tenantName, applicationName, instanceName),
requireZone(environment, region));
RestartFilter restartFilter = new RestartFilter()
- .withHostName(Optional.ofNullable(request.getProperty("hostname")).map(HostName::from))
+ .withHostName(Optional.ofNullable(request.getProperty("hostname")).map(HostName::of))
.withClusterType(Optional.ofNullable(request.getProperty("clusterType")).map(ClusterSpec.Type::from))
.withClusterId(Optional.ofNullable(request.getProperty("clusterId")).map(ClusterSpec.Id::from));
@@ -2080,7 +2059,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
ApplicationPackage applicationPackage = new ApplicationPackage(dataParts.get(EnvironmentResource.APPLICATION_ZIP));
controller.applications().verifyApplicationIdentityConfiguration(id.tenant(),
Optional.of(id.instance()),
- Optional.of(type.zone(controller.system())),
+ Optional.of(type.zone()),
applicationPackage,
Optional.of(requireUserPrincipal(request)));
@@ -2193,7 +2172,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
.flatMap(instance -> instance.productionDeployments().keySet().stream())
.map(zone -> new DeploymentId(prodInstanceId, zone))
.collect(Collectors.toCollection(HashSet::new));
- ZoneId testedZone = type.zone(controller.system());
+ ZoneId testedZone = type.zone();
// If a production job is specified, the production deployment of the orchestrated instance is the relevant one,
// as user instances should not exist in prod.
@@ -2295,7 +2274,7 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
try {
node = nodeRepository.getNode(zone, hostname);
} catch (IllegalArgumentException e) {
- throw new NotExistsException(new Hostname(hostname));
+ throw new NotExistsException(hostname);
}
ApplicationId app = ApplicationId.from(tenant, application, instance);
ApplicationId owner = node.owner().orElseThrow(() -> new IllegalArgumentException("Node has no owner"));
@@ -2477,17 +2456,17 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
.flatMap(application -> application.instances().values().stream())
.flatMap(instance -> instance.deployments().values().stream()
.filter(deployment -> deployment.zone().environment() == Environment.dev)
- .map(deployment -> lastDeploymentStart(instance.id(), deployment)))
+ .map(deployment -> controller.jobController().lastDeploymentStart(instance.id(), deployment)))
.max(Comparator.naturalOrder())
.or(() -> applications.stream()
.flatMap(application -> application.instances().values().stream())
- .flatMap(instance -> JobType.allIn(controller.system()).stream()
+ .flatMap(instance -> JobType.allIn(controller.zoneRegistry()).stream()
.filter(job -> job.environment() == Environment.dev)
.flatMap(jobType -> controller.jobController().last(instance.id(), jobType).stream()))
.map(Run::start)
.max(Comparator.naturalOrder()));
Optional<Instant> lastSubmission = applications.stream()
- .flatMap(app -> app.latestVersion().flatMap(ApplicationVersion::buildTime).stream())
+ .flatMap(app -> app.revisions().last().flatMap(ApplicationVersion::buildTime).stream())
.max(Comparator.naturalOrder());
object.setLong("createdAtMillis", tenant.createdAt().toEpochMilli());
if (tenant.type() == Tenant.Type.deleted)
@@ -2537,15 +2516,6 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
}
}
- private void toSlime(Run run, Cursor object) {
- object.setLong("id", run.id().number());
- object.setString("version", run.versions().targetPlatform().toFullString());
- if ( ! run.versions().targetApplication().isUnknown())
- JobControllerApiHandlerHelper.toSlime(object.setObject("revision"), run.versions().targetApplication());
- object.setString("reason", "unknown reason");
- object.setLong("at", run.end().orElse(run.start()).toEpochMilli());
- }
-
private Slime toSlime(InputStream jsonStream) {
try {
byte[] jsonBytes = IOUtils.readBytes(jsonStream, 1000 * 1000);
@@ -2713,11 +2683,11 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
return ApplicationId.from(path.get("tenant"), path.get("application"), path.get("instance"));
}
- private static JobType jobTypeFromPath(Path path) {
- return JobType.fromJobName(path.get("jobtype"));
+ private JobType jobTypeFromPath(Path path) {
+ return JobType.fromJobName(path.get("jobtype"), controller.zoneRegistry());
}
- private static RunId runIdFromPath(Path path) {
+ private RunId runIdFromPath(Path path) {
long number = Long.parseLong(path.get("number"));
return new RunId(appIdFromPath(path), jobTypeFromPath(path), number);
}
@@ -2725,7 +2695,8 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
private HttpResponse submit(String tenant, String application, HttpRequest request) {
Map<String, byte[]> dataParts = parseDataParts(request);
Inspector submitOptions = SlimeUtils.jsonToSlime(dataParts.get(EnvironmentResource.SUBMIT_OPTIONS)).get();
- long projectId = Math.max(1, submitOptions.field("projectId").asLong()); // Absence of this means it's not a prod app :/
+ long projectId = submitOptions.field("projectId").asLong(); // Absence of this means it's not a prod app :/
+ projectId = projectId == 0 ? 1 : projectId;
Optional<String> repository = optional("repository", submitOptions);
Optional<String> branch = optional("branch", submitOptions);
Optional<String> commit = optional("commit", submitOptions);
@@ -2734,6 +2705,8 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
: Optional.empty();
Optional<String> sourceUrl = optional("sourceUrl", submitOptions);
Optional<String> authorEmail = optional("authorEmail", submitOptions);
+ Optional<String> description = optional("description", submitOptions);
+ int risk = (int) submitOptions.field("risk").asLong();
sourceUrl.map(URI::create).ifPresent(url -> {
if (url.getHost() == null || url.getScheme() == null)
@@ -2742,29 +2715,26 @@ public class ApplicationApiHandler extends AuditLoggingRequestHandler {
ApplicationPackage applicationPackage = new ApplicationPackage(dataParts.get(EnvironmentResource.APPLICATION_ZIP), true);
+ byte[] testPackage = dataParts.get(EnvironmentResource.APPLICATION_TEST_ZIP);
+ Submission submission = new Submission(applicationPackage, testPackage, sourceUrl, sourceRevision, authorEmail, description, risk);
+
controller.applications().verifyApplicationIdentityConfiguration(TenantName.from(tenant),
Optional.empty(),
Optional.empty(),
applicationPackage,
Optional.of(requireUserPrincipal(request)));
- ensureApplicationExists(TenantAndApplicationId.from(tenant, application), request);
-
- return JobControllerApiHandlerHelper.submitResponse(controller.jobController(),
- tenant,
- application,
- sourceRevision,
- authorEmail,
- sourceUrl,
- projectId,
- applicationPackage,
- dataParts.get(EnvironmentResource.APPLICATION_TEST_ZIP));
+ TenantAndApplicationId id = TenantAndApplicationId.from(tenant, application);
+ ensureApplicationExists(id, request);
+ return JobControllerApiHandlerHelper.submitResponse(controller.jobController(), id, submission, projectId);
}
private HttpResponse removeAllProdDeployments(String tenant, String application) {
- JobControllerApiHandlerHelper.submitResponse(controller.jobController(), tenant, application,
- Optional.empty(), Optional.empty(), Optional.empty(), 1,
- ApplicationPackage.deploymentRemoval(), new byte[0]);
+ JobControllerApiHandlerHelper.submitResponse(controller.jobController(),
+ TenantAndApplicationId.from(tenant, application),
+ new Submission(ApplicationPackage.deploymentRemoval(), new byte[0], Optional.empty(),
+ Optional.empty(), Optional.empty(), Optional.empty(), 0),
+ 0);
return new MessageResponse("All deployments removed");
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.java
index 2375b5cf049..80425609aa6 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.java
@@ -20,6 +20,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.LogEntry;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
+import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.SourceRevision;
import com.yahoo.vespa.hosted.controller.application.Change;
@@ -33,6 +34,7 @@ import com.yahoo.vespa.hosted.controller.deployment.Run;
import com.yahoo.vespa.hosted.controller.deployment.RunLog;
import com.yahoo.vespa.hosted.controller.deployment.RunStatus;
import com.yahoo.vespa.hosted.controller.deployment.Step;
+import com.yahoo.vespa.hosted.controller.deployment.Submission;
import com.yahoo.vespa.hosted.controller.deployment.Versions;
import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
@@ -43,7 +45,6 @@ import java.time.format.TextStyle;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
-import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -77,29 +78,29 @@ class JobControllerApiHandlerHelper {
Cursor responseObject = slime.setObject();
Cursor jobsArray = responseObject.setArray("deployment");
- Arrays.stream(JobType.values())
- .filter(type -> type.environment().isManuallyDeployed())
- .map(devType -> new JobId(id, devType))
- .forEach(job -> {
- Collection<Run> runs = controller.jobController().runs(job).descendingMap().values();
- if (runs.isEmpty())
- return;
-
- Cursor jobObject = jobsArray.addObject();
- jobObject.setString("jobName", job.type().jobName());
- toSlime(jobObject.setArray("runs"), runs, 10, baseUriForJobs);
- });
+ JobType.allIn(controller.zoneRegistry()).stream()
+ .filter(type -> type.environment().isManuallyDeployed())
+ .map(devType -> new JobId(id, devType))
+ .forEach(job -> {
+ Collection<Run> runs = controller.jobController().runs(job).descendingMap().values();
+ if (runs.isEmpty())
+ return;
+
+ Cursor jobObject = jobsArray.addObject();
+ jobObject.setString("jobName", job.type().jobName());
+ toSlime(jobObject.setArray("runs"), runs, controller.applications().requireApplication(TenantAndApplicationId.from(id)), 10, baseUriForJobs);
+ });
return new SlimeJsonResponse(slime);
}
/** Returns a response with the runs for the given job type. */
- static HttpResponse runResponse(Map<RunId, Run> runs, Optional<String> limitStr, URI baseUriForJobType) {
+ static HttpResponse runResponse(Application application, Map<RunId, Run> runs, Optional<String> limitStr, URI baseUriForJobType) {
Slime slime = new Slime();
Cursor cursor = slime.setObject();
int limit = limitStr.map(Integer::parseInt).orElse(Integer.MAX_VALUE);
- toSlime(cursor.setArray("runs"), runs.values(), limit, baseUriForJobType);
+ toSlime(cursor.setArray("runs"), runs.values(), application, limit, baseUriForJobType);
return new SlimeJsonResponse(slime);
}
@@ -194,19 +195,8 @@ class JobControllerApiHandlerHelper {
*
* @return Response with the new application version
*/
- static HttpResponse submitResponse(JobController jobController, String tenant, String application,
- Optional<SourceRevision> sourceRevision, Optional<String> authorEmail,
- Optional<String> sourceUrl, long projectId,
- ApplicationPackage applicationPackage, byte[] testPackage) {
- ApplicationVersion version = jobController.submit(TenantAndApplicationId.from(tenant, application),
- sourceRevision,
- authorEmail,
- sourceUrl,
- projectId,
- applicationPackage,
- testPackage);
-
- return new MessageResponse(version.toString());
+ static HttpResponse submitResponse(JobController jobController, TenantAndApplicationId id, Submission submission, long projectId) {
+ return new MessageResponse("application " + jobController.submit(id, submission, projectId));
}
/** Aborts any job of the given type. */
@@ -230,6 +220,7 @@ class JobControllerApiHandlerHelper {
case aborted: return "aborted";
case error: return "error";
case testFailure: return "testFailure";
+ case noTests: return "noTests";
case endpointCertificateTimeout: return "endpointCertificateTimeout";
case nodeAllocationFailure: return "nodeAllocationFailure";
case installationFailed: return "installationFailed";
@@ -276,7 +267,7 @@ class JobControllerApiHandlerHelper {
stepStatus.coolingDownUntil(change).ifPresent(until -> stepObject.setLong("coolingDownUntil", until.toEpochMilli()));
stepStatus.blockedUntil(Change.of(controller.systemVersion(versionStatus))) // Dummy version — just anything with a platform.
.ifPresent(until -> stepObject.setLong("platformBlockedUntil", until.toEpochMilli()));
- application.latestVersion().map(Change::of).flatMap(stepStatus::blockedUntil) // Dummy version — just anything with an application.
+ stepStatus.blockedUntil(Change.of(RevisionId.forProduction(1))) // Dummy version — just anything with an application.
.ifPresent(until -> stepObject.setLong("applicationBlockedUntil", until.toEpochMilli()));
if (stepStatus.type() == DeploymentStatus.StepType.delay)
@@ -286,7 +277,7 @@ class JobControllerApiHandlerHelper {
Cursor deployingObject = stepObject.setObject("deploying");
if ( ! change.isEmpty()) {
change.platform().ifPresent(version -> deployingObject.setString("platform", version.toFullString()));
- change.application().ifPresent(version -> toSlime(deployingObject.setObject("application"), version));
+ change.revision().ifPresent(revision -> toSlime(deployingObject.setObject("application"), application.revisions().get(revision)));
}
Cursor latestVersionsObject = stepObject.setObject("latestVersions");
@@ -303,38 +294,33 @@ class JobControllerApiHandlerHelper {
|| deployments.stream().anyMatch(deployment -> deployment.version().isBefore(latestPlatform.versionNumber())));
Cursor availableArray = latestPlatformObject.setArray("available");
+ boolean isUpgrade = true;
for (VespaVersion available : availablePlatforms) {
if ( deployments.stream().anyMatch(deployment -> deployment.version().isAfter(available.versionNumber()))
|| deployments.stream().noneMatch(deployment -> deployment.version().isBefore(available.versionNumber())) && ! deployments.isEmpty()
|| status.hasCompleted(stepStatus.instance(), Change.of(available.versionNumber()))
- || change.platform().map(available.versionNumber()::compareTo).orElse(1) <= 0)
- break;
+ || change.platform().map(available.versionNumber()::compareTo).orElse(1) < 0)
+ isUpgrade = false;
- availableArray.addObject().setString("platform", available.versionNumber().toFullString());
+ Cursor platformObject = availableArray.addObject();
+ platformObject.setString("platform", available.versionNumber().toFullString());
+ platformObject.setBool("upgrade", isUpgrade || change.platform().map(available.versionNumber()::equals).orElse(false));
}
- change.platform().ifPresent(version -> availableArray.addObject().setString("platform", version.toFullString()));
toSlime(latestPlatformObject.setArray("blockers"), blockers.stream().filter(ChangeBlocker::blocksVersions));
}
- List<ApplicationVersion> availableApplications = new ArrayList<>(application.deployableVersions(false));
+ List<ApplicationVersion> availableApplications = new ArrayList<>(application.revisions().deployable(false));
if ( ! availableApplications.isEmpty()) {
var latestApplication = availableApplications.get(0);
Cursor latestApplicationObject = latestVersionsObject.setObject("application");
toSlime(latestApplicationObject.setObject("application"), latestApplication);
latestApplicationObject.setLong("at", latestApplication.buildTime().orElse(Instant.EPOCH).toEpochMilli());
- latestApplicationObject.setBool("upgrade", change.application().map(latestApplication::compareTo).orElse(1) > 0 && deployments.isEmpty()
- || deployments.stream().anyMatch(deployment -> deployment.applicationVersion().compareTo(latestApplication) < 0));
+ latestApplicationObject.setBool("upgrade", change.revision().map(latestApplication.id()::compareTo).orElse(1) > 0 && deployments.isEmpty()
+ || deployments.stream().anyMatch(deployment -> deployment.revision().compareTo(latestApplication.id()) < 0));
Cursor availableArray = latestApplicationObject.setArray("available");
- for (ApplicationVersion available : availableApplications) {
- if ( deployments.stream().anyMatch(deployment -> deployment.applicationVersion().compareTo(available) > 0)
- || deployments.stream().noneMatch(deployment -> deployment.applicationVersion().compareTo(available) < 0) && ! deployments.isEmpty()
- || status.hasCompleted(stepStatus.instance(), Change.of(available))
- || change.application().map(available::compareTo).orElse(1) <= 0)
- break;
-
+ for (ApplicationVersion available : availableApplications)
toSlime(availableArray.addObject().setObject("application"), available);
- }
- change.application().ifPresent(version -> toSlime(availableArray.addObject().setObject("application"), version));
+
toSlime(latestApplicationObject.setArray("blockers"), blockers.stream().filter(ChangeBlocker::blocksRevisions));
}
}
@@ -346,12 +332,12 @@ class JobControllerApiHandlerHelper {
"/job/" + job.type().jobName()).normalize();
stepObject.setString("url", baseUriForJob.toString());
stepObject.setString("environment", job.type().environment().value());
- stepObject.setString("region", job.type().zone(controller.system()).value());
+ stepObject.setString("region", job.type().zone().value());
if (job.type().isProduction() && job.type().isDeployment()) {
status.deploymentFor(job).ifPresent(deployment -> {
stepObject.setString("currentPlatform", deployment.version().toFullString());
- toSlime(stepObject.setObject("currentApplication"), deployment.applicationVersion());
+ toSlime(stepObject.setObject("currentApplication"), application.revisions().get(deployment.revision()));
});
}
@@ -367,19 +353,26 @@ class JobControllerApiHandlerHelper {
continue; // Run will be contained in the "runs" array.
Cursor runObject = toRunArray.addObject();
- toSlime(runObject.setObject("versions"), versions.versions());
+ toSlime(runObject.setObject("versions"), versions.versions(), application);
}
- toSlime(stepObject.setArray("runs"), jobStatus.runs().descendingMap().values(), 10, baseUriForJob);
+ toSlime(stepObject.setArray("runs"), jobStatus.runs().descendingMap().values(), application, 10, baseUriForJob);
});
}
Cursor buildsArray = responseObject.setArray("builds");
- application.versions().stream().sorted(reverseOrder()).forEach(version -> toSlime(buildsArray.addObject(), version));
+ application.revisions().withPackage().stream().sorted(reverseOrder()).forEach(version -> toRichSlime(buildsArray.addObject(), version));
return new SlimeJsonResponse(slime);
}
+ static void toRichSlime(Cursor versionObject, ApplicationVersion version) {
+ toSlime(versionObject, version);
+ version.description().ifPresent(description -> versionObject.setString("description", description));
+ if (version.risk() != 0) versionObject.setLong("risk", version.risk());
+ versionObject.setBool("deployable", version.isDeployable());
+ }
+
static void toSlime(Cursor versionObject, ApplicationVersion version) {
version.buildNumber().ifPresent(id -> versionObject.setLong("build", id));
version.compileVersion().ifPresent(platform -> versionObject.setString("compileVersion", platform.toFullString()));
@@ -387,11 +380,11 @@ class JobControllerApiHandlerHelper {
version.commit().ifPresent(commit -> versionObject.setString("commit", commit));
}
- private static void toSlime(Cursor versionsObject, Versions versions) {
+ private static void toSlime(Cursor versionsObject, Versions versions, Application application) {
versionsObject.setString("targetPlatform", versions.targetPlatform().toFullString());
- toSlime(versionsObject.setObject("targetApplication"), versions.targetApplication());
+ toSlime(versionsObject.setObject("targetApplication"), application.revisions().get(versions.targetRevision()));
versions.sourcePlatform().ifPresent(platform -> versionsObject.setString("sourcePlatform", platform.toFullString()));
- versions.sourceApplication().ifPresent(application -> toSlime(versionsObject.setObject("sourceApplication"), application));
+ versions.sourceRevision().ifPresent(revision -> toSlime(versionsObject.setObject("sourceApplication"), application.revisions().get(revision)));
}
private static void toSlime(Cursor blockersArray, Stream<ChangeBlocker> blockers) {
@@ -427,7 +420,7 @@ class JobControllerApiHandlerHelper {
return candidates;
}
- private static void toSlime(Cursor runsArray, Collection<Run> runs, int limit, URI baseUriForJob) {
+ private static void toSlime(Cursor runsArray, Collection<Run> runs, Application application, int limit, URI baseUriForJob) {
runs.stream().limit(limit).forEach(run -> {
Cursor runObject = runsArray.addObject();
runObject.setLong("id", run.id().number());
@@ -435,7 +428,7 @@ class JobControllerApiHandlerHelper {
runObject.setLong("start", run.start().toEpochMilli());
run.end().ifPresent(end -> runObject.setLong("end", end.toEpochMilli()));
runObject.setString("status", run.status().name());
- toSlime(runObject.setObject("versions"), run.versions());
+ toSlime(runObject.setObject("versions"), run.versions(), application);
Cursor runStepsArray = runObject.setArray("steps");
run.steps().forEach((step, info) -> {
Cursor runStepObject = runStepsArray.addObject();
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ServiceApiResponse.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ServiceApiResponse.java
deleted file mode 100644
index 67b47aa976a..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ServiceApiResponse.java
+++ /dev/null
@@ -1,197 +0,0 @@
-// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.application;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.container.jdisc.HttpResponse;
-import ai.vespa.http.HttpURL;
-import ai.vespa.http.HttpURL.Path;
-import ai.vespa.http.HttpURL.Query;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.JsonFormat;
-import com.yahoo.slime.Slime;
-import com.yahoo.vespa.serviceview.bindings.ApplicationView;
-import com.yahoo.vespa.serviceview.bindings.ClusterView;
-import com.yahoo.vespa.serviceview.bindings.ServiceView;
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.net.URI;
-import java.util.List;
-import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * A response containing a service view for an application deployment.
- * This does not define the API response but merely proxies the API response provided by Vespa, with URLs
- * rewritten to include zone and application information allow proxying through the controller
- *
- * @author Steinar Knutsen
- * @author bratseth
- */
-class ServiceApiResponse extends HttpResponse {
-
- private final ZoneId zone;
- private final ApplicationId application;
- private final List<URI> configServerURIs;
- private final Slime slime;
- private final HttpURL requestUri;
-
- // Only set for one of the setResponse calls
- private String serviceName = null;
- private Path restPath = null;
-
- public ServiceApiResponse(ZoneId zone, ApplicationId application, List<URI> configServerURIs, URI requestUri) {
- super(200);
- this.zone = zone;
- this.application = application;
- this.configServerURIs = configServerURIs;
- this.slime = new Slime();
- this.requestUri = HttpURL.from(requestUri).withQuery(Query.empty());
- }
-
- public void setResponse(ApplicationView applicationView) {
- Cursor clustersArray = slime.setObject().setArray("clusters");
- for (ClusterView clusterView : applicationView.clusters) {
- Cursor clusterObject = clustersArray.addObject();
- clusterObject.setString("name", clusterView.name);
- clusterObject.setString("type", clusterView.type);
- setNullableString("url", rewriteIfUrl(clusterView.url, requestUri), clusterObject);
- Cursor servicesArray = clusterObject.setArray("services");
- for (ServiceView serviceView : clusterView.services) {
- Cursor serviceObject = servicesArray.addObject();
- setNullableString("url", rewriteIfUrl(serviceView.url, requestUri), serviceObject);
- serviceObject.setString("serviceType", serviceView.serviceType);
- serviceObject.setString("serviceName", serviceView.serviceName);
- serviceObject.setString("configId", serviceView.configId);
- serviceObject.setString("host", serviceView.host);
- }
- }
- }
-
- public void setResponse(Map<?,?> responseData, String serviceName, Path restPath) {
- this.serviceName = serviceName;
- this.restPath = restPath;
- mapToSlime(responseData, slime.setObject());
- }
-
- @Override
- public void render(OutputStream stream) throws IOException {
- new JsonFormat(true).encode(stream, slime);
- }
-
- @Override
- public String getContentType() {
- return "application/json";
- }
-
- @SuppressWarnings("unchecked")
- private void mapToSlime(Map<?,?> data, Cursor object) {
- for (Map.Entry<String, Object> entry : ((Map<String, Object>)data).entrySet())
- fieldToSlime(entry.getKey(), entry.getValue(), object);
- }
-
- private void fieldToSlime(String key, Object value, Cursor object) {
- if (value instanceof String) {
- if (key.equals("url") || key.equals("link"))
- value = rewriteIfUrl((String)value, generateLocalLinkPrefix(serviceName, restPath));
- setNullableString(key, (String)value, object);
- }
- else if (value instanceof Integer) {
- object.setLong(key, (int)value);
- }
- else if (value instanceof Long) {
- object.setLong(key, (long)value);
- }
- else if (value instanceof Float) {
- object.setDouble(key, (double)value);
- }
- else if (value instanceof Double) {
- object.setDouble(key, (double)value);
- }
- else if (value instanceof List) {
- listToSlime((List)value, object.setArray(key));
- }
- else if (value instanceof Map) {
- mapToSlime((Map<?,?>)value, object.setObject(key));
- }
- }
-
- private void listToSlime(List<?> list, Cursor array) {
- for (Object entry : list)
- entryToSlime(entry, array);
- }
-
- private void entryToSlime(Object entry, Cursor array) {
- if (entry instanceof String)
- addNullableString(rewriteIfUrl((String)entry, generateLocalLinkPrefix(serviceName, restPath)), array);
- else if (entry instanceof Integer)
- array.addLong((long)entry);
- else if (entry instanceof Long)
- array.addLong((long)entry);
- else if (entry instanceof Float)
- array.addDouble((double)entry);
- else if (entry instanceof Double)
- array.addDouble((double)entry);
- else if (entry instanceof List)
- listToSlime((List)entry, array.addArray());
- else if (entry instanceof Map)
- mapToSlime((Map)entry, array.addObject());
- }
-
- private String rewriteIfUrl(String urlOrAnyString, HttpURL requestUri) {
- if (urlOrAnyString == null) return null;
-
- String hostPattern = "(" +
- String.join(
- "|", configServerURIs.stream()
- .map(URI::toString)
- .map(s -> s.substring(0, s.length() -1))
- .map(Pattern::quote)
- .toArray(String[]::new))
- + ")";
-
- String remoteServicePath = "/serviceview/"
- + "v1/tenant/" + application.tenant().value()
- + "/application/" + application.application().value()
- + "/environment/" + zone.environment().value()
- + "/region/" + zone.region().value()
- + "/instance/" + application.instance()
- + "/service/";
-
- Pattern remoteServiceResourcePattern = Pattern.compile("^(" + hostPattern + Pattern.quote(remoteServicePath) + ")");
- Matcher matcher = remoteServiceResourcePattern.matcher(urlOrAnyString);
-
- if (matcher.find()) {
- String proxiedPath = urlOrAnyString.substring(matcher.group().length());
- return requestUri.withPath(requestUri.path().append(Path.parse(proxiedPath))).asURI().toString();
- } else {
- return urlOrAnyString; // not a service url
- }
- }
-
- private HttpURL generateLocalLinkPrefix(String identifier, Path restPath) {
- Path proxiedPath = Path.parse(identifier).append(restPath);
- if (requestUri.path().tail(proxiedPath.length()).equals(proxiedPath)) {
- return requestUri.withPath(requestUri.path().cut(proxiedPath.length()));
- } else {
- throw new IllegalStateException("Expected the resource " + requestUri.path() + " to end with " + proxiedPath);
- }
- }
-
- private void setNullableString(String key, String valueOrNull, Cursor receivingObject) {
- if (valueOrNull == null)
- receivingObject.setNix(key);
- else
- receivingObject.setString(key, valueOrNull);
- }
-
- private void addNullableString(String valueOrNull, Cursor receivingArray) {
- if (valueOrNull == null)
- receivingArray.addNix();
- else
- receivingArray.addString(valueOrNull);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandler.java
index 45c4978ab9f..33307ea1677 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandler.java
@@ -19,10 +19,10 @@ import com.yahoo.vespa.hosted.controller.ApplicationController;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.TenantController;
import com.yahoo.vespa.hosted.controller.api.integration.billing.Bill;
+import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingController;
import com.yahoo.vespa.hosted.controller.api.integration.billing.CollectionMethod;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.PaymentInstrument;
import com.yahoo.vespa.hosted.controller.api.integration.billing.InstrumentOwner;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingController;
+import com.yahoo.vespa.hosted.controller.api.integration.billing.PaymentInstrument;
import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId;
import com.yahoo.vespa.hosted.controller.tenant.Tenant;
import com.yahoo.yolean.Exceptions;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2.java
index a5aca4adf10..0f3e5b7f76b 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2.java
@@ -28,15 +28,12 @@ import com.yahoo.vespa.hosted.controller.tenant.Tenant;
import javax.ws.rs.BadRequestException;
import java.math.BigDecimal;
import java.time.Clock;
-import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
-import java.time.format.DateTimeParseException;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
-import java.util.logging.Level;
/**
* @author ogronnesby
@@ -182,7 +179,6 @@ public class BillingApiHandlerV2 extends RestApiRequestHandler<BillingApiHandler
var tenant = tenants.require(tenantName, CloudTenant.class);
var untilAt = untilParameter(requestContext);
var usage = billing.createUncommittedBill(tenant.name(), untilAt);
-
var slime = new Slime();
usageToSlime(slime.setObject(), usage);
return slime;
@@ -322,7 +318,8 @@ public class BillingApiHandlerV2 extends RestApiRequestHandler<BillingApiHandler
private LocalDate untilParameter(RestApi.RequestContext ctx) {
return ctx.queryParameters().getString("until")
- .map(this::parseLocalDate)
+ .map(LocalDate::parse)
+ .map(date -> date.plusDays(1))
.orElseGet(this::tomorrow);
}
@@ -330,12 +327,6 @@ public class BillingApiHandlerV2 extends RestApiRequestHandler<BillingApiHandler
return LocalDate.now(clock).plusDays(1);
}
- private LocalDate parseLocalDate(String until) {
- if (until.isEmpty() || until.isBlank())
- return tomorrow();
- else return LocalDate.parse(until);
- }
-
private static String getInspectorFieldOrThrow(Inspector inspector, String field) {
if (!inspector.field(field).valid())
throw new BadRequestException("Field " + field + " cannot be null");
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/ChangeManagementApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/ChangeManagementApiHandler.java
index e1fc68974cc..c72d8ceb089 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/ChangeManagementApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/ChangeManagementApiHandler.java
@@ -280,7 +280,7 @@ public class ChangeManagementApiHandler extends AuditLoggingRequestHandler {
private Optional<ZoneId> affectedZone(List<String> hosts) {
NodeFilter affectedHosts = NodeFilter.all().hostnames(hosts.stream()
- .map(HostName::from)
+ .map(HostName::of)
.collect(Collectors.toSet()));
for (var zone : getProdZones()) {
var affectedHostsInZone = controller.serviceRegistry().configServer().nodeRepository().list(zone, affectedHosts);
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/ConfigServerApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/ConfigServerApiHandler.java
index 8caa741d737..467b0c094cc 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/ConfigServerApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/ConfigServerApiHandler.java
@@ -1,12 +1,12 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.restapi.configserver;
+import ai.vespa.http.HttpURL;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.config.provision.zone.ZoneList;
import com.yahoo.container.jdisc.HttpRequest;
import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.restapi.ErrorResponse;
-import ai.vespa.http.HttpURL;
import com.yahoo.restapi.Path;
import com.yahoo.restapi.SlimeJsonResponse;
import com.yahoo.slime.Cursor;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/AuditLogResponse.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/AuditLogResponse.java
index 7e250ce62ab..c3155406194 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/AuditLogResponse.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/AuditLogResponse.java
@@ -1,10 +1,10 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.restapi.controller;
+import com.yahoo.restapi.SlimeJsonResponse;
import com.yahoo.slime.Cursor;
import com.yahoo.slime.Slime;
import com.yahoo.vespa.hosted.controller.auditlog.AuditLog;
-import com.yahoo.restapi.SlimeJsonResponse;
/**
* @author mpolden
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/UpgraderResponse.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/UpgraderResponse.java
index e68517f7134..122ea94ab6c 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/UpgraderResponse.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/UpgraderResponse.java
@@ -1,10 +1,10 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.restapi.controller;
+import com.yahoo.restapi.SlimeJsonResponse;
import com.yahoo.slime.Cursor;
import com.yahoo.slime.Slime;
import com.yahoo.vespa.hosted.controller.maintenance.Upgrader;
-import com.yahoo.restapi.SlimeJsonResponse;
/**
* @author mpolden
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/BadgeApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/BadgeApiHandler.java
index eb74f931b2c..9b400fdfb78 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/BadgeApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/BadgeApiHandler.java
@@ -81,16 +81,14 @@ public class BadgeApiHandler extends ThreadedHttpRequestHandler {
() -> {
DeploymentStatus status = controller.jobController().deploymentStatus(controller.applications().requireApplication(TenantAndApplicationId.from(id)));
Predicate<JobStatus> isDeclaredJob = job -> status.jobSteps().get(job.id()) != null && status.jobSteps().get(job.id()).isDeclared();
- return Badges.overviewBadge(id,
- status.jobs().instance(id.instance()).matching(isDeclaredJob),
- controller.system());
+ return Badges.overviewBadge(id, status.jobs().instance(id.instance()).matching(isDeclaredJob));
});
}
/** Returns a URI which points to a history badge for the given application and job type. */
private HttpResponse historyBadge(String tenant, String application, String instance, String jobName, String historyLength) {
ApplicationId id = ApplicationId.from(tenant, application, instance);
- JobType type = JobType.fromJobName(jobName);
+ JobType type = JobType.fromJobName(jobName, controller.zoneRegistry());
int length = historyLength == null ? 5 : Math.min(32, Math.max(0, Integer.parseInt(historyLength)));
return cachedResponse(new Key(id, type, length),
controller.clock().instant(),
@@ -135,7 +133,7 @@ public class BadgeApiHandler extends ThreadedHttpRequestHandler {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Key key = (Key) o;
- return historyLength == key.historyLength && id.equals(key.id) && type == key.type;
+ return historyLength == key.historyLength && id.equals(key.id) && Objects.equals(type, key.type);
}
@Override
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/Badges.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/Badges.java
index 1fe5ebfa9a9..26a5da45bdb 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/Badges.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/Badges.java
@@ -15,6 +15,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.Optional;
import static java.util.stream.Collectors.toList;
@@ -51,14 +52,18 @@ public class Badges {
return widthOf(text, 11);
}
- static String colorOf(Run run, Boolean wasOk) {
+ static String colorOf(Run run, Optional<RunStatus> previous) {
switch (run.status()) {
- case running:
- return wasOk ? "url(#run-on-success)" : "url(#run-on-failure)";
- case success:
- return success;
- default:
- return failure;
+ case running: switch (previous.orElse(RunStatus.success)) {
+ case success: return "url(#run-on-success)";
+ case aborted:
+ case noTests: return "url(#run-on-warning)";
+ default: return "url(#run-on-failure)";
+ }
+ case success: return success;
+ case aborted:
+ case noTests: return warning;
+ default: return failure;
}
}
@@ -71,9 +76,10 @@ public class Badges {
static final double xPad = 6;
static final double logoSize = 16;
static final String dark = "#404040";
- static final String success = "#00f244";
+ static final String success = "#00f844";
static final String running = "#ab83ff";
static final String failure = "#bf103c";
+ static final String warning = "#bd890b";
static void addText(List<String> texts, String text, double x, double width) {
addText(texts, text, x, width, 11);
@@ -116,13 +122,11 @@ public class Badges {
.limit(length)
.collect(toList());
- boolean isOk = status.lastCompleted().map(run -> run.status() == RunStatus.success).orElse(true);
-
text = lastTriggered.id().type().jobName();
textWidth = widthOf(text);
dx = xPad + textWidth + xPad;
addShade(sections, x, dx);
- sections.add(" <rect x='" + (x - 6) + "' rx='3' width='" + (dx + 6) + "' height='20' fill='" + colorOf(lastTriggered, isOk) + "'/>\n");
+ sections.add(" <rect x='" + (x - 6) + "' rx='3' width='" + (dx + 6) + "' height='20' fill='" + colorOf(lastTriggered, status.lastStatus()) + "'/>\n");
addShadow(sections, x + dx);
addText(texts, text, x + dx / 2, textWidth);
x += dx;
@@ -130,7 +134,7 @@ public class Badges {
dx = xPad * (192.0 / (32 + runs.size())); // Broader sections with shorter history.
for (Run run : runs) {
addShade(sections, x, dx);
- sections.add(" <rect x='" + (x - 6) + "' rx='3' width='" + (dx + 6) + "' height='20' fill='" + colorOf(run, null) + "'/>\n");
+ sections.add(" <rect x='" + (x - 6) + "' rx='3' width='" + (dx + 6) + "' height='20' fill='" + colorOf(run, Optional.empty()) + "'/>\n");
addShadow(sections, x + dx);
dx *= Math.pow(0.3, 1.0 / (runs.size() + 8)); // Gradually narrowing sections with age.
x += dx;
@@ -140,7 +144,7 @@ public class Badges {
return badge(sections, texts, x);
}
- static String overviewBadge(ApplicationId id, JobList jobs, SystemName system) {
+ static String overviewBadge(ApplicationId id, JobList jobs) {
// Put production tests right after their deployments, for a more compact rendering.
List<Run> runs = new ArrayList<>(jobs.lastTriggered().asList());
boolean anyTest = false;
@@ -149,7 +153,7 @@ public class Badges {
if (run.id().type().isProduction() && run.id().type().isTest()) {
anyTest = true;
int j = i;
- while ( ! runs.get(j - 1).id().type().zone(system).equals(run.id().type().zone(system)))
+ while ( ! runs.get(j - 1).id().type().zone().equals(run.id().type().zone()))
runs.set(j, runs.get(--j));
runs.set(j, run);
}
@@ -179,7 +183,7 @@ public class Badges {
text = nameOf(run.id().type());
textWidth = widthOf(text, isTest ? 9 : 11);
dx = xPad + textWidth + (isTest ? 0 : xPad);
- boolean wasOk = jobs.get(run.id().job()).flatMap(JobStatus::lastStatus).map(RunStatus.success::equals).orElse(true);
+ Optional<RunStatus> previous = jobs.get(run.id().job()).flatMap(JobStatus::lastStatus);
addText(texts, text, x + (dx - (isTest ? xPad : 0)) / 2, textWidth, isTest ? 9 : 11);
@@ -197,10 +201,10 @@ public class Badges {
// Add colored section for job ...
if (test == null)
- sections.add(" <rect x='" + (x - 16) + "' rx='3' width='" + (dx + 16) + "' height='20' fill='" + colorOf(run, wasOk) + "'/>\n");
+ sections.add(" <rect x='" + (x - 16) + "' rx='3' width='" + (dx + 16) + "' height='20' fill='" + colorOf(run, previous) + "'/>\n");
// ... with a slant if a test is next.
else
- sections.add(" <polygon points='" + (x - 6) + " 0 " + (x - 6) + " 20 " + (x + dx - 7) + " 20 " + (x + dx + 1) + " 0' fill='" + colorOf(run, wasOk) + "'/>\n");
+ sections.add(" <polygon points='" + (x - 6) + " 0 " + (x - 6) + " 20 " + (x + dx - 7) + " 20 " + (x + dx + 1) + " 0' fill='" + colorOf(run, previous) + "'/>\n");
// Cast a shadow onto the next zone ...
if (test == null)
@@ -255,6 +259,13 @@ public class Badges {
" <animate attributeName='x1' values='-110%;150%;20%;-110%' dur='6s' repeatCount='indefinite' />\n" +
" <animate attributeName='x2' values='-10%;250%;120%;-10%' dur='6s' repeatCount='indefinite' />\n" +
" </linearGradient>\n" +
+ // Running color sloshing back and forth on top of the warning color.
+ " <linearGradient id='run-on-warning' x1='40%' x2='80%' y2='0%'>\n" +
+ " <stop offset='0' stop-color='" + running + "' />\n" +
+ " <stop offset='1' stop-color='" + warning + "' />\n" +
+ " <animate attributeName='x1' values='-110%;150%;20%;-110%' dur='6s' repeatCount='indefinite' />\n" +
+ " <animate attributeName='x2' values='-10%;250%;120%;-10%' dur='6s' repeatCount='indefinite' />\n" +
+ " </linearGradient>\n" +
// Running color sloshing back and forth on top of the success color.
" <linearGradient id='run-on-success' x1='40%' x2='80%' y2='0%'>\n" +
" <stop offset='0' stop-color='" + running + "' />\n" +
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 1a4a42cb521..effa0906b94 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
@@ -210,7 +210,7 @@ public class DeploymentApiHandler extends ThreadedHttpRequestHandler {
});
});
}
- JobType.allIn(controller.system()).stream()
+ JobType.allIn(controller.zoneRegistry()).stream()
.filter(job -> ! job.environment().isManuallyDeployed())
.map(JobType::jobName).forEach(root.setArray("jobs")::addString);
return new SlimeJsonResponse(slime);
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilter.java
index a7472ced09c..01bd02fdc13 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilter.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilter.java
@@ -21,6 +21,7 @@ import com.yahoo.vespa.hosted.controller.TenantController;
import com.yahoo.vespa.hosted.controller.api.integration.athenz.ApplicationAction;
import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactory;
import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
+import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry;
import com.yahoo.vespa.hosted.controller.api.role.Role;
import com.yahoo.vespa.hosted.controller.api.role.SecurityContext;
import com.yahoo.vespa.hosted.controller.athenz.impl.AthenzFacade;
@@ -62,6 +63,7 @@ public class AthenzRoleFilter extends JsonSecurityRequestFilterBase {
private final TenantController tenants;
private final ExecutorService executor;
private final SystemName systemName;
+ private final ZoneRegistry zones;
@Inject
public AthenzRoleFilter(AthenzClientFactory athenzClientFactory, Controller controller) {
@@ -69,6 +71,7 @@ public class AthenzRoleFilter extends JsonSecurityRequestFilterBase {
this.tenants = controller.tenants();
this.executor = Executors.newCachedThreadPool();
this.systemName = controller.system();
+ this.zones = controller.zoneRegistry();
}
@Override
@@ -108,8 +111,7 @@ public class AthenzRoleFilter extends JsonSecurityRequestFilterBase {
} else if(path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/{*}")) {
zone = Optional.of(ZoneId.from(path.get("environment"), path.get("region")));
} else if(path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/deploy/{jobname}")) {
- var jobtype= JobType.fromJobName(path.get("jobname"));
- zone = Optional.of(jobtype.zone(systemName));
+ zone = Optional.of(JobType.fromJobName(path.get("jobname"), zones).zone());
} else {
zone = Optional.empty();
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilter.java
index 08ec3caa829..7c695ef51d7 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilter.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilter.java
@@ -6,7 +6,6 @@ import com.yahoo.jdisc.Response;
import com.yahoo.jdisc.http.HttpRequest;
import com.yahoo.jdisc.http.filter.DiscFilterRequest;
import com.yahoo.jdisc.http.filter.security.base.JsonSecurityRequestFilterBase;
-import java.util.logging.Level;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.api.role.Action;
import com.yahoo.vespa.hosted.controller.api.role.Enforcer;
@@ -15,6 +14,7 @@ import com.yahoo.vespa.hosted.controller.api.role.SecurityContext;
import java.util.Optional;
import java.util.Set;
+import java.util.logging.Level;
import java.util.logging.Logger;
/**
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java
index 985919581ef..b7c77e7bfb4 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java
@@ -9,7 +9,6 @@ import com.yahoo.config.provision.ApplicationName;
import com.yahoo.config.provision.TenantName;
import com.yahoo.jdisc.http.filter.DiscFilterRequest;
import com.yahoo.jdisc.http.filter.security.base.JsonSecurityRequestFilterBase;
-import java.util.logging.Level;
import com.yahoo.security.KeyUtils;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.Controller;
@@ -24,6 +23,7 @@ import java.security.PublicKey;
import java.util.Base64;
import java.util.Optional;
import java.util.Set;
+import java.util.logging.Level;
import java.util.logging.Logger;
import static java.nio.charset.StandardCharsets.UTF_8;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java
index 33e6632b8e1..025e8dff659 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java
@@ -3,7 +3,6 @@ package com.yahoo.vespa.hosted.controller.restapi.user;
import com.google.inject.Inject;
import com.yahoo.config.provision.ApplicationName;
-import com.yahoo.config.provision.InstanceName;
import com.yahoo.config.provision.TenantName;
import com.yahoo.container.jdisc.HttpRequest;
import com.yahoo.container.jdisc.HttpResponse;
@@ -28,7 +27,7 @@ import com.yahoo.vespa.hosted.controller.LockedTenant;
import com.yahoo.vespa.hosted.controller.api.integration.billing.Plan;
import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId;
import com.yahoo.vespa.hosted.controller.api.integration.user.Roles;
-import com.yahoo.vespa.hosted.controller.api.integration.user.User;
+import com.yahoo.jdisc.http.filter.security.misc.User;
import com.yahoo.vespa.hosted.controller.api.integration.user.UserId;
import com.yahoo.vespa.hosted.controller.api.integration.user.UserManagement;
import com.yahoo.vespa.hosted.controller.api.role.Role;
@@ -172,7 +171,6 @@ public class UserApiHandler extends ThreadedHttpRequestHandler {
toSlime(root.setObject("user"), user);
Cursor tenants = root.setObject("tenants");
- InstanceName userInstance = InstanceName.from(user.nickname());
tenantRolesByTenantName.keySet().stream()
.sorted()
.forEach(tenant -> {
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
index 5b165a9ef37..320321080c8 100644
--- 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
@@ -7,13 +7,13 @@ import com.yahoo.config.provision.zone.ZoneApi;
import com.yahoo.container.jdisc.HttpRequest;
import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.container.jdisc.ThreadedHttpRequestHandler;
+import com.yahoo.restapi.ErrorResponse;
+import com.yahoo.restapi.Path;
+import com.yahoo.restapi.SlimeJsonResponse;
import com.yahoo.slime.Cursor;
import com.yahoo.slime.Slime;
import com.yahoo.vespa.hosted.controller.api.integration.ServiceRegistry;
import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry;
-import com.yahoo.restapi.ErrorResponse;
-import com.yahoo.restapi.Path;
-import com.yahoo.restapi.SlimeJsonResponse;
import com.yahoo.yolean.Exceptions;
import java.util.Comparator;
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
index 800588fdf8c..e3f1e9b5f94 100644
--- 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
@@ -1,13 +1,13 @@
// Copyright Yahoo. 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 ai.vespa.http.HttpURL;
import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.config.provision.zone.ZoneList;
import com.yahoo.container.jdisc.HttpRequest;
import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.container.jdisc.ThreadedHttpRequestHandler;
import com.yahoo.restapi.ErrorResponse;
-import ai.vespa.http.HttpURL;
import com.yahoo.restapi.Path;
import com.yahoo.restapi.SlimeJsonResponse;
import com.yahoo.slime.Cursor;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicies.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicies.java
index b98ef717dd3..a5a09ab6551 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicies.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicies.java
@@ -1,11 +1,12 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.routing;
+import ai.vespa.http.DomainName;
import com.yahoo.config.application.api.DeploymentSpec;
import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.HostName;
import com.yahoo.config.provision.zone.RoutingMethod;
import com.yahoo.config.provision.zone.ZoneId;
+import com.yahoo.transaction.Mutex;
import com.yahoo.vespa.curator.Lock;
import com.yahoo.vespa.hosted.controller.Application;
import com.yahoo.vespa.hosted.controller.Controller;
@@ -152,7 +153,7 @@ public class RoutingPolicies {
}
/** Update global DNS records for given policies */
- private void updateGlobalDnsOf(RoutingPolicyList instancePolicies, Set<ZoneId> inactiveZones, @SuppressWarnings("unused") Lock lock) {
+ private void updateGlobalDnsOf(RoutingPolicyList instancePolicies, Set<ZoneId> inactiveZones, @SuppressWarnings("unused") Mutex lock) {
Map<RoutingId, List<RoutingPolicy>> routingTable = instancePolicies.asInstanceRoutingTable();
for (Map.Entry<RoutingId, List<RoutingPolicy>> routeEntry : routingTable.entrySet()) {
RoutingId routingId = routeEntry.getKey();
@@ -215,7 +216,7 @@ public class RoutingPolicies {
}
var weightedTarget = new WeightedAliasTarget(policy.canonicalName(), policy.dnsZone().get(),
policy.id().zone(), weight);
- endpoints.computeIfAbsent(regionEndpoint, (k) -> new RegionEndpoint(new LatencyAliasTarget(HostName.from(regionEndpoint.dnsName()),
+ endpoints.computeIfAbsent(regionEndpoint, (k) -> new RegionEndpoint(new LatencyAliasTarget(DomainName.of(regionEndpoint.dnsName()),
policy.dnsZone().get(),
policy.id().zone())))
.zoneTargets()
@@ -225,7 +226,7 @@ public class RoutingPolicies {
}
- private void updateApplicationDnsOf(RoutingPolicyList routingPolicies, Set<ZoneId> inactiveZones, @SuppressWarnings("unused") Lock lock) {
+ private void updateApplicationDnsOf(RoutingPolicyList routingPolicies, Set<ZoneId> inactiveZones, @SuppressWarnings("unused") Mutex lock) {
// In the context of single deployment (which this is) there is only one routing policy per routing ID. I.e.
// there is no scenario where more than one deployment within an instance can be a member the same
// application-level endpoint. However, to allow this in the future the routing table remains
@@ -307,7 +308,7 @@ public class RoutingPolicies {
*
* @return the updated policies
*/
- private RoutingPolicyList storePoliciesOf(LoadBalancerAllocation allocation, RoutingPolicyList instancePolicies, @SuppressWarnings("unused") Lock lock) {
+ private RoutingPolicyList storePoliciesOf(LoadBalancerAllocation allocation, RoutingPolicyList instancePolicies, @SuppressWarnings("unused") Mutex lock) {
Map<RoutingPolicyId, RoutingPolicy> policies = new LinkedHashMap<>(instancePolicies.asMap());
for (LoadBalancer loadBalancer : allocation.loadBalancers) {
if (loadBalancer.hostname().isEmpty()) continue;
@@ -343,7 +344,7 @@ public class RoutingPolicies {
*
* @return the updated policies
*/
- private RoutingPolicyList removePoliciesUnreferencedBy(LoadBalancerAllocation allocation, RoutingPolicyList instancePolicies, @SuppressWarnings("unused") Lock lock) {
+ private RoutingPolicyList removePoliciesUnreferencedBy(LoadBalancerAllocation allocation, RoutingPolicyList instancePolicies, @SuppressWarnings("unused") Mutex lock) {
Map<RoutingPolicyId, RoutingPolicy> newPolicies = new LinkedHashMap<>(instancePolicies.asMap());
Set<RoutingPolicyId> activeIds = allocation.asPolicyIds();
RoutingPolicyList removable = instancePolicies.deployment(allocation.deployment)
@@ -363,7 +364,7 @@ public class RoutingPolicies {
}
/** Remove unreferenced instance endpoints from DNS */
- private void removeGlobalDnsUnreferencedBy(LoadBalancerAllocation allocation, RoutingPolicyList deploymentPolicies, @SuppressWarnings("unused") Lock lock) {
+ private void removeGlobalDnsUnreferencedBy(LoadBalancerAllocation allocation, RoutingPolicyList deploymentPolicies, @SuppressWarnings("unused") Mutex lock) {
Set<RoutingId> removalCandidates = new HashSet<>(deploymentPolicies.asInstanceRoutingTable().keySet());
Set<RoutingId> activeRoutingIds = instanceRoutingIds(allocation);
removalCandidates.removeAll(activeRoutingIds);
@@ -380,7 +381,7 @@ public class RoutingPolicies {
}
/** Remove unreferenced application endpoints in given allocation from DNS */
- private void removeApplicationDnsUnreferencedBy(LoadBalancerAllocation allocation, RoutingPolicyList deploymentPolicies, @SuppressWarnings("unused") Lock lock) {
+ private void removeApplicationDnsUnreferencedBy(LoadBalancerAllocation allocation, RoutingPolicyList deploymentPolicies, @SuppressWarnings("unused") Mutex lock) {
Map<RoutingId, List<RoutingPolicy>> routingTable = deploymentPolicies.asApplicationRoutingTable();
Set<RoutingId> removalCandidates = new HashSet<>(routingTable.keySet());
Set<RoutingId> activeRoutingIds = applicationRoutingIds(allocation);
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicy.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicy.java
index 34736c16a6b..1ccb3205816 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicy.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicy.java
@@ -1,8 +1,8 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.routing;
+import ai.vespa.http.DomainName;
import com.google.common.collect.ImmutableSortedSet;
-import com.yahoo.config.provision.HostName;
import com.yahoo.config.provision.SystemName;
import com.yahoo.config.provision.zone.RoutingMethod;
import com.yahoo.text.Text;
@@ -27,14 +27,14 @@ import java.util.Set;
public class RoutingPolicy {
private final RoutingPolicyId id;
- private final HostName canonicalName;
+ private final DomainName canonicalName;
private final Optional<String> dnsZone;
private final Set<EndpointId> instanceEndpoints;
private final Set<EndpointId> applicationEndpoints;
private final Status status;
/** DO NOT USE. Public for serialization purposes */
- public RoutingPolicy(RoutingPolicyId id, HostName canonicalName, Optional<String> dnsZone,
+ public RoutingPolicy(RoutingPolicyId id, DomainName canonicalName, Optional<String> dnsZone,
Set<EndpointId> instanceEndpoints, Set<EndpointId> applicationEndpoints, Status status) {
this.id = Objects.requireNonNull(id, "id must be non-null");
this.canonicalName = Objects.requireNonNull(canonicalName, "canonicalName must be non-null");
@@ -50,7 +50,7 @@ public class RoutingPolicy {
}
/** The canonical name for the load balancer this applies to (rhs of a CNAME or ALIAS record) */
- public HostName canonicalName() {
+ public DomainName canonicalName() {
return canonicalName;
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/rotation/Rotation.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/rotation/Rotation.java
index 0cf7101cac0..8eeab8c20e3 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/rotation/Rotation.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/rotation/Rotation.java
@@ -2,6 +2,7 @@
package com.yahoo.vespa.hosted.controller.routing.rotation;
import com.yahoo.text.Text;
+
import java.util.Objects;
/**
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/rotation/RotationLock.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/rotation/RotationLock.java
index 36a43f80e9a..39fc70aac64 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/rotation/RotationLock.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/rotation/RotationLock.java
@@ -1,6 +1,7 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.routing.rotation;
+import com.yahoo.transaction.Mutex;
import com.yahoo.vespa.curator.Lock;
import java.util.Objects;
@@ -12,9 +13,9 @@ import java.util.Objects;
*/
public class RotationLock implements AutoCloseable {
- private final Lock lock;
+ private final Mutex lock;
- RotationLock(Lock lock) {
+ RotationLock(Mutex lock) {
this.lock = Objects.requireNonNull(lock, "lock cannot be null");
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/support/access/SupportAccessControl.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/support/access/SupportAccessControl.java
index a4af9f8e268..27b61a4fd17 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/support/access/SupportAccessControl.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/support/access/SupportAccessControl.java
@@ -1,13 +1,12 @@
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.support.access;
-import com.yahoo.vespa.athenz.api.AthenzIdentity;
+import com.yahoo.transaction.Mutex;
import com.yahoo.vespa.athenz.api.AthenzUser;
import com.yahoo.vespa.curator.Lock;
import com.yahoo.vespa.hosted.controller.Controller;
import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import java.security.Principal;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.time.Period;
@@ -37,7 +36,7 @@ public class SupportAccessControl {
}
public SupportAccess disallow(DeploymentId deployment, String by) {
- try (Lock lock = controller.curator().lockSupportAccess(deployment)) {
+ try (Mutex lock = controller.curator().lockSupportAccess(deployment)) {
var now = controller.clock().instant();
SupportAccess supportAccess = forDeployment(deployment);
if (supportAccess.currentStatus(now).state() == NOT_ALLOWED) {
@@ -51,7 +50,7 @@ public class SupportAccessControl {
}
public SupportAccess allow(DeploymentId deployment, Instant until, String by) {
- try (Lock lock = controller.curator().lockSupportAccess(deployment)) {
+ try (Mutex lock = controller.curator().lockSupportAccess(deployment)) {
var now = controller.clock().instant();
if (until.isAfter(now.plus(MAX_SUPPORT_ACCESS_TIME))) {
throw new IllegalArgumentException("Support access cannot be allowed for more than 10 days");
@@ -63,7 +62,7 @@ public class SupportAccessControl {
}
public SupportAccess registerGrant(DeploymentId deployment, String by, X509Certificate certificate) {
- try (Lock lock = controller.curator().lockSupportAccess(deployment)) {
+ try (Mutex lock = controller.curator().lockSupportAccess(deployment)) {
var now = controller.clock().instant();
SupportAccess supportAccess = forDeployment(deployment);
if (certificate.getNotAfter().toInstant().isBefore(now)) {
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/ControllerVersion.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/ControllerVersion.java
deleted file mode 100644
index 5b293081dc2..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/ControllerVersion.java
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright Yahoo. 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.yahoo.component.Version;
-import com.yahoo.component.Vtag;
-
-import java.time.Instant;
-import java.util.Objects;
-
-/**
- * A controller's Vespa version and commit details.
- *
- * @author mpolden
- */
-public class ControllerVersion implements Comparable<ControllerVersion> {
-
- /** The current version of this controller */
- public static final ControllerVersion CURRENT = new ControllerVersion(Vtag.currentVersion, Vtag.commitSha,
- Vtag.commitDate);
-
- private final Version version;
- private final String commitSha;
- private final Instant commitDate;
-
- public ControllerVersion(Version version, String commitSha, Instant commitDate) {
- this.version = Objects.requireNonNull(version);
- this.commitSha = Objects.requireNonNull(commitSha);
- this.commitDate = Objects.requireNonNull(commitDate);
- }
-
- /** Vespa version */
- public Version version() {
- return version;
- }
-
- /** Commit SHA of this */
- public String commitSha() {
- return commitSha;
- }
-
- /** The time this was committed */
- public Instant commitDate() {
- return commitDate;
- }
-
- @Override
- public String toString() {
- return version + ", commit " + commitSha + " @ " + commitDate;
- }
-
- @Override
- public int compareTo(ControllerVersion o) {
- return version.compareTo(o.version);
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- ControllerVersion that = (ControllerVersion) o;
- return version.equals(that.version);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(version);
- }
-
-}
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 c1abe38a2a9..179a64d9491 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
@@ -9,7 +9,6 @@ import com.yahoo.vespa.hosted.controller.deployment.DeploymentStatusList;
import com.yahoo.vespa.hosted.controller.deployment.JobList;
import com.yahoo.vespa.hosted.controller.deployment.JobStatus;
import com.yahoo.vespa.hosted.controller.deployment.Run;
-import com.yahoo.vespa.hosted.controller.deployment.RunStatus;
import java.util.ArrayList;
import java.util.Collection;
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 3fa440e694e..117abd52193 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
@@ -4,6 +4,7 @@ package com.yahoo.vespa.hosted.controller.versions;
import com.yahoo.component.Version;
import com.yahoo.config.provision.HostName;
import com.yahoo.vespa.hosted.controller.Controller;
+import com.yahoo.vespa.hosted.controller.api.identifiers.ControllerVersion;
import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeFilter;
import com.yahoo.vespa.hosted.controller.application.ApplicationList;
import com.yahoo.vespa.hosted.controller.application.SystemApplication;
@@ -197,7 +198,7 @@ public class VersionStatus {
.add(controller.hostname());
} else {
for (String host : controller.curator().cluster()) {
- HostName hostname = HostName.from(host);
+ HostName hostname = HostName.of(host);
versions.computeIfAbsent(controller.curator().readControllerVersion(hostname), (k) -> new ArrayList<>())
.add(hostname);
}