com.yahoo.vespa.hosted.provision.persistence.SnapshotSerializer Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of node-repository Show documentation
Show all versions of node-repository Show documentation
Keeps track of node assignment in a multi-application setup.
The newest version!
// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.provision.persistence;
import ai.vespa.secret.model.SecretVersionId;
import com.google.common.collect.ImmutableMap;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.CloudAccount;
import com.yahoo.config.provision.ClusterSpec;
import com.yahoo.config.provision.HostName;
import com.yahoo.config.provision.SnapshotId;
import com.yahoo.security.SealedSharedKey;
import com.yahoo.slime.ArrayTraverser;
import com.yahoo.slime.Cursor;
import com.yahoo.slime.Inspector;
import com.yahoo.slime.Slime;
import com.yahoo.slime.SlimeUtils;
import com.yahoo.vespa.hosted.provision.backup.Snapshot;
import com.yahoo.vespa.hosted.provision.backup.SnapshotKey;
import com.yahoo.vespa.hosted.provision.node.ClusterId;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
/**
* @author mpolden
*/
public class SnapshotSerializer {
private static final String SNAPSHOTS_FIELD = "snapshots";
private static final String ID_FIELD = "id";
private static final String HOSTNAME_FIELD = "hostname";
private static final String STATE_FIELD = "state";
private static final String INSTANCE_FIELD = "instance";
private static final String CLUSTER_FIELD = "cluster";
private static final String CLUSTER_INDEX_FIELD = "clusterIndex";
private static final String HISTORY_FIELD = "history";
private static final String EVENT_FIELD = "event";
private static final String AT_FIELD = "at";
private static final String CLOUD_ACCOUNT_FIELD = "cloudAccount";
private static final String SEALED_SHARED_KEY_FIELD = "sealedSharedKey";
private static final String SEALING_KEY_VERSION = "sealingKeyVersion";
private SnapshotSerializer() {}
public static Snapshot fromInspector(Inspector object, CloudAccount systemAccount) {
ImmutableMap.Builder history = ImmutableMap.builder();
object.field(HISTORY_FIELD).traverse((ArrayTraverser) (idx, inspector) -> {
Snapshot.State type = stateFromSlime(inspector.field(EVENT_FIELD).asString());
Instant at = Instant.ofEpochMilli(inspector.field(AT_FIELD).asLong());
history.put(type, new Snapshot.History.Event(type, at));
});
// TODO(mpolden): Require field after 2024-12-01
CloudAccount cloudAccount = SlimeUtils.optionalString(object.field(CLOUD_ACCOUNT_FIELD))
.map(CloudAccount::from)
.orElse(systemAccount);
Optional encryptionKey = Optional.empty();
if (object.field(SEALED_SHARED_KEY_FIELD).valid()) {
SealedSharedKey sharedKey = SealedSharedKey.fromTokenString(object.field(SEALED_SHARED_KEY_FIELD).asString());
SecretVersionId sealingKeyVersion = SecretVersionId.of(object.field(SEALING_KEY_VERSION).asString());
encryptionKey = Optional.of(new SnapshotKey(sharedKey, sealingKeyVersion));
}
return new Snapshot(SnapshotId.of(object.field(ID_FIELD).asString()),
HostName.of(object.field(HOSTNAME_FIELD).asString()),
stateFromSlime(object.field(STATE_FIELD).asString()),
new Snapshot.History(history.build()),
new ClusterId(ApplicationId.fromSerializedForm(object.field(INSTANCE_FIELD).asString()),
ClusterSpec.Id.from(object.field(CLUSTER_FIELD).asString())),
(int) object.field(CLUSTER_INDEX_FIELD).asLong(),
cloudAccount,
encryptionKey
);
}
public static Snapshot fromSlime(Slime slime, CloudAccount cloudAccount) {
return fromInspector(slime.get(), cloudAccount);
}
public static Slime toSlime(Snapshot snapshot) {
Slime slime = new Slime();
toSlime(snapshot, slime.setObject());
return slime;
}
public static List listFromSlime(Slime slime, CloudAccount systemAccount) {
Cursor root = slime.get();
return SlimeUtils.entriesStream(root.field(SNAPSHOTS_FIELD))
.map(i -> SnapshotSerializer.fromInspector(i, systemAccount))
.toList();
}
public static Slime toSlime(List snapshots) {
Slime slime = new Slime();
Cursor root = slime.setObject();
Cursor array = root.setArray(SNAPSHOTS_FIELD);
for (var snapshot : snapshots) {
toSlime(snapshot, array.addObject());
}
return slime;
}
public static void toSlime(Snapshot snapshot, Cursor object) {
object.setString(ID_FIELD, snapshot.id().toString());
object.setString(HOSTNAME_FIELD, snapshot.hostname().value());
object.setString(STATE_FIELD, asString(snapshot.state()));
object.setString(INSTANCE_FIELD, snapshot.cluster().application().serializedForm());
object.setString(CLUSTER_FIELD, snapshot.cluster().cluster().value());
object.setLong(CLUSTER_INDEX_FIELD, snapshot.clusterIndex());
Cursor historyArray = object.setArray(HISTORY_FIELD);
snapshot.history().events().values().forEach(event -> {
Cursor eventObject = historyArray.addObject();
eventObject.setString(EVENT_FIELD, asString(event.type()));
eventObject.setLong(AT_FIELD, event.at().toEpochMilli());
});
object.setString(CLOUD_ACCOUNT_FIELD, snapshot.cloudAccount().value());
snapshot.key().ifPresent(k -> {
object.setString(SEALED_SHARED_KEY_FIELD, k.sharedKey().toTokenString());
object.setString(SEALING_KEY_VERSION, k.sealingKeyVersion().value());
});
}
public static String asString(Snapshot.State state) {
return switch (state) {
case creating -> "creating";
case created -> "created";
case restoring -> "restoring";
case restored -> "restored";
};
}
private static Snapshot.State stateFromSlime(String value) {
return switch (value) {
case "creating" -> Snapshot.State.creating;
case "created" -> Snapshot.State.created;
case "restoring" -> Snapshot.State.restoring;
case "restored" -> Snapshot.State.restored;
default -> throw new IllegalArgumentException("Unknown snapshot state '" + value + "'");
};
}
}