diff options
Diffstat (limited to 'controller-server/src/main/java/com')
16 files changed, 215 insertions, 278 deletions
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 f046d23c5ce..c69fe5021e7 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 @@ -326,9 +326,18 @@ public class ApplicationController { ? triggered.sourceApplication().orElse(triggered.application()) : triggered.application(); - applicationPackage = application.get().deploymentJobs().deployedInternally() - ? new ApplicationPackage(applicationStore.getApplicationPackage(application.get().id(), applicationVersion.id())) - : new ApplicationPackage(artifactRepository.getApplicationPackage(application.get().id(), applicationVersion.id())); + try { + applicationPackage = application.get().deploymentJobs().deployedInternally() + ? new ApplicationPackage(applicationStore.getApplicationPackage(application.get().id(), applicationVersion.id())) + : new ApplicationPackage(artifactRepository.getApplicationPackage(application.get().id(), applicationVersion.id())); + } + catch (RuntimeException e) { // If application has switched deployment pipeline, artifacts stored prior to the switch are in the other artifact store. + log.info("Fetching application package for " + applicationId + " from alternate repository; it is now deployed " + + (application.get().deploymentJobs().deployedInternally() ? "internally" : "externally")); + applicationPackage = application.get().deploymentJobs().deployedInternally() + ? new ApplicationPackage(artifactRepository.getApplicationPackage(application.get().id(), applicationVersion.id())) + : new ApplicationPackage(applicationStore.getApplicationPackage(application.get().id(), applicationVersion.id())); + } validateRun(application.get(), zone, platformVersion, applicationVersion); } 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 eddf4e6b3a1..bd10a884213 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 @@ -113,6 +113,12 @@ public class LockedApplication { ownershipIssueId, metrics, rotation, rotationStatus); } + public LockedApplication withJobPause(JobType jobType, OptionalLong pausedUntil) { + return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, + deploymentJobs.withPause(jobType, pausedUntil), change, outstandingChange, + ownershipIssueId, metrics, rotation, rotationStatus); + } + public LockedApplication withJobCompletion(long projectId, JobType jobType, JobStatus.JobRun completion, Optional<DeploymentJobs.JobError> jobError) { return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides, deployments, 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 1fa579684de..0fb6459611c 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 @@ -69,6 +69,15 @@ public final class Change { return new Change(platform, Optional.of(applicationVersion)); } + /** 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()); + return other; + } + @Override public int hashCode() { return Objects.hash(platform, application); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentJobs.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentJobs.java index 23826a47931..6e9318042b5 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentJobs.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentJobs.java @@ -59,7 +59,7 @@ public class DeploymentJobs { if (job == null) job = JobStatus.initial(jobType); return job.withCompletion(completion, jobError); }); - return new DeploymentJobs(OptionalLong.of(projectId), status, issueId, builtInternally); + return new DeploymentJobs(jobType == JobType.component ? OptionalLong.of(projectId) : this.projectId, status, issueId, builtInternally); } public DeploymentJobs withTriggering(JobType jobType, JobStatus.JobRun jobRun) { @@ -71,6 +71,15 @@ public class DeploymentJobs { return new DeploymentJobs(projectId, status, issueId, builtInternally); } + public DeploymentJobs withPause(JobType jobType, OptionalLong pausedUntil) { + Map<JobType, JobStatus> status = new LinkedHashMap<>(this.status); + status.compute(jobType, (__, job) -> { + if (job == null) job = JobStatus.initial(jobType); + return job.withPause(pausedUntil); + }); + return new DeploymentJobs(projectId, status, issueId, builtInternally); + } + public DeploymentJobs withProjectId(OptionalLong projectId) { return new DeploymentJobs(projectId, status, issueId, builtInternally); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/JobStatus.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/JobStatus.java index a06a3e00340..59220d38821 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/JobStatus.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/JobStatus.java @@ -7,6 +7,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType; import java.time.Instant; import java.util.Objects; import java.util.Optional; +import java.util.OptionalLong; import static java.util.Objects.requireNonNull; @@ -25,6 +26,7 @@ public class JobStatus { private final Optional<JobRun> lastCompleted; private final Optional<JobRun> firstFailing; private final Optional<JobRun> lastSuccess; + private final OptionalLong pausedUntil; private final Optional<DeploymentJobs.JobError> jobError; @@ -34,27 +36,23 @@ public class JobStatus { */ public JobStatus(JobType type, Optional<DeploymentJobs.JobError> jobError, Optional<JobRun> lastTriggered, Optional<JobRun> lastCompleted, - Optional<JobRun> firstFailing, Optional<JobRun> lastSuccess) { - requireNonNull(type, "jobType cannot be null"); - requireNonNull(jobError, "jobError cannot be null"); - requireNonNull(lastTriggered, "lastTriggered cannot be null"); - requireNonNull(lastCompleted, "lastCompleted cannot be null"); - requireNonNull(firstFailing, "firstFailing cannot be null"); - requireNonNull(lastSuccess, "lastSuccess cannot be null"); - - this.type = type; - this.jobError = jobError; + Optional<JobRun> firstFailing, Optional<JobRun> lastSuccess, + OptionalLong pausedUntil) { + this.type = requireNonNull(type, "jobType cannot be null"); + this.jobError = requireNonNull(jobError, "jobError cannot be null"); // Never say we triggered component because we don't: - this.lastTriggered = type == JobType.component ? Optional.empty() : lastTriggered; - this.lastCompleted = lastCompleted; - this.firstFailing = firstFailing; - this.lastSuccess = lastSuccess; + this.lastTriggered = type == JobType.component ? Optional.empty() : requireNonNull(lastTriggered, "lastTriggered cannot be null"); + this.lastCompleted = requireNonNull(lastCompleted, "lastCompleted cannot be null"); + this.firstFailing = requireNonNull(firstFailing, "firstFailing cannot be null"); + this.lastSuccess = requireNonNull(lastSuccess, "lastSuccess cannot be null"); + this.pausedUntil = requireNonNull(pausedUntil, "pausedUntil cannot be null"); + } /** Returns an empty job status */ public static JobStatus initial(JobType type) { - return new JobStatus(type, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()); + return new JobStatus(type, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), OptionalLong.empty()); } public JobStatus withTriggering(Version platform, ApplicationVersion application, Optional<Deployment> deployment, String reason, Instant triggeredAt) { @@ -62,7 +60,7 @@ public class JobStatus { } public JobStatus withTriggering(JobRun jobRun) { - return new JobStatus(type, jobError, Optional.of(jobRun), lastCompleted, firstFailing, lastSuccess); + return new JobStatus(type, jobError, Optional.of(jobRun), lastCompleted, firstFailing, lastSuccess, OptionalLong.empty()); } public JobStatus withCompletion(long runId, Optional<DeploymentJobs.JobError> jobError, Instant completion) { @@ -80,7 +78,11 @@ public class JobStatus { firstFailing = Optional.empty(); } - return new JobStatus(type, jobError, lastTriggered, Optional.of(completion), firstFailing, lastSuccess); + return new JobStatus(type, jobError, lastTriggered, Optional.of(completion), firstFailing, lastSuccess, pausedUntil); + } + + public JobStatus withPause(OptionalLong pausedUntil) { + return new JobStatus(type, jobError, lastTriggered, lastCompleted, firstFailing, lastSuccess, pausedUntil); } public JobType type() { return type; } @@ -113,17 +115,21 @@ public class JobStatus { /** Returns the run when this last succeeded, or empty if it has never succeeded */ public Optional<JobRun> lastSuccess() { return lastSuccess; } + /** Returns the time until which this job is paused, if currently paused */ + public OptionalLong pausedUntil() { return pausedUntil; } + @Override public String toString() { return "job status of " + type + "[ " + "last triggered: " + lastTriggered.map(JobRun::toString).orElse("(never)") + ", last completed: " + lastCompleted.map(JobRun::toString).orElse("(never)") + ", first failing: " + firstFailing.map(JobRun::toString).orElse("(not failing)") + - ", lastSuccess: " + lastSuccess.map(JobRun::toString).orElse("(never)") + "]"; + ", lastSuccess: " + lastSuccess.map(JobRun::toString).orElse("(never)") + + ", pausedUntil: " + (pausedUntil.isPresent() ? pausedUntil.getAsLong() : "(not paused)") + "]"; } @Override - public int hashCode() { return Objects.hash(type, jobError, lastTriggered, lastCompleted, firstFailing, lastSuccess); } + public int hashCode() { return Objects.hash(type, jobError, lastTriggered, lastCompleted, firstFailing, lastSuccess, pausedUntil); } @Override public boolean equals(Object o) { @@ -135,7 +141,8 @@ public class JobStatus { Objects.equals(lastTriggered, other.lastTriggered) && Objects.equals(lastCompleted, other.lastCompleted) && Objects.equals(firstFailing, other.firstFailing) && - Objects.equals(lastSuccess, other.lastSuccess); + Objects.equals(lastSuccess, other.lastSuccess) && + Objects.equals(pausedUntil, other.pausedUntil); } /** Information about a particular triggering or completion of a run of a job. This is immutable. */ @@ -202,7 +209,7 @@ public class JobStatus { @Override public boolean equals(Object o) { if (this == o) return true; - if (!(o instanceof JobRun)) return false; + if ( ! (o instanceof JobRun)) return false; JobRun run = (JobRun) o; @@ -224,6 +231,7 @@ public class JobStatus { result = 31 * result + at.hashCode(); return result; } + } } 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 6dbdefb0913..69a42866a9e 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 @@ -66,6 +66,8 @@ import static java.util.stream.Collectors.toList; */ public class DeploymentTrigger { + public static final Duration maxPause = Duration.ofDays(3); + private final static Logger log = Logger.getLogger(DeploymentTrigger.class.getName()); private final Controller controller; @@ -222,31 +224,33 @@ public class DeploymentTrigger { .map(Job::jobType).collect(toList()); } + /** Prevents jobs of the given type from starting, until the given time. */ + public void pauseJob(ApplicationId id, JobType jobType, Instant until) { + if (until.isAfter(clock.instant().plus(maxPause))) + throw new IllegalArgumentException("Pause only allowed for up to " + maxPause); + + applications().lockOrThrow(id, application -> + applications().store(application.withJobPause(jobType, OptionalLong.of(until.toEpochMilli())))); + } + /** Triggers a change of this application, unless it already has a change. */ public void triggerChange(ApplicationId applicationId, Change change) { applications().lockOrThrow(applicationId, application -> { - if ( ! application.get().change().isPresent()) { - if (change.application().isPresent()) - application = application.withOutstandingChange(Change.empty()); - - applications().store(application.withChange(change)); - } + if ( ! application.get().change().isPresent()) + forceChange(applicationId, change); }); } /** Overrides the given application's platform and application changes with any contained in the given change. */ public void forceChange(ApplicationId applicationId, Change change) { applications().lockOrThrow(applicationId, application -> { - Change current = application.get().change(); - if (change.platform().isPresent()) - current = current.with(change.platform().get()); if (change.application().isPresent()) - current = current.with(change.application().get()); - applications().store(application.withChange(current)); + application = application.withOutstandingChange(Change.empty()); + applications().store(application.withChange(change.onTopOf(application.get().change()))); }); } - /** Cancels a platform upgrade of the given application, and an application upgrade as well if {@code keepApplicationChange}. */ + /** Cancels the indicated part of the given application's change. */ public void cancelChange(ApplicationId applicationId, ChangesToCancel cancellation) { applications().lockOrThrow(applicationId, application -> { Change change; @@ -313,7 +317,7 @@ public class DeploymentTrigger { for (Step step : steps.production()) { List<JobType> stepJobs = steps.toJobs(step); List<JobType> remainingJobs = stepJobs.stream().filter(job -> ! isComplete(change, application, job)).collect(toList()); - if (!remainingJobs.isEmpty()) { // Change is incomplete; trigger remaining jobs if ready, or their test jobs if untested. + if ( ! remainingJobs.isEmpty()) { // Change is incomplete; trigger remaining jobs if ready, or their test jobs if untested. for (JobType job : remainingJobs) { Versions versions = Versions.from(change, application, deploymentFor(application, job), controller.systemVersion()); @@ -321,7 +325,7 @@ public class DeploymentTrigger { if (completedAt.isPresent() && canTrigger(job, versions, application, stepJobs)) { jobs.add(deploymentJob(application, versions, change, job, reason, completedAt.get())); } - if (!alreadyTriggered(application, versions)) { + if ( ! alreadyTriggered(application, versions)) { testJobs = emptyList(); } } @@ -348,10 +352,7 @@ public class DeploymentTrigger { } } if (testJobs == null) { // If nothing to test, but outstanding commits, test those. - Change latestChange = application.outstandingChange().application().isPresent() - ? change.with(application.outstandingChange().application().get()) - : change; - testJobs = testJobs(application, Versions.from(latestChange, + testJobs = testJobs(application, Versions.from(application.outstandingChange().onTopOf(application.change()), application, steps.sortedDeployments(application.productionDeployments().values()).stream().findFirst(), controller.systemVersion()), @@ -392,11 +393,12 @@ public class DeploymentTrigger { /** Returns whether the given job can trigger at the given instant */ public boolean triggerAt(Instant instant, JobType job, Versions versions, Application application) { Optional<JobStatus> jobStatus = application.deploymentJobs().statusOf(job); - if (!jobStatus.isPresent()) return true; + if ( ! jobStatus.isPresent()) return true; + if (jobStatus.get().pausedUntil().isPresent() && jobStatus.get().pausedUntil().getAsLong() > clock.instant().toEpochMilli()) return false; if (jobStatus.get().isSuccess()) return true; // Success - if (!jobStatus.get().lastCompleted().isPresent()) return true; // Never completed - if (!jobStatus.get().firstFailing().isPresent()) return true; // Should not happen as firstFailing should be set for an unsuccessful job - if (!versions.targetsMatch(jobStatus.get().lastCompleted().get())) return true; // Always trigger as targets have changed + if ( ! jobStatus.get().lastCompleted().isPresent()) return true; // Never completed + if ( ! jobStatus.get().firstFailing().isPresent()) return true; // Should not happen as firstFailing should be set for an unsuccessful job + if ( ! versions.targetsMatch(jobStatus.get().lastCompleted().get())) return true; // Always trigger as targets have changed if (application.deploymentSpec().upgradePolicy() == DeploymentSpec.UpgradePolicy.canary) return true; // Don't throttle canaries Instant firstFailing = jobStatus.get().firstFailing().get().at(); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ClusterUtilizationMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ClusterUtilizationMaintainer.java index 44f29eb6113..ac080fe15d7 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ClusterUtilizationMaintainer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ClusterUtilizationMaintainer.java @@ -3,26 +3,17 @@ package com.yahoo.vespa.hosted.controller.maintenance; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ClusterSpec; -import com.yahoo.slime.Cursor; -import com.yahoo.slime.Slime; -import com.yahoo.vespa.config.SlimeUtils; import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneId; import com.yahoo.vespa.hosted.controller.Application; import com.yahoo.vespa.hosted.controller.Controller; import com.yahoo.vespa.hosted.controller.api.integration.MetricsService; +import com.yahoo.vespa.hosted.controller.application.ApplicationList; import com.yahoo.vespa.hosted.controller.application.ClusterUtilization; import com.yahoo.vespa.hosted.controller.application.Deployment; -import com.yahoo.vespa.hosted.controller.authority.config.ApiAuthorityConfig; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.entity.ByteArrayEntity; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; import java.time.Duration; import java.util.HashMap; -import java.util.List; import java.util.Map; -import java.util.logging.Level; /** * Fetch utilization metrics and update applications with this data. @@ -32,12 +23,10 @@ import java.util.logging.Level; public class ClusterUtilizationMaintainer extends Maintainer { private final Controller controller; - private final List<String> baseUris; - public ClusterUtilizationMaintainer(Controller controller, Duration duration, JobControl jobControl, ApiAuthorityConfig apiAuthorityConfig) { + public ClusterUtilizationMaintainer(Controller controller, Duration duration, JobControl jobControl) { super(controller, duration, jobControl); this.controller = controller; - this.baseUris = apiAuthorityConfig.authorities(); } private Map<ClusterSpec.Id, ClusterUtilization> getUpdatedClusterUtilizations(ApplicationId app, ZoneId zone) { @@ -55,45 +44,15 @@ public class ClusterUtilizationMaintainer extends Maintainer { @Override protected void maintain() { - try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) { - String uri = baseUris.get(0) + "metricforwarding/v1/clusterutilization"; // For now, we only feed to one controller - Slime slime = getMetricSlime(); - ByteArrayEntity entity = new ByteArrayEntity(SlimeUtils.toJsonBytes(slime)); - HttpPost httpPost = new HttpPost(uri); - httpPost.setEntity(entity); - httpClient.execute(httpPost); - } catch (Exception e) { - log.log(Level.WARNING, "Failed to update cluster utilization metrics", e); - } - - } - - private Slime getMetricSlime() { - Slime slime = new Slime(); - Cursor cursor = slime.setArray(); for (Application application : controller().applications().asList()) { - Cursor applicationCursor = cursor.addObject(); - applicationCursor.setString("applicationId", application.id().serializedForm()); - Cursor deploymentArray = applicationCursor.setArray("deployments"); for (Deployment deployment : application.deployments().values()) { - Cursor deploymentEntry = deploymentArray.addObject(); - deploymentEntry.setString("zoneId", deployment.zone().value()); - Cursor clusterArray = deploymentEntry.setArray("clusterUtil"); + Map<ClusterSpec.Id, ClusterUtilization> clusterUtilization = getUpdatedClusterUtilizations(application.id(), deployment.zone()); - fillClusterUtilization(clusterArray, clusterUtilization); + + controller().applications().lockIfPresent(application.id(), lockedApplication -> + controller().applications().store(lockedApplication.withClusterUtilization(deployment.zone(), clusterUtilization))); } } - return slime; } - private void fillClusterUtilization(Cursor cursor, Map<ClusterSpec.Id, ClusterUtilization> clusterUtilization) { - for (Map.Entry<ClusterSpec.Id, ClusterUtilization> entry : clusterUtilization.entrySet()) { - Cursor clusterUtilCursor = cursor.addObject(); - clusterUtilCursor.setString("clusterSpecId", entry.getKey().value()); - clusterUtilCursor.setDouble("cpu", entry.getValue().getCpu()); - clusterUtilCursor.setDouble("memory", entry.getValue().getMemory()); - clusterUtilCursor.setDouble("disk", entry.getValue().getDisk()); - clusterUtilCursor.setDouble("diskBusy", entry.getValue().getDiskBusy()); - } - } } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ContactInformationMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ContactInformationMaintainer.java index 3cf3c47a6f3..bf5743e2d3c 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ContactInformationMaintainer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ContactInformationMaintainer.java @@ -1,31 +1,17 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.controller.maintenance; -import com.yahoo.vespa.hosted.controller.authority.config.ApiAuthorityConfig; import com.yahoo.config.provision.SystemName; import com.yahoo.log.LogLevel; -import com.yahoo.slime.ArrayTraverser; -import com.yahoo.slime.Cursor; -import com.yahoo.slime.Inspector; -import com.yahoo.slime.Slime; -import com.yahoo.vespa.config.SlimeUtils; import com.yahoo.vespa.hosted.controller.Controller; -import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId; import com.yahoo.vespa.hosted.controller.api.integration.organization.Organization; import com.yahoo.vespa.hosted.controller.api.integration.organization.User; +import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant; import com.yahoo.vespa.hosted.controller.tenant.Contact; +import com.yahoo.vespa.hosted.controller.tenant.Tenant; import com.yahoo.yolean.Exceptions; -import org.apache.http.HttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.entity.ByteArrayEntity; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; -import org.apache.http.util.EntityUtils; -import java.io.IOException; import java.time.Duration; -import java.util.ArrayList; import java.util.EnumSet; import java.util.List; import java.util.Objects; @@ -43,107 +29,44 @@ public class ContactInformationMaintainer extends Maintainer { private static final Logger log = Logger.getLogger(ContactInformationMaintainer.class.getName()); private final Organization organization; - private final List<String> baseUris; - private CloseableHttpClient httpClient = HttpClientBuilder.create().build(); - public ContactInformationMaintainer(Controller controller, Duration interval, JobControl jobControl, Organization organization, ApiAuthorityConfig apiAuthorityConfig) { + public ContactInformationMaintainer(Controller controller, Duration interval, JobControl jobControl, Organization organization) { super(controller, interval, jobControl, null, EnumSet.of(SystemName.cd, SystemName.main)); this.organization = Objects.requireNonNull(organization, "organization must be non-null"); - this.baseUris = apiAuthorityConfig.authorities(); } - // The maintainer will eventually feed contact info to systems other than its own, determined by the baseUris list. @Override protected void maintain() { - for (String baseUri : baseUris) { - for (String tenantName : getTenantList(baseUri)) { - Optional<PropertyId> tenantPropertyId = getPropertyId(tenantName, baseUri); - if (!tenantPropertyId.isPresent()) - continue; - findContact(tenantPropertyId.get()).ifPresent(contact -> { - feedContact(tenantName, contact, baseUri); + for (Tenant t : controller().tenants().asList()) { + if (!(t instanceof AthenzTenant)) continue; // No contact information for non-Athenz tenants + AthenzTenant tenant = (AthenzTenant) t; + if (!tenant.propertyId().isPresent()) continue; // Can only update contact information if property ID is known + try { + findContact(tenant).ifPresent(contact -> { + controller().tenants().lockIfPresent(t.name(), lockedTenant -> controller().tenants().store(lockedTenant.with(contact))); }); + } catch (Exception e) { + log.log(LogLevel.WARNING, "Failed to update contact information for " + tenant + ": " + + Exceptions.toMessageString(e) + ". Retrying in " + + maintenanceInterval()); } } } - private void feedContact(String tenantName, Contact contact, String baseUri) { - try { - CloseableHttpClient httpClient = HttpClientBuilder.create().build(); - String uri = baseUri + "contactinfo/v1/tenant/" + tenantName; - HttpPost httpPost = new HttpPost(uri); - httpPost.setEntity(contactToByteArrayEntity(contact)); - httpClient.execute(httpPost); - } catch (Exception e) { - log.log(LogLevel.WARNING, "Failed to update contact information for " + tenantName + ": " + - Exceptions.toMessageString(e) + ". Retrying in " + - maintenanceInterval()); - } - } - - private ByteArrayEntity contactToByteArrayEntity(Contact contact) throws IOException { - Slime slime = new Slime(); - Cursor cursor = slime.setObject(); - cursor.setString("url", contact.url().toString()); - cursor.setString("issueTrackerUrl", contact.issueTrackerUrl().toString()); - cursor.setString("propertyUrl", contact.propertyUrl().toString()); - Cursor personsCursor = cursor.setArray("persons"); - for (List<String> personList : contact.persons()) { - Cursor sublist = personsCursor.addArray(); - for(String person : personList) { - sublist.addString(person); - } - } - return new ByteArrayEntity(SlimeUtils.toJsonBytes(slime)); - } - - private List<String> getTenantList(String baseUri) { - List<String> tenantList = new ArrayList<>(); - HttpGet getRequest = new HttpGet(baseUri + "application/v4/tenant/"); - try { - HttpResponse response = httpClient.execute(getRequest); - Slime slime = SlimeUtils.jsonToSlime(EntityUtils.toByteArray(response.getEntity())); - Inspector inspector = slime.get(); - inspector.traverse((ArrayTraverser) (index, tenant) -> { - tenantList.add(tenant.field("tenant").asString()); - }); - } catch (IOException e) { - log.log(LogLevel.WARNING, "Failed to get tenant list from base URI: " + baseUri.toString() + - Exceptions.toMessageString(e) + ". Retrying in " + - maintenanceInterval()); - } - return tenantList; - } - - private Optional<PropertyId> getPropertyId(String tenantName, String baseUri) { - Optional<PropertyId> propertyId = Optional.empty(); - HttpGet getRequest = new HttpGet(baseUri + "application/v4/tenant/" + tenantName); - try { - HttpResponse response = httpClient.execute(getRequest); - Slime slime = SlimeUtils.jsonToSlime(EntityUtils.toByteArray(response.getEntity())); - Inspector inspector = slime.get(); - if (!inspector.field("propertyId").valid()) { - log.log(LogLevel.WARNING, "Unable to get property id for " + tenantName); - return Optional.empty(); - } - propertyId = Optional.of(new PropertyId(inspector.field("propertyId").asString())); - } catch (IOException e) { - log.log(LogLevel.WARNING, "Unable to get property idfor " + tenantName, e); - } - return propertyId; - } - /** Find contact information for given tenant */ - private Optional<Contact> findContact(PropertyId propertyId) { - List<List<String>> persons = organization.contactsFor(propertyId) + private Optional<Contact> findContact(AthenzTenant tenant) { + if (!tenant.propertyId().isPresent()) { + return Optional.empty(); + } + List<List<String>> persons = organization.contactsFor(tenant.propertyId().get()) .stream() .map(personList -> personList.stream() .map(User::displayName) .collect(Collectors.toList())) .collect(Collectors.toList()); - return Optional.of(new Contact(organization.contactsUri(propertyId), - organization.propertyUri(propertyId), - organization.issueCreationUri(propertyId), + return Optional.of(new Contact(organization.contactsUri(tenant.propertyId().get()), + organization.propertyUri(tenant.propertyId().get()), + organization.issueCreationUri(tenant.propertyId().get()), persons)); } 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 b2968270af9..7b17f38bd78 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 @@ -65,15 +65,15 @@ public class ControllerMaintenance extends AbstractComponent { upgrader = new Upgrader(controller, maintenanceInterval, jobControl, curator); readyJobsTrigger = new ReadyJobsTrigger(controller, Duration.ofMinutes(1), jobControl); clusterInfoMaintainer = new ClusterInfoMaintainer(controller, Duration.ofHours(2), jobControl, nodeRepositoryClient); - clusterUtilizationMaintainer = new ClusterUtilizationMaintainer(controller, Duration.ofHours(2), jobControl, apiAuthorityConfig); - deploymentMetricsMaintainer = new DeploymentMetricsMaintainer(controller, Duration.ofMinutes(10), jobControl, apiAuthorityConfig); + clusterUtilizationMaintainer = new ClusterUtilizationMaintainer(controller, Duration.ofHours(2), jobControl); + deploymentMetricsMaintainer = new DeploymentMetricsMaintainer(controller, Duration.ofMinutes(10), jobControl); applicationOwnershipConfirmer = new ApplicationOwnershipConfirmer(controller, Duration.ofHours(12), jobControl, ownershipIssues); dnsMaintainer = new DnsMaintainer(controller, Duration.ofHours(12), jobControl, nameService); systemUpgrader = new SystemUpgrader(controller, Duration.ofMinutes(1), jobControl); jobRunner = new JobRunner(controller, Duration.ofMinutes(2), jobControl); osUpgraders = osUpgraders(controller, jobControl); osVersionStatusUpdater = new OsVersionStatusUpdater(controller, maintenanceInterval, jobControl); - contactInformationMaintainer = new ContactInformationMaintainer(controller, Duration.ofHours(12), jobControl, organization, apiAuthorityConfig); + contactInformationMaintainer = new ContactInformationMaintainer(controller, Duration.ofHours(12), jobControl, organization); } public Upgrader upgrader() { return upgrader; } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentMetricsMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentMetricsMaintainer.java index 67fb224f1ea..c17d070e92e 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentMetricsMaintainer.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentMetricsMaintainer.java @@ -2,23 +2,16 @@ package com.yahoo.vespa.hosted.controller.maintenance; import com.yahoo.config.provision.HostName; -import com.yahoo.slime.Cursor; -import com.yahoo.slime.Slime; -import com.yahoo.vespa.config.SlimeUtils; 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.api.integration.MetricsService; +import com.yahoo.vespa.hosted.controller.application.ApplicationList; import com.yahoo.vespa.hosted.controller.application.Deployment; +import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics; import com.yahoo.vespa.hosted.controller.application.RotationStatus; -import com.yahoo.vespa.hosted.controller.authority.config.ApiAuthorityConfig; import com.yahoo.yolean.Exceptions; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.entity.ByteArrayEntity; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; -import java.io.IOException; import java.time.Duration; import java.util.Collections; import java.util.List; @@ -45,12 +38,10 @@ public class DeploymentMetricsMaintainer extends Maintainer { private static final int applicationsToUpdateInParallel = 10; private final ApplicationController applications; - private final List<String> baseUris; - public DeploymentMetricsMaintainer(Controller controller, Duration duration, JobControl jobControl, ApiAuthorityConfig apiAuthorityConfig) { + public DeploymentMetricsMaintainer(Controller controller, Duration duration, JobControl jobControl) { super(controller, duration, jobControl); this.applications = controller.applications(); - baseUris = apiAuthorityConfig.authorities(); } @Override @@ -61,27 +52,37 @@ public class DeploymentMetricsMaintainer extends Maintainer { // Run parallel stream inside a custom ForkJoinPool so that we can control the number of threads used ForkJoinPool pool = new ForkJoinPool(applicationsToUpdateInParallel); - Slime slime = new Slime(); - Cursor cursor = slime.setArray(); pool.submit(() -> { applicationList.parallelStream().forEach(application -> { - Cursor applicationCursor = cursor.addObject(); - applicationCursor.setString("applicationId", application.id().serializedForm()); - Cursor applicationMetrics = applicationCursor.setObject("applicationMetrics"); - fillApplicationMetrics(applicationMetrics, application); - Cursor rotationStatus = applicationCursor.setArray("rotationStatus"); - fillRotationStatus(rotationStatus, application); - Cursor deploymentArray = applicationCursor.setArray("deploymentMetrics"); - for (Deployment deployment : application.deployments().values()) { - Cursor deploymentEntry = deploymentArray.addObject(); - fillDeploymentMetrics(deploymentEntry, application, deployment); + try { + applications.lockIfPresent(application.id(), locked -> + applications.store(locked.with(controller().metricsService().getApplicationMetrics(application.id())))); + + applications.lockIfPresent(application.id(), locked -> + applications.store(locked.withRotationStatus(rotationStatus(application)))); + + for (Deployment deployment : application.deployments().values()) { + MetricsService.DeploymentMetrics deploymentMetrics = controller().metricsService() + .getDeploymentMetrics(application.id(), deployment.zone()); + DeploymentMetrics newMetrics = new DeploymentMetrics(deploymentMetrics.queriesPerSecond(), + deploymentMetrics.writesPerSecond(), + deploymentMetrics.documentCount(), + deploymentMetrics.queryLatencyMillis(), + deploymentMetrics.writeLatencyMillis()); + + applications.lockIfPresent(application.id(), locked -> + applications.store(locked.with(deployment.zone(), newMetrics) + .recordActivityAt(controller().clock().instant(), deployment.zone()))); + } + } catch (Exception e) { + failures.incrementAndGet(); + lastException.set(e); } }); }); pool.shutdown(); try { pool.awaitTermination(30, TimeUnit.MINUTES); - feedMetrics(slime); if (lastException.get() != null) { log.log(Level.WARNING, String.format("Failed to query metrics service for %d/%d applications. Last error: %s. Retrying in %s", failures.get(), @@ -91,8 +92,6 @@ public class DeploymentMetricsMaintainer extends Maintainer { } } catch (InterruptedException e) { throw new RuntimeException(e); - } catch (IOException e) { - log.log(Level.WARNING, "Unable to feed metrics to API", e); } } @@ -108,40 +107,6 @@ public class DeploymentMetricsMaintainer extends Maintainer { .orElseGet(Collections::emptyMap); } - private void fillApplicationMetrics(Cursor applicationCursor, Application application) { - MetricsService.ApplicationMetrics metrics = controller().metricsService().getApplicationMetrics(application.id()); - applicationCursor.setDouble("queryServiceQuality", metrics.queryServiceQuality()); - applicationCursor.setDouble("writeServiceQuality", metrics.writeServiceQuality()); - } - - private void fillRotationStatus(Cursor rotationStatusCursor, Application application) { - Map<HostName, RotationStatus> rotationStatus = rotationStatus(application); - for (Map.Entry<HostName, RotationStatus> entry : rotationStatus.entrySet()) { - Cursor rotationStatusEntry = rotationStatusCursor.addObject(); - rotationStatusEntry.setString("hostname", entry.getKey().value()); - rotationStatusEntry.setString("rotationStatus", entry.getValue().toString()); - } - } - - private void fillDeploymentMetrics(Cursor deploymentCursor, Application application, Deployment deployment) { - MetricsService.DeploymentMetrics deploymentMetrics = controller().metricsService() - .getDeploymentMetrics(application.id(), deployment.zone()); - deploymentCursor.setString("zoneId", deployment.zone().value()); - deploymentCursor.setDouble("queriesPerSecond", deploymentMetrics.queriesPerSecond()); - deploymentCursor.setDouble("writesPerSecond", deploymentMetrics.writesPerSecond()); - deploymentCursor.setDouble("documentCount", deploymentMetrics.documentCount()); - deploymentCursor.setDouble("queryLatencyMillis", deploymentMetrics.queryLatencyMillis()); - deploymentCursor.setDouble("writeLatencyMillis", deploymentMetrics.writeLatencyMillis()); - } - - private void feedMetrics(Slime slime) throws IOException { - String uri = baseUris.get(0) + "/metricforwarding/v1/deploymentmetrics/"; // For now, we only feed to one controller - CloseableHttpClient httpClient = HttpClientBuilder.create().build(); - HttpPost httpPost = new HttpPost(uri); - httpPost.setEntity(new ByteArrayEntity(SlimeUtils.toJsonBytes(slime))); - httpClient.execute(httpPost); - } - private static RotationStatus from(com.yahoo.vespa.hosted.controller.api.integration.routing.RotationStatus status) { switch (status) { case IN: return RotationStatus.in; 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 7b615249a65..365d74babb5 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 @@ -95,6 +95,7 @@ public class ApplicationSerializer { private final String lastCompletedField = "lastCompleted"; private final String firstFailingField = "firstFailing"; private final String lastSuccessField = "lastSuccess"; + private final String pausedUntilField = "pausedUntil"; // JobRun fields private final String jobRunIdField = "id"; @@ -252,6 +253,7 @@ public class ApplicationSerializer { jobStatus.lastCompleted().ifPresent(run -> jobRunToSlime(run, object, lastCompletedField)); jobStatus.lastSuccess().ifPresent(run -> jobRunToSlime(run, object, lastSuccessField)); jobStatus.firstFailing().ifPresent(run -> jobRunToSlime(run, object, firstFailingField)); + jobStatus.pausedUntil().ifPresent(until -> object.setLong(pausedUntilField, until)); } private void jobRunToSlime(JobStatus.JobRun jobRun, Cursor parent, String jobRunObjectName) { @@ -440,11 +442,13 @@ public class ApplicationSerializer { if (object.field(errorField).valid()) jobError = Optional.of(JobError.valueOf(object.field(errorField).asString())); - return Optional.of(new JobStatus(jobType.get(), jobError, - jobRunFromSlime(object.field(lastTriggeredField)), - jobRunFromSlime(object.field(lastCompletedField)), - jobRunFromSlime(object.field(firstFailingField)), - jobRunFromSlime(object.field(lastSuccessField)))); + return Optional.of(new JobStatus(jobType.get(), + jobError, + jobRunFromSlime(object.field(lastTriggeredField)), + jobRunFromSlime(object.field(lastCompletedField)), + jobRunFromSlime(object.field(firstFailingField)), + jobRunFromSlime(object.field(lastSuccessField)), + optionalLong(object.field(pausedUntilField)))); } private Optional<JobStatus.JobRun> jobRunFromSlime(Inspector object) { 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 4c6baece7f1..dac65f16832 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 @@ -65,7 +65,7 @@ import com.yahoo.vespa.hosted.controller.application.JobStatus; import com.yahoo.vespa.hosted.controller.application.RotationStatus; import com.yahoo.vespa.hosted.controller.application.SourceRevision; import com.yahoo.vespa.hosted.controller.athenz.impl.ZmsClientFacade; -import com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger.ChangesToCancel; +import com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger; import com.yahoo.vespa.hosted.controller.restapi.ErrorResponse; import com.yahoo.vespa.hosted.controller.restapi.MessageResponse; import com.yahoo.vespa.hosted.controller.restapi.ResourceResponse; @@ -89,6 +89,7 @@ import java.net.URISyntaxException; import java.security.Principal; import java.time.DayOfWeek; import java.time.Duration; +import java.time.Instant; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -99,6 +100,7 @@ import java.util.logging.Level; import java.util.stream.Collectors; import static com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger.ChangesToCancel.ALL; +import static java.util.stream.Collectors.joining; /** * This implements the application/v4 API which is used to deploy and manage applications @@ -202,6 +204,8 @@ public class ApplicationApiHandler extends LoggingRequestHandler { if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying")) return deploy(path.get("tenant"), path.get("application"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/jobreport")) return notifyJobCompletion(path.get("tenant"), path.get("application"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/submit")) return submit(path.get("tenant"), path.get("application"), request); + if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/job/{jobtype}")) return trigger(appIdFromPath(path), jobTypeFromPath(path), request); + if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/job/{jobtype}/pause")) return pause(appIdFromPath(path), jobTypeFromPath(path)); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}")) return deploy(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}/deploy")) return deploy(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request); // legacy synonym of the above if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/restart")) return restart(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request); @@ -214,6 +218,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { if (path.matches("/application/v4/tenant/{tenant}")) return deleteTenant(path.get("tenant"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}")) return deleteApplication(path.get("tenant"), path.get("application"), request); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying")) return cancelDeploy(path.get("tenant"), path.get("application")); + if (path.matches("/application/v4/tenant/{tenant}/application/{application}/submit")) return JobControllerApiHandlerHelper.unregisterResponse(controller.jobController(), path.get("tenant"), path.get("application")); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/job/{jobtype}")) return JobControllerApiHandlerHelper.abortJobResponse(controller.jobController(), appIdFromPath(path), jobTypeFromPath(path)); if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}")) return deactivate(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}/global-rotation/override")) @@ -366,6 +371,19 @@ public class ApplicationApiHandler extends LoggingRequestHandler { return new SlimeJsonResponse(slime); } + private HttpResponse trigger(ApplicationId id, JobType type, HttpRequest request) { + String triggered = controller.applications().deploymentTrigger() + .forceTrigger(id, type, request.getJDiscRequest().getUserPrincipal().getName()) + .stream().map(JobType::jobName).collect(joining(", ")); + return new MessageResponse("Triggered " + triggered + " for " + id); + } + + private HttpResponse pause(ApplicationId id, JobType type) { + Instant until = controller.clock().instant().plus(DeploymentTrigger.maxPause); + controller.applications().deploymentTrigger().pauseJob(id, type, until); + return new MessageResponse(type.jobName() + " for " + id + " paused for " + DeploymentTrigger.maxPause); + } + private HashMap<String, String> getParameters(String query) { HashMap<String, String> keyValPair = new HashMap<>(); Arrays.stream(query.split("&")).forEach(pair -> { @@ -738,8 +756,11 @@ public class ApplicationApiHandler extends LoggingRequestHandler { return new SlimeJsonResponse(slime); } - /** Trigger deployment of the last built application package, on a given version */ - // TODO Consider move to API for maintenance related operations + /** + * Trigger deployment of the given Vespa version if a valid one is given, e.g., "7.8.9", + * or the latest known commit of the application if "commit" is given, + * or an upgrade to the system version if no data is provided. + */ private HttpResponse deploy(String tenantName, String applicationName, HttpRequest request) { ApplicationId id = ApplicationId.from(tenantName, applicationName, "default"); String requestVersion = readToString(request.getData()); @@ -758,17 +779,16 @@ public class ApplicationApiHandler extends LoggingRequestHandler { .stream() .map(VespaVersion::versionNumber) .map(Version::toString) - .collect(Collectors.joining(", "))); + .collect(joining(", "))); change = Change.of(version); } - controller.applications().deploymentTrigger().forceChange(application.get().id(), change); - response.append("Triggered " + change + " for " + application); + controller.applications().deploymentTrigger().forceChange(id, change); + response.append("Triggered " + change + " for " + id); }); return new MessageResponse(response.toString()); } /** Cancel any ongoing change for given application */ - // TODO Consider move to API for maintenance related operations private HttpResponse cancelDeploy(String tenantName, String applicationName) { ApplicationId id = ApplicationId.from(tenantName, applicationName, "default"); Application application = controller.applications().require(id); @@ -777,7 +797,7 @@ public class ApplicationApiHandler extends LoggingRequestHandler { return new MessageResponse("No deployment in progress for " + application + " at this time"); controller.applications().lockOrThrow(id, lockedApplication -> - controller.applications().deploymentTrigger().cancelChange(id, ALL)); + controller.applications().deploymentTrigger().cancelChange(id, ALL)); return new MessageResponse("Cancelled " + change + " for " + application); } @@ -895,9 +915,15 @@ public class ApplicationApiHandler extends LoggingRequestHandler { } } - private HttpResponse notifyJobCompletion(String tenant, String applicationName, HttpRequest request) { + private HttpResponse notifyJobCompletion(String tenant, String application, HttpRequest request) { try { - controller.applications().deploymentTrigger().notifyOfCompletion(toJobReport(tenant, applicationName, toSlime(request.getData()).get())); + DeploymentJobs.JobReport report = toJobReport(tenant, application, toSlime(request.getData()).get()); + if (controller.applications().require(report.applicationId()).deploymentJobs().deployedInternally()) + throw new IllegalArgumentException(report.applicationId() + " is set up to be deployed from internally, and no " + + "longer accepts reports from Screwdriver v3 jobs. If you need to revert " + + "to the old pipeline, please file a ticket at yo/vespa-support and request this."); + + controller.applications().deploymentTrigger().notifyOfCompletion(report); return new MessageResponse("ok"); } catch (IllegalStateException e) { return ErrorResponse.badRequest(Exceptions.toMessageString(e)); 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 0d02c4003e7..fa5d713d6b3 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 @@ -203,6 +203,8 @@ class JobControllerApiHandlerHelper { private static void jobTypeToSlime(Cursor jobObject, Controller controller, Application application, JobType type, DeploymentSteps steps, Map<JobType, Versions> pendingProduction, Map<JobType, Run> running, URI baseUriForJob) { + application.deploymentJobs().statusOf(type).ifPresent(status -> status.pausedUntil().ifPresent(until -> + jobObject.setLong("pausedUntil", until))); int runs = 0; Cursor runArray = jobObject.setArray("runs"); if (type.isTest()) { @@ -239,7 +241,9 @@ class JobControllerApiHandlerHelper { runObject.setString("status", "pending"); versionsToSlime(runObject, pendingProduction.get(type)); Cursor pendingObject = runObject.setObject("tasks"); - if ( ! controller.applications().deploymentTrigger().triggerAt(controller.clock().instant(), type, versions, application)) + if (application.deploymentJobs().statusOf(type).map(status -> status.pausedUntil().isPresent()).orElse(false)) + pendingObject.setString("paused", "pending"); + else if ( ! controller.applications().deploymentTrigger().triggerAt(controller.clock().instant(), type, versions, application)) pendingObject.setString("cooldown", "failed"); else { int pending = 0; @@ -421,5 +425,14 @@ class JobControllerApiHandlerHelper { return new SlimeJsonResponse(slime); } + /** Unregisters the application from the internal deployment pipeline. */ + static HttpResponse unregisterResponse(JobController jobs, String tenantName, String applicationName) { + ApplicationId id = ApplicationId.from(tenantName, applicationName, "default"); + jobs.unregister(id); + Slime slime = new Slime(); + slime.setObject().setString("message", "Unregistered '" + id + "' from internal deployment pipeline."); + return new SlimeJsonResponse(slime); + } + } 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 b9f91a35790..b1e3f8799d6 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 @@ -117,8 +117,7 @@ public class ControllerAuthorizationFilter extends CorsRequestFilterBase { private static boolean isHostedOperatorOperation(Path path, Method method) { if (isWhiteListedOperation(path, method)) return false; - return path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying") || - path.matches("/controller/v1/{*}") || + return path.matches("/controller/v1/{*}") || path.matches("/provision/v2/{*}") || path.matches("/screwdriver/v1/trigger/tenant/{*}") || path.matches("/os/v1/{*}") || @@ -131,7 +130,8 @@ public class ControllerAuthorizationFilter extends CorsRequestFilterBase { if (isHostedOperatorOperation(path, method)) return false; return path.matches("/application/v4/tenant/{tenant}") || path.matches("/application/v4/tenant/{tenant}/application/{application}") || - path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/job/{job}") || + path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying") || + path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/job/{job}/{*}") || path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/dev/{*}") || path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/perf/{*}") || path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/global-rotation/override"); diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/metrics/MetricForwardingApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/metrics/MetricForwardingApiHandler.java index 91c847d10f1..f3112b09173 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/metrics/MetricForwardingApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/metrics/MetricForwardingApiHandler.java @@ -26,6 +26,7 @@ import com.yahoo.vespa.hosted.controller.restapi.StringResponse; import java.io.IOException; import java.util.HashMap; import java.util.Map; +import java.util.logging.Level; /** * This implements the metricforwarding/v1 API which allows feeding @@ -72,7 +73,8 @@ public class MetricForwardingApiHandler extends LoggingRequestHandler { }); }); } catch (IOException e) { - ErrorResponse.badRequest("Unable to parse request for metrics - " + e.getMessage()); + log.log(Level.WARNING, "Unable to parse request for cluster utilization metrics", e); + return ErrorResponse.badRequest("Unable to parse request for cluster utilization metrics - " + e.getMessage()); } return new StringResponse("Added cluster utilization metrics"); } @@ -99,7 +101,8 @@ public class MetricForwardingApiHandler extends LoggingRequestHandler { } }); } catch (IOException e) { - ErrorResponse.badRequest("Unable to parse request for metrics - " + e.getMessage()); + log.log(Level.WARNING, "Unable to parse request for deployment metrics", e); + return ErrorResponse.badRequest("Unable to parse request for deployment metrics - " + e.getMessage()); } return new StringResponse("Added deployment metrics"); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/ScrewdriverApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/ScrewdriverApiHandler.java index 6633eafc509..14bb89520d7 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/ScrewdriverApiHandler.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/screwdriver/ScrewdriverApiHandler.java @@ -26,6 +26,7 @@ import java.util.logging.Logger; import static java.util.stream.Collectors.joining; +// TODO jvenstad: Only useful method has been moved to application API. Delete this when users have updated. /** * This API lists deployment jobs that are queued for execution on Screwdriver. * |