summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/ChangeRequest.java2
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/ChangeRequestSource.java2
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/HostAction.java57
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/VespaChangeRequest.java74
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeRequestMaintainer.java61
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ChangeRequestSerializer.java150
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java30
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/ChangeManagementApiHandler.java16
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeRequestMaintainerTest.java17
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ChangeRequestSerializerTest.java45
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/ChangeManagementApiHandlerTest.java31
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/responses/vcmrs.json40
12 files changed, 512 insertions, 13 deletions
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/ChangeRequest.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/ChangeRequest.java
index 31665c8ae0a..11adc1f7bb6 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/ChangeRequest.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/ChangeRequest.java
@@ -16,7 +16,7 @@ public class ChangeRequest {
private final Approval approval;
private final Impact impact;
- private ChangeRequest(String id, ChangeRequestSource changeRequestSource, List<String> impactedSwitches, List<String> impactedHosts, Approval approval, Impact impact) {
+ public ChangeRequest(String id, ChangeRequestSource changeRequestSource, List<String> impactedSwitches, List<String> impactedHosts, Approval approval, Impact impact) {
this.id = Objects.requireNonNull(id);
this.changeRequestSource = Objects.requireNonNull(changeRequestSource);
this.impactedSwitches = Objects.requireNonNull(impactedSwitches);
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/ChangeRequestSource.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/ChangeRequestSource.java
index 63f6c256766..632999ed1c3 100644
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/ChangeRequestSource.java
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/ChangeRequestSource.java
@@ -17,7 +17,7 @@ public class ChangeRequestSource {
private final ZonedDateTime plannedEndTime;
- private ChangeRequestSource(String system, String id, String url, Status status, ZonedDateTime plannedStartTime, ZonedDateTime plannedEndTime) {
+ public ChangeRequestSource(String system, String id, String url, Status status, ZonedDateTime plannedStartTime, ZonedDateTime plannedEndTime) {
this.system = Objects.requireNonNull(system);
this.id = Objects.requireNonNull(id);
this.url = Objects.requireNonNull(url);
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/HostAction.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/HostAction.java
new file mode 100644
index 00000000000..eeb7d89cbe7
--- /dev/null
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/HostAction.java
@@ -0,0 +1,57 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.api.integration.vcmr;
+
+import java.time.Instant;
+import java.util.Objects;
+
+/**
+ * @author olaa
+ *
+ */
+public class HostAction {
+
+ private final String hostname;
+ private final State state;
+ private final Instant lastUpdated;
+
+ public HostAction(String hostname, State state, Instant lastUpdated) {
+ this.hostname = hostname;
+ this.state = state;
+ this.lastUpdated = lastUpdated;
+ }
+
+ public String getHostname() {
+ return hostname;
+ }
+
+ public State getState() {
+ return state;
+ }
+
+ public Instant getLastUpdated() {
+ return lastUpdated;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ HostAction that = (HostAction) o;
+ return Objects.equals(hostname, that.hostname) &&
+ state == that.state &&
+ Objects.equals(lastUpdated, that.lastUpdated);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(hostname, state, lastUpdated);
+ }
+
+ public enum State {
+ REQUIRES_OPERATOR_ACTION,
+ PENDING_RETIREMENT,
+ RETIRING,
+ RETIRED,
+ COMPLETE
+ }
+}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/VespaChangeRequest.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/VespaChangeRequest.java
new file mode 100644
index 00000000000..1dda6bb099c
--- /dev/null
+++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/VespaChangeRequest.java
@@ -0,0 +1,74 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.api.integration.vcmr;
+
+import com.yahoo.config.provision.zone.ZoneId;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * @author olaa
+ */
+public class VespaChangeRequest extends ChangeRequest {
+
+ private final Status status;
+ private final ZoneId zoneId;
+ // TODO: Create applicationActionPlan
+ private final List<HostAction> hostActionPlan;
+
+ public VespaChangeRequest(String id, ChangeRequestSource changeRequestSource, List<String> impactedSwitches, List<String> impactedHosts, Approval approval, Impact impact, Status status, List<HostAction> hostActionPlan, ZoneId zoneId) {
+ super(id, changeRequestSource, impactedSwitches, impactedHosts, approval, impact);
+ this.status = status;
+ this.hostActionPlan = hostActionPlan;
+ this.zoneId = zoneId;
+ }
+ public VespaChangeRequest(ChangeRequest changeRequest, ZoneId zoneId) {
+ this(changeRequest.getId(), changeRequest.getChangeRequestSource(), changeRequest.getImpactedSwitches(),
+ changeRequest.getImpactedHosts(), changeRequest.getApproval(), changeRequest.getImpact(), Status.PENDING_ASSESSMENT, List.of(), zoneId);
+ }
+
+ public Status getStatus() {
+ return status;
+ }
+
+ public List<HostAction> getHostActionPlan() {
+ return hostActionPlan;
+ }
+
+ public ZoneId getZoneId() {
+ return zoneId;
+ }
+
+ public VespaChangeRequest withSource(ChangeRequestSource source) {
+ return new VespaChangeRequest(getId(), source, getImpactedSwitches(), getImpactedHosts(), getApproval(), getImpact(), status, hostActionPlan, zoneId);
+ }
+
+ public VespaChangeRequest withImpactedHosts(List<String> hosts) {
+ return new VespaChangeRequest(getId(), getChangeRequestSource(), getImpactedSwitches(), new ArrayList<>(hosts), getApproval(), getImpact(), status, hostActionPlan, zoneId);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ if (!super.equals(o)) return false;
+ VespaChangeRequest that = (VespaChangeRequest) o;
+ return status == that.status &&
+ Objects.equals(hostActionPlan, that.hostActionPlan) &&
+ Objects.equals(zoneId, that.zoneId);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(super.hashCode(), status, hostActionPlan, zoneId);
+ }
+
+ public enum Status {
+ COMPLETED,
+ IN_PROGRESS,
+ PENDING_ACTION,
+ PENDING_ASSESSMENT,
+ REQUIRES_OPERATOR_ACTION
+ }
+}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeRequestMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeRequestMaintainer.java
index ca9ebe132fd..c26e982d911 100644
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeRequestMaintainer.java
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeRequestMaintainer.java
@@ -1,13 +1,22 @@
// Copyright Verizon Media. 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.Environment;
import com.yahoo.config.provision.SystemName;
+import com.yahoo.config.provision.zone.ZoneId;
import com.yahoo.vespa.hosted.controller.Controller;
+import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeRepository;
import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequest;
import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequestClient;
+import com.yahoo.vespa.hosted.controller.api.integration.vcmr.VespaChangeRequest;
+import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
import java.time.Duration;
+import java.util.Collections;
import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
import java.util.function.Predicate;
import java.util.logging.Logger;
import java.util.stream.Collectors;
@@ -20,11 +29,15 @@ public class ChangeRequestMaintainer extends ControllerMaintainer {
private final Logger logger = Logger.getLogger(ChangeRequestMaintainer.class.getName());
private final ChangeRequestClient changeRequestClient;
private final SystemName system;
+ private final CuratorDb curator;
+ private final NodeRepository nodeRepository;
public ChangeRequestMaintainer(Controller controller, Duration interval) {
super(controller, interval, null, SystemName.allOf(Predicate.not(SystemName::isPublic)));
this.changeRequestClient = controller.serviceRegistry().changeRequestClient();
this.system = controller.system();
+ this.curator = controller.curator();
+ this.nodeRepository = controller.serviceRegistry().configServer().nodeRepository();
}
@@ -37,10 +50,11 @@ public class ChangeRequestMaintainer extends ControllerMaintainer {
changeRequests.forEach(changeRequest -> logger.info(changeRequest::toString));
}
- if (system.equals(SystemName.main))
+ if (system.equals(SystemName.main)) {
approveChanges(changeRequests);
+ storeChangeRequests(changeRequests);
+ }
- // TODO: Store in curator?
return true;
}
@@ -52,4 +66,47 @@ public class ChangeRequestMaintainer extends ControllerMaintainer {
changeRequestClient.approveChangeRequests(unapprovedRequests);
}
+
+ private void storeChangeRequests(List<ChangeRequest> changeRequests) {
+ var existingChangeRequests = curator.readChangeRequests()
+ .stream()
+ .collect(Collectors.toMap(ChangeRequest::getId, Function.identity()));
+
+ var hostsByZone = hostsByZone();
+ // Create or update requests in curator
+ try (var lock = curator.lockChangeRequests()) {
+ changeRequests.forEach(changeRequest -> {
+ var optionalZone = inferZone(changeRequest, hostsByZone);
+ optionalZone.ifPresent(zone -> {
+ var vcmr = existingChangeRequests
+ .getOrDefault(changeRequest.getId(), new VespaChangeRequest(changeRequest, zone))
+ .withSource(changeRequest.getChangeRequestSource());
+ curator.writeChangeRequest(vcmr);
+ });
+ });
+ }
+ }
+
+ private Map<ZoneId, List<String>> hostsByZone() {
+ return controller().zoneRegistry()
+ .zones()
+ .reachable()
+ .in(Environment.prod)
+ .ids()
+ .stream()
+ .collect(Collectors.toMap(
+ zone -> zone,
+ zone -> nodeRepository.list(zone, false)
+ .stream()
+ .map(node -> node.hostname().value())
+ .collect(Collectors.toList())
+ ));
+ }
+
+ private Optional<ZoneId> inferZone(ChangeRequest changeRequest, Map<ZoneId, List<String>> hostsByZone) {
+ return hostsByZone.entrySet().stream()
+ .filter(entry -> !Collections.disjoint(entry.getValue(), changeRequest.getImpactedHosts()))
+ .map(Map.Entry::getKey)
+ .findFirst();
+ }
}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ChangeRequestSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ChangeRequestSerializer.java
new file mode 100644
index 00000000000..407eb5ad5ab
--- /dev/null
+++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ChangeRequestSerializer.java
@@ -0,0 +1,150 @@
+// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.hosted.controller.persistence;
+
+import com.yahoo.config.provision.zone.ZoneId;
+import com.yahoo.slime.ArrayTraverser;
+import com.yahoo.slime.Cursor;
+import com.yahoo.slime.Inspector;
+import com.yahoo.slime.Slime;
+import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequest;
+import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequestSource;
+import com.yahoo.vespa.hosted.controller.api.integration.vcmr.HostAction;
+import com.yahoo.vespa.hosted.controller.api.integration.vcmr.VespaChangeRequest;
+
+import java.time.Instant;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author olaa
+ */
+public class ChangeRequestSerializer {
+
+ // WARNING: Since there are multiple servers in a ZooKeeper cluster and they upgrade one by one
+ // (and rewrite all nodes on startup), changes to the serialized format must be made
+ // such that what is serialized on version N+1 can be read by version N:
+ // - ADDING FIELDS: Always ok
+ // - 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.
+
+ private static final String ID_FIELD = "id";
+ private static final String SOURCE_FIELD = "source";
+ private static final String SOURCE_SYSTEM_FIELD = "system";
+ private static final String STATUS_FIELD = "status";
+ private static final String URL_FIELD = "url";
+ private static final String ZONE_FIELD = "zoneId";
+ private static final String START_TIME_FIELD = "plannedStartTime";
+ private static final String END_TIME_FIELD = "plannedEndTime";
+ private static final String APPROVAL_FIELD = "approval";
+ private static final String IMPACT_FIELD = "impact";
+ private static final String IMPACTED_HOSTS_FIELD = "impactedHosts";
+ private static final String IMPACTED_SWITCHES_FIELD = "impactedSwitches";
+ private static final String ACTION_PLAN_FIELD = "actionPlan";
+ private static final String HOST_FIELD = "hostname";
+ private static final String ACTION_STATE_FIELD = "state";
+ private static final String LAST_UPDATED_FIELD = "lastUpdated";
+ private static final String HOSTS_FIELD = "hosts";
+
+
+ public static VespaChangeRequest fromSlime(Slime slime) {
+ var inspector = slime.get();
+ var id = inspector.field(ID_FIELD).asString();
+ var zoneId = ZoneId.from(inspector.field(ZONE_FIELD).asString());
+ var changeRequestSource = readChangeRequestSource(inspector.field(SOURCE_FIELD));
+ var actionPlan = readHostActionPlan(inspector.field(ACTION_PLAN_FIELD));
+ var status = VespaChangeRequest.Status.valueOf(inspector.field(STATUS_FIELD).asString());
+ var impact = ChangeRequest.Impact.valueOf(inspector.field(IMPACT_FIELD).asString());
+ var approval = ChangeRequest.Approval.valueOf(inspector.field(APPROVAL_FIELD).asString());
+
+ var impactedHosts = new ArrayList<String>();
+ inspector.field(IMPACTED_HOSTS_FIELD)
+ .traverse((ArrayTraverser) (i, hostname) -> impactedHosts.add(hostname.asString()));
+ var impactedSwitches = new ArrayList<String>();
+ inspector.field(IMPACTED_SWITCHES_FIELD)
+ .traverse((ArrayTraverser) (i, switchName) -> impactedSwitches.add(switchName.asString()));
+
+ return new VespaChangeRequest(
+ id,
+ changeRequestSource,
+ impactedSwitches,
+ impactedHosts,
+ approval,
+ impact,
+ status,
+ actionPlan,
+ zoneId);
+ }
+
+ public static Slime toSlime(VespaChangeRequest changeRequest) {
+ var slime = new Slime();
+ writeChangeRequest(slime.setObject(), changeRequest);
+ return slime;
+ }
+
+ public static void writeChangeRequest(Cursor cursor, VespaChangeRequest changeRequest) {
+ cursor.setString(ID_FIELD, changeRequest.getId());
+ cursor.setString(STATUS_FIELD, changeRequest.getStatus().name());
+ cursor.setString(IMPACT_FIELD, changeRequest.getImpact().name());
+ cursor.setString(APPROVAL_FIELD, changeRequest.getApproval().name());
+ cursor.setString(ZONE_FIELD, changeRequest.getZoneId().value());
+ writeChangeRequestSource(cursor.setObject(SOURCE_FIELD), changeRequest.getChangeRequestSource());
+ writeActionPlan(cursor.setObject(ACTION_PLAN_FIELD), changeRequest);
+
+ var impactedHosts = cursor.setArray(IMPACTED_HOSTS_FIELD);
+ changeRequest.getImpactedHosts().forEach(impactedHosts::addString);
+ var impactedSwitches = cursor.setArray(IMPACTED_SWITCHES_FIELD);
+ changeRequest.getImpactedSwitches().forEach(impactedSwitches::addString);
+ }
+
+ private static void writeActionPlan(Cursor cursor, VespaChangeRequest changeRequest) {
+ var hostsCursor = cursor.setArray(HOSTS_FIELD);
+
+ changeRequest.getHostActionPlan().forEach(action -> {
+ var actionCursor = hostsCursor.addObject();
+ actionCursor.setString(HOST_FIELD, action.getHostname());
+ actionCursor.setString(ACTION_STATE_FIELD, action.getState().name());
+ actionCursor.setString(LAST_UPDATED_FIELD, action.getLastUpdated().toString());
+ });
+
+ // TODO: Add action plan per application
+ }
+
+ private static void writeChangeRequestSource(Cursor cursor, ChangeRequestSource source) {
+ cursor.setString(SOURCE_SYSTEM_FIELD, source.getSystem());
+ cursor.setString(ID_FIELD, source.getId());
+ cursor.setString(URL_FIELD, source.getUrl());
+ cursor.setString(START_TIME_FIELD, source.getPlannedStartTime().toString());
+ cursor.setString(END_TIME_FIELD, source.getPlannedEndTime().toString());
+ cursor.setString(STATUS_FIELD, source.getStatus().name());
+ }
+
+ private static ChangeRequestSource readChangeRequestSource(Inspector inspector) {
+ return new ChangeRequestSource(
+ inspector.field(SOURCE_SYSTEM_FIELD).asString(),
+ inspector.field(ID_FIELD).asString(),
+ inspector.field(URL_FIELD).asString(),
+ ChangeRequestSource.Status.valueOf(inspector.field(STATUS_FIELD).asString()),
+ ZonedDateTime.parse(inspector.field(START_TIME_FIELD).asString()),
+ ZonedDateTime.parse(inspector.field(END_TIME_FIELD).asString())
+ );
+ }
+
+ private static List<HostAction> readHostActionPlan(Inspector inspector) {
+ if (!inspector.valid())
+ return List.of();
+
+ var actionPlan = new ArrayList<HostAction>();
+ inspector.field(HOSTS_FIELD).traverse((ArrayTraverser) (index, hostObject) ->
+ actionPlan.add(
+ new HostAction(
+ hostObject.field(HOST_FIELD).asString(),
+ HostAction.State.valueOf(hostObject.field(ACTION_STATE_FIELD).asString()),
+ Instant.parse(hostObject.field(LAST_UPDATED_FIELD).asString())
+ )
+ )
+ );
+ return actionPlan;
+ }
+
+}
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 34741bcaedf..aea0eebd374 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
@@ -24,6 +24,7 @@ import com.yahoo.vespa.hosted.controller.auditlog.AuditLog;
import com.yahoo.vespa.hosted.controller.deployment.Run;
import com.yahoo.vespa.hosted.controller.deployment.Step;
import com.yahoo.vespa.hosted.controller.dns.NameServiceQueue;
+import com.yahoo.vespa.hosted.controller.api.integration.vcmr.VespaChangeRequest;
import com.yahoo.vespa.hosted.controller.routing.GlobalRouting;
import com.yahoo.vespa.hosted.controller.routing.RoutingPolicy;
import com.yahoo.vespa.hosted.controller.routing.RoutingPolicyId;
@@ -39,7 +40,6 @@ import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.time.Duration;
-import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -54,7 +54,6 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeoutException;
import java.util.function.Function;
import java.util.function.Predicate;
-import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
@@ -89,6 +88,7 @@ public class CuratorDb {
private static final Path zoneRoutingPoliciesRoot = root.append("zoneRoutingPolicies");
private static final Path endpointCertificateRoot = root.append("applicationCertificates");
private static final Path archiveBucketsRoot = root.append("archiveBuckets");
+ private static final Path changeRequestsRoot = root.append("changeRequests");
private final NodeVersionSerializer nodeVersionSerializer = new NodeVersionSerializer();
private final VersionStatusSerializer versionStatusSerializer = new VersionStatusSerializer(nodeVersionSerializer);
@@ -204,6 +204,10 @@ public class CuratorDb {
return curator.lock(lockRoot.append("archiveBuckets").append(zoneId.value()), defaultLockTimeout);
}
+ public Lock lockChangeRequests() {
+ return curator.lock(lockRoot.append("changeRequests"), defaultLockTimeout);
+ }
+
// -------------- Helpers ------------------------------------------
/** Try locking with a low timeout, meaning it is OK to fail lock acquisition.
@@ -563,6 +567,24 @@ public class CuratorDb {
curator.set(archiveBucketsPath(zoneid), asJson(ArchiveBucketsSerializer.toSlime(archiveBuckets)));
}
+ // -------------- VCMRs ---------------------------------------------------
+
+ public Optional<VespaChangeRequest> readChangeRequest(String changeRequestId) {
+ return readSlime(changeRequestPath(changeRequestId)).map(ChangeRequestSerializer::fromSlime);
+ }
+
+ public List<VespaChangeRequest> readChangeRequests() {
+ return curator.getChildren(changeRequestsRoot)
+ .stream()
+ .map(this::readChangeRequest)
+ .flatMap(Optional::stream)
+ .collect(Collectors.toList());
+ }
+
+ public void writeChangeRequest(VespaChangeRequest changeRequest) {
+ curator.set(changeRequestPath(changeRequest.getId()), asJson(ChangeRequestSerializer.toSlime(changeRequest)));
+ }
+
// -------------- Paths ---------------------------------------------------
private Path lockPath(TenantName tenant) {
@@ -688,4 +710,8 @@ public class CuratorDb {
return archiveBucketsRoot.append(zoneId.value());
}
+ private static Path changeRequestPath(String id) {
+ return changeRequestsRoot.append(id);
+ }
+
}
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 2077278ee0c..5973cc3fcf3 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
@@ -20,6 +20,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequest;
import com.yahoo.vespa.hosted.controller.auditlog.AuditLoggingRequestHandler;
import com.yahoo.vespa.hosted.controller.maintenance.ChangeManagementAssessor;
+import com.yahoo.vespa.hosted.controller.persistence.ChangeRequestSerializer;
import com.yahoo.yolean.Exceptions;
import javax.ws.rs.BadRequestException;
@@ -63,6 +64,7 @@ public class ChangeManagementApiHandler extends AuditLoggingRequestHandler {
private HttpResponse get(HttpRequest request) {
Path path = new Path(request.getUri());
if (path.matches("/changemanagement/v1/assessment/{changeRequestId}")) return changeRequestAssessment(path.get("changeRequestId"));
+ if (path.matches("/changemanagement/v1/vcmr")) return getVCMRs();
return ErrorResponse.notFoundError("Nothing at " + path);
}
@@ -87,8 +89,7 @@ public class ChangeManagementApiHandler extends AuditLoggingRequestHandler {
}
private HttpResponse changeRequestAssessment(String changeRequestId) {
- var optionalChangeRequest = controller.serviceRegistry().changeRequestClient()
- .getUpcomingChangeRequests()
+ var optionalChangeRequest = controller.curator().readChangeRequests()
.stream()
.filter(request -> changeRequestId.equals(request.getChangeRequestSource().getId()))
.findFirst();
@@ -171,6 +172,17 @@ public class ChangeManagementApiHandler extends AuditLoggingRequestHandler {
return new SlimeJsonResponse(slime);
}
+ private HttpResponse getVCMRs() {
+ var changeRequests = controller.curator().readChangeRequests();
+ var slime = new Slime();
+ var cursor = slime.setObject().setArray("vcmrs");
+ changeRequests.forEach(changeRequest -> {
+ var changeCursor = cursor.addObject();
+ ChangeRequestSerializer.writeChangeRequest(changeCursor, changeRequest);
+ });
+ return new SlimeJsonResponse(slime);
+ }
+
private Optional<ZoneId> affectedZone(List<String> hosts) {
var affectedHosts = hosts.stream()
.map(HostName::from)
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeRequestMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeRequestMaintainerTest.java
index 1ce59587d6c..183130a8ec8 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeRequestMaintainerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeRequestMaintainerTest.java
@@ -1,10 +1,12 @@
// Copyright Verizon Media. 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.ControllerTester;
import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequest;
import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequestSource;
import com.yahoo.vespa.hosted.controller.api.integration.vcmr.MockChangeRequestClient;
+import com.yahoo.vespa.hosted.controller.api.integration.vcmr.VespaChangeRequest;
import org.junit.Test;
import java.time.Duration;
@@ -24,10 +26,11 @@ public class ChangeRequestMaintainerTest {
@Test
public void only_approve_requests_pending_approval() {
-
+ var changeRequest1 = newChangeRequest("id1", ChangeRequest.Approval.APPROVED);
+ var changeRequest2 = newChangeRequest("id2", ChangeRequest.Approval.REQUESTED);
var upcomingChangeRequests = List.of(
- newChangeRequest("id1", ChangeRequest.Approval.APPROVED),
- newChangeRequest("id2", ChangeRequest.Approval.REQUESTED)
+ changeRequest1,
+ changeRequest2
);
changeRequestClient.setUpcomingChangeRequests(upcomingChangeRequests);
@@ -37,6 +40,11 @@ public class ChangeRequestMaintainerTest {
assertEquals(1, approvedChangeRequests.size());
assertEquals("id2", approvedChangeRequests.get(0).getId());
+ var writtenChangeRequests = tester.curator().readChangeRequests();
+ assertEquals(2, writtenChangeRequests.size());
+
+ var expectedChangeRequest = new VespaChangeRequest(changeRequest1, ZoneId.from("prod.us-east-3"));
+ assertEquals(expectedChangeRequest, writtenChangeRequests.get(0));
}
private ChangeRequest newChangeRequest(String id, ChangeRequest.Approval approval) {
@@ -45,7 +53,7 @@ public class ChangeRequestMaintainerTest {
.approval(approval)
.impact(ChangeRequest.Impact.VERY_HIGH)
.impactedSwitches(List.of())
- .impactedHosts(List.of())
+ .impactedHosts(List.of("node-1-tenant-host-prod.us-east-3"))
.changeRequestSource(new ChangeRequestSource.Builder()
.plannedStartTime(ZonedDateTime.now())
.plannedEndTime(ZonedDateTime.now())
@@ -55,6 +63,5 @@ public class ChangeRequestMaintainerTest {
.status(ChangeRequestSource.Status.CLOSED)
.build())
.build();
-
}
}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ChangeRequestSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ChangeRequestSerializerTest.java
new file mode 100644
index 00000000000..40a045c44cf
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ChangeRequestSerializerTest.java
@@ -0,0 +1,45 @@
+package com.yahoo.vespa.hosted.controller.persistence;
+
+import com.yahoo.config.provision.zone.ZoneId;
+import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequest;
+import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequestSource;
+import com.yahoo.vespa.hosted.controller.api.integration.vcmr.HostAction;
+import com.yahoo.vespa.hosted.controller.api.integration.vcmr.VespaChangeRequest;
+import org.junit.Test;
+
+import java.time.Instant;
+import java.time.ZonedDateTime;
+import java.util.List;
+
+import static org.junit.Assert.*;
+
+/**
+ * @author olaa
+ */
+public class ChangeRequestSerializerTest {
+
+ @Test
+ public void reserialization_equality() {
+ var source = new ChangeRequestSource("aws", "id321", "url", ChangeRequestSource.Status.STARTED, ZonedDateTime.now(), ZonedDateTime.now());
+ var actionPlan = List.of(
+ new HostAction("host1", HostAction.State.RETIRING, Instant.now()),
+ new HostAction("host2", HostAction.State.RETIRED, Instant.now())
+ );
+
+ var changeRequest = new VespaChangeRequest(
+ "id123",
+ source,
+ List.of("switch1"),
+ List.of("host1", "host2"),
+ ChangeRequest.Approval.APPROVED,
+ ChangeRequest.Impact.VERY_HIGH,
+ VespaChangeRequest.Status.IN_PROGRESS,
+ actionPlan,
+ ZoneId.defaultId()
+ );
+
+ var reserialized = ChangeRequestSerializer.fromSlime(ChangeRequestSerializer.toSlime(changeRequest));
+ assertEquals(changeRequest, reserialized);
+ }
+
+} \ No newline at end of file
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/ChangeManagementApiHandlerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/ChangeManagementApiHandlerTest.java
index cd815a2064b..c4412531f80 100644
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/ChangeManagementApiHandlerTest.java
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/ChangeManagementApiHandlerTest.java
@@ -12,6 +12,10 @@ import com.yahoo.vespa.hosted.controller.api.integration.noderepository.NodeOwne
import com.yahoo.vespa.hosted.controller.api.integration.noderepository.NodeRepositoryNode;
import com.yahoo.vespa.hosted.controller.api.integration.noderepository.NodeState;
import com.yahoo.vespa.hosted.controller.api.integration.noderepository.NodeType;
+import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequest;
+import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequestSource;
+import com.yahoo.vespa.hosted.controller.api.integration.vcmr.HostAction;
+import com.yahoo.vespa.hosted.controller.api.integration.vcmr.VespaChangeRequest;
import com.yahoo.vespa.hosted.controller.restapi.ContainerTester;
import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest;
import org.intellij.lang.annotations.Language;
@@ -19,6 +23,8 @@ import org.junit.Before;
import org.junit.Test;
import java.io.File;
+import java.time.Instant;
+import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
@@ -35,11 +41,14 @@ public class ChangeManagementApiHandlerTest extends ControllerContainerTest {
addUserToHostedOperatorRole(operator);
tester.serviceRegistry().configServer().nodeRepository().addNodes(ZoneId.from("prod.us-east-3"), createNodes());
tester.serviceRegistry().configServer().nodeRepository().putNodes(ZoneId.from("prod.us-east-3"), createNode());
+ tester.controller().curator().writeChangeRequest(createChangeRequest());
+
}
@Test
public void test_api() {
assertFile(new Request("http://localhost:8080/changemanagement/v1/assessment", "{\"zone\":\"prod.us-east-3\", \"hosts\": [\"host1\"]}", Request.Method.POST), "initial.json");
+ assertFile(new Request("http://localhost:8080/changemanagement/v1/vcmr"), "vcmrs.json");
}
private void assertResponse(Request request, @Language("JSON") String body, int statusCode) {
@@ -58,6 +67,28 @@ public class ChangeManagementApiHandlerTest extends ControllerContainerTest {
.build();
}
+ private VespaChangeRequest createChangeRequest() {
+ var instant = Instant.ofEpochMilli(9001);
+ var date = ZonedDateTime.ofInstant(instant, java.time.ZoneId.of("UTC"));
+ var source = new ChangeRequestSource("aws", "id321", "url", ChangeRequestSource.Status.STARTED, date, date);
+ var actionPlan = List.of(
+ new HostAction("host1", HostAction.State.RETIRING, instant),
+ new HostAction("host2", HostAction.State.RETIRED, instant)
+ );
+
+ return new VespaChangeRequest(
+ "id123",
+ source,
+ List.of("switch1"),
+ List.of("host1", "host2"),
+ ChangeRequest.Approval.APPROVED,
+ ChangeRequest.Impact.VERY_HIGH,
+ VespaChangeRequest.Status.IN_PROGRESS,
+ actionPlan,
+ ZoneId.defaultId()
+ );
+ }
+
private List<NodeRepositoryNode> createNodes() {
List<NodeRepositoryNode> nodes = new ArrayList<>();
nodes.add(createNode("node1", "host1", "default", 0 ));
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/responses/vcmrs.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/responses/vcmrs.json
new file mode 100644
index 00000000000..54d4ea8bcbd
--- /dev/null
+++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/responses/vcmrs.json
@@ -0,0 +1,40 @@
+{
+ "vcmrs": [
+ {
+ "id": "id123",
+ "status": "IN_PROGRESS",
+ "impact": "VERY_HIGH",
+ "approval": "APPROVED",
+ "zoneId": "prod.default",
+ "source": {
+ "system": "aws",
+ "id": "id321",
+ "url": "url",
+ "plannedStartTime": "1970-01-01T00:00:09.001Z[UTC]",
+ "plannedEndTime": "1970-01-01T00:00:09.001Z[UTC]",
+ "status": "STARTED"
+ },
+ "actionPlan": {
+ "hosts": [
+ {
+ "hostname": "host1",
+ "state": "RETIRING",
+ "lastUpdated": "1970-01-01T00:00:09.001Z"
+ },
+ {
+ "hostname": "host2",
+ "state": "RETIRED",
+ "lastUpdated": "1970-01-01T00:00:09.001Z"
+ }
+ ]
+ },
+ "impactedHosts": [
+ "host1",
+ "host2"
+ ],
+ "impactedSwitches": [
+ "switch1"
+ ]
+ }
+ ]
+} \ No newline at end of file